Skip to content

Commit acb4aa7

Browse files
[#130] Explain SerializeReference in the dump docs, and say the format once
The dump page now leads with what a reader sees - a managedReference field holds a rid, not an object - and shows a script whose three fields demonstrate the points being made: two assigned the same instance, one left null. The previous example had a single field called "reference" of type "managedReference", which made it hard to tell which name came from the script and which from the format. The format itself was explained in five places in near-identical words. The full explanation now lives once, beside ProcessManagedReferenceRegistry, which is the code that needs it. ManagedReferenceRegistry keeps a shorter one because it is public API of the UnityFileSystem library and has to stand alone; the three walkers just say what their own line does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4381a38 commit acb4aa7

6 files changed

Lines changed: 96 additions & 48 deletions

File tree

‎Analyzer/PPtrAndCrcProcessor.cs‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,9 @@ public uint Process(long objectId, long offset, long size, TypeTreeNode node)
9999

100100
foreach (var child in node.Children)
101101
{
102-
// From SerializedFile version 25 the registry is a frame leading the C# class's data,
103-
// which no node describes; the flag marks the field it precedes. Only a root object's
104-
// fields carry one. The frame is not part of that field, so its references get their own
105-
// path root, named after the registry as the node-described versions are.
102+
// A version 3 registry sits here rather than in a node (see
103+
// ProcessManagedReferenceRegistry). It is not part of the field that follows it, so its
104+
// references get their own path root, named as the node-described versions are.
106105
if (child.HasSerializedRefs)
107106
{
108107
m_StringBuilder.Clear();
@@ -345,10 +344,17 @@ private void ProcessArray(TypeTreeNode node, bool isManagedReferenceRegistry, bo
345344
// version 1 - entries stored back to back and terminated by a sentinel type (see
346345
// ProcessManagedReferenceData); the rid is implied by position.
347346
// version 2 - entries stored as a "RefIds" array, each element carrying its own rid.
348-
// version 3 - from SerializedFile version 25 (Unity 6.7). No longer a node at all: the
349-
// registry is a self-delimiting frame in the object's data, ahead of the field
350-
// flagged HasSerializedRefs, holding a table of type names and a table of
351-
// records that index it. See ProcessManagedReferenceFrame.
347+
// version 3 - from SerializedFile version 25 (Unity 6.7). No node describes it at all: the
348+
// registry is a self-delimiting frame of raw bytes leading the C# class's own
349+
// data, so it sits after the built-in fields (m_GameObject, m_Name, ...) and
350+
// before the first field the script declares - which is the field flagged
351+
// HasSerializedRefs, whether or not that field is itself a reference. It holds a
352+
// table of type names and a table of records indexing into it; a record can be a
353+
// null entry, which v1 and v2 could not express. Only a root object's data
354+
// carries a frame: the same TypeTree used to lay out an instance's data inside
355+
// the registry keeps the flag but has no frame, which is why every walker honours
356+
// it only while iterating a root object's fields.
357+
// ManagedReferenceRegistry reads it; see ProcessManagedReferenceFrame here.
352358
private void ProcessManagedReferenceRegistry(TypeTreeNode node)
353359
{
354360
if (node.Children.Count < 2)

‎Documentation/command-dump.md‎

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -183,30 +183,80 @@ ID: -8138362113332287275 (ClassID: 135) SphereCollider
183183
z float 0
184184
```
185185

186-
A MonoBehaviour or ScriptableObject with `[SerializeReference]` fields also gets a `references`
187-
section. The fields themselves only store a `rid`; the instance each one points at is listed once in
188-
`references`, with its concrete C# type and its values:
186+
### `[SerializeReference]` fields
187+
188+
A field marked `[SerializeReference]` in C# appears in the dump with the type **`managedReference`**,
189+
and its value is not the object - it is a number called a **`rid`** (reference id). The objects
190+
themselves are listed together in a `references` section, once each, with their concrete C# type and
191+
their field values.
192+
193+
Take this script:
194+
195+
```csharp
196+
public class Inventory : MonoBehaviour
197+
{
198+
[Serializable]
199+
public class Item
200+
{
201+
public string name;
202+
public int count;
203+
}
204+
205+
[SerializeReference] public Item primary;
206+
[SerializeReference] public Item backup;
207+
[SerializeReference] public Item spare;
208+
}
209+
```
210+
211+
with `primary` and `backup` both assigned the *same* `Item`, and `spare` left null. Dumping it:
189212

190213
```
191-
m_Name (string) ScriptableObjectWIthSerializeReference
214+
ID: 3862108129085620391 (ClassID: 114) MonoBehaviour
215+
m_GameObject (PPtr<GameObject>)
216+
m_FileID (int) 0
217+
m_PathID (SInt64) -5904263129458716409
218+
m_Enabled (UInt8) 1
219+
m_Script (PPtr<MonoScript>)
220+
m_FileID (int) 1
221+
m_PathID (SInt64) 1197423208934291241
222+
m_Name (string)
192223
references (ManagedReferenceRegistry)
193224
version (int) 3
194-
rid(6911265806470873295) ReferencedObject
225+
rid(-2) ReferencedObject
226+
null
227+
rid(3218405420927320064) ReferencedObject
195228
type (ReferencedManagedType)
196-
class (string) Data
197-
ns (string) MyNamespace
229+
class (string) Inventory/Item
230+
ns (string)
198231
asm (string) Assembly-CSharp
199232
data ReferencedObjectData
200-
Info (string) Some info
201-
Flag (UInt8) 1
202-
reference (managedReference)
203-
rid (SInt64) 6911265806470873295
233+
name (string) Health potion
234+
count (int) 3
235+
primary (managedReference)
236+
rid (SInt64) 3218405420927320064
237+
backup (managedReference)
238+
rid (SInt64) 3218405420927320064
239+
spare (managedReference)
240+
rid (SInt64) -2
204241
```
205242

206-
Two fields assigned the same instance share a `rid`, so it still appears only once. A field set to
207-
null shows `rid (SInt64) -2`, listed as a `null` entry. The `version` line reflects how the registry
208-
is stored, which changed in Unity 6.7 - the entries mean the same thing either way, but the section
209-
appears after the referencing fields in older files and before them in 6.7 and later.
243+
Reading it:
244+
245+
* **`primary` and `backup` show the same `rid`.** They are two references to one object, so the
246+
object is listed once and the sharing is visible - this is the whole point of
247+
`[SerializeReference]` over a plain serialized field, which would have stored two independent
248+
copies.
249+
* **`spare` shows `rid (SInt64) -2`**, the marker for null, and the matching entry is listed as
250+
`null` with no type or data.
251+
* **`class` is the concrete runtime type**, which can be a subclass of the field's declared type -
252+
that is what `[SerializeReference]` is for. Nested classes use a `/`, as in `Inventory/Item`.
253+
* The entries are listed in the order the file stores them, which is not necessarily the order the
254+
fields appear in.
255+
256+
The `version` line describes how the registry is stored rather than anything about your data. It is
257+
`3` for content built with Unity 6.7 or newer and `2` before that, and in older files the
258+
`references` section appears *after* the fields instead of before them. The entries mean the same
259+
thing either way.
210260

211261
**Refer to the [TextDumper documentation](textdumper.md) for detailed output format explanation.**
212262

‎TextDumper/TextDumperTool.cs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,8 @@ void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex
340340
{
341341
foreach (var child in node.Children)
342342
{
343-
// From SerializedFile version 25 the registry is a frame leading the C# class's
344-
// data, described by no node; the flag marks the field it precedes. Only a root
345-
// object's data carries one; the same tree read as a registry blob is frameless.
343+
// A version 3 registry sits in the data before this field, with no node of its
344+
// own. Only a root object carries one, hence isRootObject.
346345
if (isRootObject && child.HasSerializedRefs)
347346
DumpManagedReferenceFrame(ref offset, level + 1);
348347

@@ -488,8 +487,7 @@ void DumpManagedReferenceRegistry(TypeTreeNode node, ref long offset, int level)
488487
}
489488
}
490489

491-
// Dumps a version 3 registry, which is a frame in the object's data rather than a node. The
492-
// output is shaped like the version 1 and 2 dumps above so the three stay comparable.
490+
// Shaped like the version 1 and 2 dumps above, so the three stay comparable in a diff.
493491
void DumpManagedReferenceFrame(ref long offset, int level)
494492
{
495493
var registry = ManagedReferenceRegistry.ReadFrame(m_Reader, offset, m_ObjectEnd - offset);

‎UnityFileSystem/ManagedReferenceRegistry.cs‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,16 @@ public sealed class ManagedReferenceEntry
2323
public long DataOffset { get; init; }
2424
}
2525

26-
// The [SerializeReference] instances owned by one serialized object.
26+
// The [SerializeReference] instances owned by one serialized object, however the file stores them.
2727
//
28-
// From SerializedFile version 25 the registry is a self-delimiting frame leading the C# class's own
29-
// data - after the built-in fields, before the first field the script declares, which is the one
30-
// flagged HasSerializedRefs. It is the first piece of object layout no TypeTree node describes, so
31-
// the tables inside it cannot be walked the way every other field is.
28+
// From SerializedFile version 25 they live in a self-delimiting frame of raw bytes that no TypeTree
29+
// node describes, sitting immediately before the field flagged HasSerializedRefs. Earlier files
30+
// describe the registry with nodes, and callers read those through the node walk into these same
31+
// entries.
3232
//
33-
// They are not parsed here either: UFS_GetRegistryFrame* wraps the engine's own frame parser, so
34-
// this class calls into the one implementation that exists rather than becoming a second one. Only
35-
// the 8-byte header is read directly, in GetFrameSize, which is all a walker needs to step over a
36-
// frame it does not want to read.
37-
//
38-
// Earlier files describe the registry with TypeTree nodes instead (versions 1 and 2 of the registry
39-
// itself), which the callers read through the node walk and present as the same entries.
33+
// The frame's tables are not parsed here: UFS_GetRegistryFrame* wraps the engine's own parser, so
34+
// this calls the one implementation that exists rather than becoming a second one. Only the 8-byte
35+
// header is read directly, in GetFrameSize.
4036
public sealed class ManagedReferenceRegistry
4137
{
4238
// The only frame layout the native parser accepts. Unrelated to the serialized file version.

‎UnityFileSystem/TypeTreeNode.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ public class TypeTreeNode
4949
// True if the field is a ManagedReferenceRegistry
5050
public bool IsManagedReferenceRegistry => ((int)Flags & (int)TypeTreeFlags.IsManagedReferenceRegistry) != 0;
5151

52-
// True if the [SerializeReference] registry frame precedes this field's data. The flag rides
53-
// the first field of the declaring class, whether or not that field is itself a reference. Only
54-
// set on a root object's fields; the same tree read as a registry blob carries no frame.
52+
// True if a version 3 [SerializeReference] registry sits in the data immediately before this
53+
// field. The flag rides the declaring class's first field, reference or not, and is meaningful
54+
// only when the tree is read as an object root. See ManagedReferenceRegistry.
5555
public bool HasSerializedRefs => ((int)Flags & (int)TypeTreeFlags.HasSerializedRefs) != 0;
5656

5757
// True if the node stands in for a compound the file stores once and shares between types

‎UnityFileSystem/TypeTreeReaders/RandomAccessReader.cs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ public class RandomAccessReader : IEnumerable<RandomAccessReader>
2727
Dictionary<string, RandomAccessReader> m_ChildrenCacheObject;
2828
List<RandomAccessReader> m_ChildrenCacheArray;
2929
private TypeTreeNode m_TypeTreeNode;
30-
// Only an object's root data carries a [SerializeReference] registry frame; the same type tree
31-
// read as a registry blob does not, so the frame is honoured in root context only.
30+
// A registry frame is honoured in root context only; a referenced instance's data has none.
3231
bool m_IsRoot;
3332
ManagedReferenceRegistry m_Registry;
3433
// Where this object's registry frame starts, once the field walk has passed it. -1 until then,
@@ -351,9 +350,8 @@ RandomAccessReader GetChild(string name)
351350
{
352351
var child = m_TypeTreeNode.Children[i];
353352

354-
// From SerializedFile version 25 the registry is a frame leading the C# class's data,
355-
// described by no node, so the marked field's data starts past it. Only its size is
356-
// needed to get there; Registry reads the tables if anyone asks for them.
353+
// A version 3 registry sits in the data before this field, so the field starts past
354+
// it. Only its size is needed to get there; Registry reads the tables on demand.
357355
if (m_IsRoot && child.HasSerializedRefs)
358356
{
359357
m_RegistryFrameOffset = offset;

0 commit comments

Comments
 (0)