[#130] Support SerializedFile v26 (Unity 6.7) - #145
SkowronskiAndrew wants to merge 9 commits into
Conversation
Unity 6.7 moves the format from v23 to v26 through three cumulative changes: TypeTrees carry their own version (24), the [SerializeReference] registry moves into the object's data as a frame no TypeTree node describes (25), and the file gains a table of shared subtrees its types reference (26). The native library already has the external-tool half of this, so the C# side adopts those entry points rather than reimplementing the layouts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The version 22 and version 26 builds of one project are the same content either side of the format change, so most of these assert the two agree rather than pinning v26 on its own. The shared subtree checks are the ones worth keeping in mind: such a node has a size and no children, so a reader that takes it at face value drops the compound's fields without failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The docs cover every Unity version UnityDataTool reads, so the 6.7 changes are described as a difference from 6.6 rather than as the way things are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds Unity's own v26 fixture, which carries a null reference, a PPtr inside a referenced object, a collection of references, a reference nested in another, a field-less compound, and plain fields either side of the registry - the last being what catches a reader that does not step over the frame, since it reads them shifted rather than failing. Also roots a framed registry's property paths at the registry rather than at the field whose data happens to follow it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The trees are finite - the deepest in the fixtures is 9 - so the depth limit was guarding against nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cleanup from a review pass, no behaviour change except where noted. The registry frame reader now throws on a malformed frame instead of returning null, which removes the identical null-check and message that all three walkers had copied. The dumper says "root object" rather than inferring it from recursion depth, and its output-writing ritual collapses into one helper. The analyzer's two registry paths now share the walk into a referenced object's data, which is also what stops their property paths from drifting apart. RandomAccessReader takes the object's size, so all three walkers bound the frame read by the object rather than one of them by the whole file, and it reads only the frame's header while walking fields - the tables are read if Registry is asked for. dump now reports an unsupported version the way analyze does, rather than leaving the native loader to call it a possibly corrupt file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
AGENT: CI is red on all three platforms for a single expected reason, not a test regression: The checked-in native library is still version 1, and reading v26 needs the shared-subtree and The suite is worth re-running once those land, since only the Windows debug build has been exercised |
| public void TryParseMetadata_VersionTooNew_ReturnsFalseWithMessage() | ||
| { | ||
| var headerInfo = new SerializedFileInfo { Version = 24 }; | ||
| var headerInfo = new SerializedFileInfo { Version = 27 }; |
There was a problem hiding this comment.
Perhaps we should set this to 99 or other much later version, so that there is less churn as the version changes.
Line 199 would still need to change, unless there is a way we can reach the actual maximum supported constant so that there is less maintenance burden for this test.
There was a problem hiding this comment.
Done — 99, and the expected-maximum assertion now reads SerializedFileDetector.MaxMetadataParseVersion instead of a literal, so neither line churns. The constant is public for that reason.
| Shows information from the metadata section of a SerializedFile. This includes the Unity version, target platform, TypeTree storage mode (inline, external, or absent), and counts of the type entries recorded in the file. The JSON output includes additional per-type details; see the notes below. | ||
|
|
||
| Requires SerializedFile version 19 (Unity 2019.1) or newer. Files older than version 19 are not supported by this subcommand. | ||
| Requires SerializedFile version 19 (Unity 2019.1) or newer, up to version 26 (Unity 6.7). Files |
There was a problem hiding this comment.
I don't like having the current supported version hardcoded here, we may neglect to update that when new versions come up. We intend to support newer versions as they arrive with new Unity versions, although there can be a time lag. Users of UnityDataTools will often be on older versions of Unity than the current beta or brand new non-LTS release.
We have a runtime error if we don't support a version so perhaps that is better than trying to keep a documentation up to date,
There was a problem hiding this comment.
Done — the version ceiling is gone from the docs. It now says a file from a newer Unity is refused rather than misread, with a message naming the version found and the highest understood, and points at header as the way to check what you have.
| instance by its `rid`, its concrete C# type and its data. Where that section appears depends on the | ||
| Unity version: up to Unity 6.6 the registry is the last field of the object, and from Unity 6.7 it | ||
| precedes the first field that can hold a reference. The reported `version` is `2` before 6.7 and `3` | ||
| from 6.7. See [TypeTrees](unity-content-format.md#typetrees) for what changed. |
There was a problem hiding this comment.
This is quite technical and there is no example. Its not very clear whether the actual dump output would look different between these two versions, or we are just describing an inner detail about the binary representation of v2 and v3.
The comment in Analyzer/PPtrAndCrcProcessor.cs is a good place to get into technical details of the different supported versions because undertanding that actual parsing code requires some explaination. Meanwhile this is the doc for the user facing dump command.
Here it can be worth mentioning SerializeReference output for dump because that is something interesting that users would see when looking at some MonoBehaviors. But I think having a short example is important, possibly just showing an example of the new expected dump output, e.g. v3 instead of getting into the history.
There was a problem hiding this comment.
Done — replaced the history paragraph with an actual example of the output (the references section for ScriptableObjectWIthSerializeReference, taken from the test data), plus one line each on the two things a user actually sees: two fields sharing a rid, and a null reference showing rid -2. The format history is left to the PPtrAndCrcProcessor comment and unity-content-format.md.
On your question of whether the dump output differs between v2 and v3: the entries are identical, and only two things change — the version number, and that the section is printed before the referencing fields in 6.7 instead of after. That is now one sentence at the end rather than the bulk of it.
| "Data", "PlayerWithTypeTreesV26", "sharedassets1.assets")); | ||
|
|
||
| // The version is a big-endian uint32 at offset 8 of the header. | ||
| BinaryPrimitives.WriteUInt32BigEndian(bytes.AsSpan(8), 27); |
There was a problem hiding this comment.
again i think a number like 99 would be better than the next expected version, so we don't have to keep churning this test as new versions arrive.
There was a problem hiding this comment.
AGENT: Agreed on the intent, but 99 does not work here specifically. This test goes through real detection, and TryDetectSerializedFile only accepts a version in 1..50 as a plausible SerializedFile header — a version-99 file is not recognised as a SerializedFile at all, so analyze ignores it and exits 0 rather than failing. (The other test hands TryParseMetadata a header directly and bypasses that, which is why 99 is fine there.)
So this one derives it: SerializedFileDetector.MaxMetadataParseVersion + 1, with both assertions built from the same constant. No churn, and it stays inside the detector's plausible range. Happy to raise that 1..50 bound instead if you would rather both tests read the same way, but it is a heuristic that helps reject non-SerializedFiles, so I left it alone.
| // sitting immediately before the data of the field flagged HasSerializedRefs. No TypeTree node | ||
| // describes it, so it is read through the native parser rather than reimplemented here - it is the | ||
| // first piece of object layout a TypeTree cannot express, and a second implementation of a layout | ||
| // documented only in the engine source would drift from it. |
There was a problem hiding this comment.
I don't understand this comment. Aren't we forced to implement that layout as a second implementation? Is it saying there is a risk of this drifting?
There was a problem hiding this comment.
AGENT: Fair — the comment was badly worded and said the opposite of what I meant. We are not writing a second implementation: UFS_GetRegistryFrameInfo/Types/Records wrap the engine's own ParseRegistryTableRaw, so the only parser of that layout is Unity's, and this class calls it. The one exception is the 8-byte header (version + byteLength), read directly in GetFrameSize, because a walker that only needs to step over a frame should not pay to parse its tables. Rewritten to say that.
…comments The two tests that need an unsupported version now derive it from SerializedFileDetector.MaxMetadataParseVersion, which is public for that reason, so neither churns as Unity adds versions. The serialized-file docs no longer name the ceiling either - the runtime error reports it. The dump docs show what a SerializeReference object actually looks like instead of describing how the format changed, and the registry class says plainly that it calls the engine's frame parser rather than being a second implementation of that layout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sits RandomAccessReader.GetSize asked for the last child of a node with no children, which threw for a [Serializable] class with no serialized fields. Such a node occupies whatever its own size says, normally nothing. Exposed by the new managedreferences fixture, whose ShapeNoData field sits between the references and a plain int that has to read correctly after it. The comments describing the registry frame said it precedes the first field that can hold a reference. It does not: it leads the declaring class's own data, so the flag rides that class's first field whether or not that field is a reference. In the fixture it is a plain int. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t 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>
Summary
Fixes #130.
Unity 6.7 moves the SerializedFile format from version 23 to version 26 through three cumulative
changes, two of which alter how a file has to be read.
dumpandanalyzedid not work on 6.7output at all, and most
serialized-filesubcommands refused it. This makes all of them work,while continuing to read every older version.
What changed in the format, from
Runtime/Serialize/SerializedFileFormatVersion.h:TypeTreeFormatVersion(32 and up) instead of the file's. The metadata layout is unchanged.[SerializeReference]registry becomes a self-delimiting frame in the object's data, ahead of the first field that can hold a reference. No TypeTree node describes it - the first piece of object layout a TypeTree cannot express.Only
<23,23and26occur in the wild - 24 and 25 shipped inside the 6.7 alpha - but thechanges are cumulative, so all three matter.
The shared-subtree change is the dangerous one. Such a node has a byte size and no children of its
own, which is exactly how every reader of this API recognises a basic type, so a naive reader
takes a fixed-size compound for a primitive of the same width, drops its fields - PPtrs among them -
and still lands in the right place afterwards. Unity's
UnityFileSystemApitherefore now refusesthat node through
UFS_GetTypeTreeNodeInfoand serves it through a subtree API instead. Thatrefusal, the registry frame parser, and the independent TypeTree version all arrived in the native
library in Unity commit
e06bfae736e1, whose message says outright that the UnityDataTools side isa separate PR pinning that library. This is that PR.
Where the layout is documented only in the engine source, this uses the library's entry points
rather than re-implementing it. The exception is
SerializedFileDetector, which deliberately parsesmetadata in pure C# without the native library, and which gains the v26 metadata changes directly.
Changes
Native library and its C# wrapper
UnityFileSystemApiversion 2 or newer, checked once inUnityFileSystem.Initwitha message naming what is needed. Older files are read through the same library, so there is
nothing to gain from a fallback. This branch does not contain that library yet - see Before
merging.
DllWrappergainsGetTypeTreeSubtreeNodeInfo,GetTypeTreeRefSubtreeand the threeGetRegistryFrame*entry points, theHasSerializedRefs/IsSharedSubtreeRefnode flags, andthe
HigherTypeTreeVersion/RequiresSubtreeApiresult codes.TypeTreeNodenow reads every node through the subtree API, passing a null subtree for thetree's own nodes, so one walker covers both sides and there is no second path to keep in step. A
shared subtree reference resolves its children through
GetTypeTreeRefSubtree, and is neverreported as a basic type.
RequiresSubtreeApithrows, so a walk that bypassesTypeTreeNodefails loudly rather than silently misreading.
ManagedReferenceRegistrymodels the registry:Version,FrameSizeand the entries itholds. Versions 1 and 2 are read from their TypeTree nodes into the same entries, so consumers see
one shape whatever the file version. It is public API of the
UnityFileSystemlibrary, whichAGENTS.mddescribes as reusable for other analysis; inside this repo the frame path is what thethree walkers use.
Reading objects
The registry frame sits ahead of the field flagged
HasSerializedRefs, in root context only - thesame type tree read as a registry blob is frameless.
TextDumperTool,PPtrAndCrcProcessorandRandomAccessReadereach step over it and read it; their walks stay separate because each maintainsits own cursor and does something different with the result, but the frame parsing itself is shared.
dumpprints the registry in the shape the version 1 and 2 dumps already used, so the three staycomparable. It also prints a null reference, which only the version 3 format can express.
analyzewalks each instance's data through its own type tree for PPtrs, as it already did forversions 1 and 2, and CRCs the frame's header and tables as the raw bytes they are. Note that a
v23 and a v26 build of the same content will not have equal CRCs; the CRC is a walk-derived
fingerprint, not a hash of the object's bytes (see CRC of objects with references is not comparable across separate databases #74).
serialized-file
layout is identical to 23, so refusing a layout we can read would be a failure with nothing behind
it.
metadatareports the shared subtree table, and in JSON each subtree's content hash, size andwhether it is inline. Each type entry also reports
typeTreeFormatVersion, the stamp on the blobitself - 0 before version 23, 23 for version 23 files, 32 or higher from 6.7. That is the only way
to see a TypeTree change that no longer bumps the file version. Cross-referenced with Expose TypeTree information for SerializedFile #54, which
covers exposing TypeTree information properly.
Error reporting
analyzesurfaced an unreadable version asSequence contains no elements(reported in #130 whiletesting #128). The version is known before the file is opened, so both
analyzeanddumpnow saywhich version they found and which is the highest they understand, instead of leaving the native
loader to call it a possibly corrupt file.
Documentation
unity-content-format.mdgains a "What changed in Unity 6.7" section under TypeTrees. Because thedocs cover every version UnityDataTool reads, the 6.7 behaviour is described as a difference from
6.6 rather than as the way things are.
command-serialized-file.mdandcommand-dump.mdfollow.Test data
TestCommon/Data/PlayerWithTypeTreesV26/- a 6.7 build of the same project and the same twoscenes as the existing
PlayerWithTypeTrees(which is version 22, not 23). Same contenteither side of the format change, so most of the new tests assert the two agree rather than
pinning v26 on its own. 933 KB, against 814 KB for its older sibling.
TestCommon/Data/AssetBundleTypeTreeVariations/v26/managedreferences.bundle- Unity's own v26fixture, copied from its test corpus, which the two repos already share a source script with. It
carries the
[SerializeReference]shapes nothing else here reaches: a null reference, a PPtrinside a referenced object, a collection of references, a reference nested in another, a
field-less compound, and a plain
intbefore and after the references - that last pair being whatcatches a reader that does not step over the frame, since it reads them shifted rather than
failing.
TestCommon/Data/LegacyFormats/v26format.assets- one file pegged at v26, since LeadingEdge movesforward with Unity.
Testing
dotnet test- 821 pass, 0 fail (802 before this branch). The pre-existing tests over v17 to v23files are the backward-compatibility guard that matters most here, since
TypeTreeNodenow routesevery node through the subtree API, for old files as much as new.
New coverage: shared subtree resolution and the absence of those nodes in a v22 file; the registry
read through all three of its on-disk versions; the detector's shared subtree table and blob version
stamps across v22, v23 and v26; the registry shapes listed above; and the unsupported-version
message.
Verified by hand against the full 48 MB 6.7 player build, not just the checked-in subset:
dumpproduces clean output for every serialized file in it, and
analyzecompletes with no failures. Thev22 and v26 builds of
level0produce identical analyze graphs (7 objects, 13 references), andtheir dumps differ only by a genuinely new 6.7 field (
m_CAHMap) and by PathIDs.Before merging
UnityFileSystemApilibrary is not in this branch. The checked-in library isstill version 1, which the new
Initcheck rejects, so CI will fail on every platform untilofficial Windows, macOS and Linux builds from Unity 6.7 are committed here. Everything above was
developed and verified against a local Windows debug build of
6000.7.0b2(
UFS_GetDllVersion= 2), which was deliberately kept out of the commits. The suite should bere-run once the official libraries land, since only the Windows debug build has been exercised.
ContentLayout.json) and Unity 6.7 - Support unity archive format v9 #144 (6.7 archive format) are separate 6.7problems, deliberately out of scope here.
UnityProjects/LeadingEdgewas not rebuilt with6.7, because doing so would break ContentLayout and Archive tests for those unrelated reasons; the
coverage it was wanted for is provided by the fixtures above. The version bump and release also
wait for the point where all the 6.7 fixes are assembled onto one branch.
🤖 Generated with Claude Code