From 978b5a370ddc4d365ff31808be0038495ae7b999 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Sun, 5 Jul 2026 22:51:24 -0400 Subject: [PATCH 1/5] Fix one part of Issue 81 - handling of PreloadData object for Player builds Also flesh out the documentation for these tables to precisely explain what they contain --- Analyzer/Resources/AssetBundle.sql | 7 ++- .../SQLite/Handlers/PreloadDataHandler.cs | 19 +++++-- .../Writers/SerializedFileSQLiteWriter.cs | 9 +-- Documentation/analyzer.md | 56 ++++++++++++++++--- .../UnityDataToolPlayerDataTests.cs | 28 ++++++++++ 5 files changed, 99 insertions(+), 20 deletions(-) diff --git a/Analyzer/Resources/AssetBundle.sql b/Analyzer/Resources/AssetBundle.sql index 1040018..ed7ae3a 100644 --- a/Analyzer/Resources/AssetBundle.sql +++ b/Analyzer/Resources/AssetBundle.sql @@ -18,9 +18,10 @@ CREATE TABLE IF NOT EXISTS assetbundle_assets( -- * AssetBundleHandler: an asset's slice of the AssetBundle object's m_PreloadTable. -- * SerializedFileSQLiteWriter: a scene object -> each object in the scene's SerializedFiles. -- * PreloadDataHandler: the PreloadData object's m_Assets. PreloadData is a *separate* Unity --- object (not part of the AssetBundle object) and also exists in Player builds (one per scene in its sharedAsset file), --- so this table is NOT empty there; but those builds have no scene object, so PreloadDataHandler currently --- attributes the rows to object id -1 (issue 81). +-- object (not part of the AssetBundle object) and also exists in Player builds (one per scene +-- in its sharedAssetsN.assets, plus one in globalgamemanagers.assets), so this table is NOT +-- empty there. Player builds have no scene object, so those rows hang off the PreloadData +-- object itself; scene bundles hang them off the synthetic Scene object. CREATE TABLE IF NOT EXISTS preload_dependencies( object INTEGER, dependency INTEGER diff --git a/Analyzer/SQLite/Handlers/PreloadDataHandler.cs b/Analyzer/SQLite/Handlers/PreloadDataHandler.cs index 7d57a90..588b9a7 100644 --- a/Analyzer/SQLite/Handlers/PreloadDataHandler.cs +++ b/Analyzer/SQLite/Handlers/PreloadDataHandler.cs @@ -6,11 +6,16 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; -// Processes the PreloadData object found in scene bundles. Its m_Assets list is recorded as the -// scene's dependencies (preload_dependencies), so it is meaningful only alongside a synthetic -// Scene object. This is AssetBundle-specific in practice: Player builds also contain a PreloadData -// object but have no scene object, so ctx.SceneId is -1 there and the rows below are written -// against object id -1 (a limitation, not intended output). ContentDirectory builds have neither. +// Processes the PreloadData object, recording its m_Assets list as preload_dependencies. The +// "object" the dependencies hang off of depends on the build: +// * Scene bundle: the synthetic Scene object (ctx.SceneId), which also aggregates the scene's +// own content. +// * Player build: there is no scene object (ctx.SceneId == -1), so the dependencies are hung off +// the PreloadData object itself. A player build has one PreloadData per scene (in its +// sharedassetsN.assets) plus one in globalgamemanagers.assets for the always-loaded set. +// A dependency may resolve to an object that analyze never tracked (e.g. objects in +// "unity default resources", which normally has no TypeTrees), leaving a row whose dependency has +// no objects-table entry. ContentDirectory builds have no PreloadData object. public class PreloadDataHandler : ISQLiteHandler { private SqliteCommand m_InsertDepCommand; @@ -28,7 +33,9 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s { var preloadData = PreloadData.Read(reader); m_InsertDepCommand.Transaction = ctx.Transaction; - m_InsertDepCommand.Parameters["@object"].Value = ctx.SceneId; + // Scene bundles hang the dependencies off the synthetic Scene object; player builds have + // none, so they hang off the PreloadData object itself. + m_InsertDepCommand.Parameters["@object"].Value = ctx.SceneId != -1 ? ctx.SceneId : objectId; foreach (var asset in preloadData.Assets) { diff --git a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs index 86cbe13..0d36685 100644 --- a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs +++ b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs @@ -41,10 +41,11 @@ public class SerializedFileSQLiteWriter : IDisposable // LIMITATION (issue 81): this only matches the BuildPipeline.BuildAssetBundles naming convention // ("BuildPlayer-"). It does NOT match scene bundles produced by the Scriptable // Build Pipeline / Addressables ("CAB-") or the Multi-Process Build - // Pipeline ("CAB-"), nor player-build scenes ("level0", "level1", ...). For - // those builds no synthetic Scene object is created (see the scene handling in - // WriteSerializedFile), so the scene rows AssetBundleHandler writes into assetbundle_assets - // end up dangling and PreloadDataHandler attributes preload dependencies to SceneId -1. + // Pipeline ("CAB-"), nor player-build scenes ("level0", "level1", ...), so no + // synthetic Scene object is created for those. For Scriptable Build Pipeline / Addressables + // scene bundles that still leaves the assetbundle_assets rows AssetBundleHandler writes + // dangling (unfixed part of issue 81). Player builds have no AssetBundle object; their + // PreloadData dependencies are attributed to the PreloadData object itself (PreloadDataHandler). private Regex m_RegexSceneFile = new(@"BuildPlayer-([^\.]+)(?:\.sharedAssets)?"); // Rebuilt for each serialized file: maps a PPtr's local m_FileID (0 = this file, 1..N = an diff --git a/Documentation/analyzer.md b/Documentation/analyzer.md index 2ecf285..2d5aeea 100644 --- a/Documentation/analyzer.md +++ b/Documentation/analyzer.md @@ -52,16 +52,58 @@ it should be very accurate because CRCs are used to determine if objects are ide ## assetbundle_asset_view (AssetBundleProcessor) -This view lists all the assets that have been explicitly assigned to AssetBundles. The dependencies -that were automatically added by Unity at build time won't appear in this view. The columns are the -same as those in the *object_view* with the addition of the *asset_name* that contains the filename -of the asset. +Lists the assets that were explicitly assigned to AssetBundles, one row per entry in the AssetBundle +object's `m_Container`. Dependencies that Unity pulled in automatically at build time are not listed +here (see `preload_dependencies_view`). The columns are those of *object_view* plus `asset_name`, +the container path of the asset. + +This data comes from the AssetBundle Unity object, so the view is only populated for AssetBundle +builds; Player and ContentDirectory builds have no such object and it is empty for them. For a scene +bundle the container entry names the scene (its `.unity` path) and points at a synthetic object of +type `Scene` (there is no single Unity object that represents a scene). + +The view is built with an INNER JOIN to *object_view*, so any container entry whose object is not in +the `objects` table is silently omitted. This currently happens for scene bundles built by the +Scriptable Build Pipeline / Addressables (and the Multi-Process Build Pipeline), where the synthetic +Scene object is not created (see issue #81); the underlying `assetbundle_assets` table still holds +those rows. + +## preload_dependencies + +This table records preload relationships as `object` -> `dependency` id pairs, where both are +`objects.id` values. A "dependency" is an object that Unity preloads / pulls in alongside another +object. The rows come from the AssetBundle and PreloadData Unity objects, so the table is populated +for AssetBundle and Player builds but not ContentDirectory builds (which have neither object). + +What the `object` is depends on the build: + +- **AssetBundle asset**: the explicitly-assigned asset, with its dependencies taken from the + AssetBundle object's preload table. +- **Scene bundle**: a synthetic `Scene` object that also aggregates the scene's own content objects. +- **Player build**: the `PreloadData` object itself, because a player build has no scene object to + attach the dependencies to. A player build has one `PreloadData` per scene (in its + `sharedassetsN.assets`) plus one in `globalgamemanagers.assets` for the always-loaded set. + +The `dependency` side can reference an object that analyze never recorded, leaving a "dangling" id +with no matching `objects` row. The most common case is objects in `unity default resources`, the +built-in resource file that ships with the Unity Editor without TypeTrees and so cannot be analyzed +without a specially built copy. (`Resources/unity_builtin_extra` is built alongside your content and +can be analyzed, but produces the same dangling references when it is not part of the analyzed set.) ## preload_dependencies_view (AssetBundleProcessor) -This view lists the dependencies of all the assets. You can filter by id or asset_name to get all -the dependencies of an asset. Conversely, filtering by dep_id will return all the assets that -depend on this object. This can be useful to figure out why an asset was included in a build. +A convenience view over `preload_dependencies` that resolves the ids to readable columns: it joins +each row's `object` to `assetbundle_asset_view` and its `dependency` to *object_view*. Filter by `id` +or `asset_name` for the dependencies of one asset, or by `dep_id` for everything that depends on a +given object (useful for figuring out why an object was included in a build). + +Because of those inner joins the view is narrower than the underlying table: + +- It only includes rows whose `object` is an AssetBundle asset or scene (i.e. present in + `assetbundle_asset_view`). Player-build rows hang off a `PreloadData` object rather than an + AssetBundle asset, so they do not appear here - query the `preload_dependencies` table directly. +- It drops rows whose `dependency` is a dangling id (see above), because those have no *object_view* + row to join to. ## monoscripts diff --git a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs index 3e84682..686a6f5 100644 --- a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs +++ b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs @@ -67,6 +67,34 @@ public async Task Analyze_PlayerData_DatabaseCorrect() Assert.AreEqual(1, reader.GetInt32(4)); } + [Test] + public async Task Analyze_PlayerDataPreloadDependencies_ObjectResolvesToRealObject() + { + // Regression test for issue #81 (player-build case): a player build's PreloadData + // dependencies were all attributed to object id -1. They must instead hang off a real + // object (the PreloadData object itself, since a player build has no scene object). + // PlayerWithTypeTrees exercises this - it has PreloadData in its sharedassetsN.assets and + // in globalgamemanagers.assets. + var analyzePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "PlayerWithTypeTrees"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath, "-o", databasePath })); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + // The build has PreloadData objects, so there must be rows to validate. + Assert.Greater(SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM preload_dependencies"), 0); + + // Every row's 'object' must be a real, tracked object: never -1, never a dangling id. + // (The 'dependency' side is intentionally allowed to dangle, e.g. references into + // "unity default resources", which is not analyzed - see analyzer.md.) + SQLTestHelper.AssertQueryInt(db, + "SELECT COUNT(*) FROM preload_dependencies WHERE object = -1", + 0, "preload_dependencies.object should never be -1 (issue #81)"); + SQLTestHelper.AssertQueryInt(db, + "SELECT COUNT(*) FROM preload_dependencies d LEFT JOIN objects o ON o.id = d.object WHERE o.id IS NULL", + 0, "every preload_dependencies.object should resolve to an objects row"); + } + [Test] public async Task DumpText_PlayerData_TextFileCreatedCorrectly() { From d46c6af446dfa1e17c962484c28449f15281fb6b Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Mon, 6 Jul 2026 06:58:06 -0400 Subject: [PATCH 2/5] [#81] Track scenes in Scriptable Build Pipeline / Addressables bundles SBP/Addressables name scene files "CAB-" (and "CAB-.sharedAssets"), which the BuildPlayer- scene regex never matched, so no synthetic Scene object was created: assetbundle_assets scene rows dangled and the scene->dependency relationship was lost. Use the AssetBundle object's m_SceneHashes (scene path -> scene SerializedFile) to key a synthetic Scene object on the scene's file. AssetBundleHandler creates that object and its assetbundle_assets row; SerializedFileSQLiteWriter resolves the same id for a ".sharedAssets" file so PreloadDataHandler and the content loop attach the scene's dependencies to it. Order-independent because the id is derived deterministically from the file name. Adds a scene-bundle analyze regression test. SBP-specific fixture data is tracked separately (issue #86); the SBP path was verified manually against a large Addressables build. --- .../SQLite/Handlers/AssetBundleHandler.cs | 54 +++++++++++++-- .../SQLite/Handlers/PreloadDataHandler.cs | 5 +- .../Writers/SerializedFileSQLiteWriter.cs | 47 ++++++++------ Analyzer/SerializedObjects/AssetBundle.cs | 18 ++++- Documentation/analyzer.md | 18 +++-- .../AnalyzeSceneBundleTests.cs | 65 +++++++++++++++++++ 6 files changed, 171 insertions(+), 36 deletions(-) create mode 100644 UnityDataTool.Tests/AnalyzeSceneBundleTests.cs diff --git a/Analyzer/SQLite/Handlers/AssetBundleHandler.cs b/Analyzer/SQLite/Handlers/AssetBundleHandler.cs index 5900e77..d0a5760 100644 --- a/Analyzer/SQLite/Handlers/AssetBundleHandler.cs +++ b/Analyzer/SQLite/Handlers/AssetBundleHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using Microsoft.Data.Sqlite; using UnityDataTools.Analyzer.SerializedObjects; @@ -11,17 +12,24 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; // assets) and adds their dependencies from m_PreloadTable. Player and ContentDirectory builds // have no AssetBundle object, so this handler never runs for them (preload_dependencies can still // get rows from other sources there - see AssetBundle.sql). +// +// Scenes have no single Unity object, so for scene bundles we synthesize one "Scene" object per +// scene and point the assetbundle_assets row at it. The scene object is keyed on the scene's +// SerializedFile so that PreloadDataHandler (processing that scene's .sharedAssets file) resolves +// the same id and attaches the scene's preload dependencies to it - see SerializedFileSQLiteWriter. public class AssetBundleHandler : ISQLiteHandler { SqliteCommand m_InsertCommand; private SqliteCommand m_InsertDepCommand; + private SqliteCommand m_InsertSceneObjectCommand; - // Extracts the scene name from a container entry like "Assets/Foo/Scene1.unity". - // This name must match the one SerializedFileSQLiteWriter derives from the scene's file name - // so both compute the same synthetic Scene object id. They only agree for - // BuildPipeline.BuildAssetBundles; for Scriptable Build Pipeline / Addressables and other - // pipelines the writer never creates the Scene object, so the assetbundle_assets row written - // below points at an object id that has no objects-table row (a dangling reference). + // Scene object ids already inserted, so a scene referenced by more than one bundle is not + // inserted twice (objects.id is a primary key). Persists across files for the whole run. + private HashSet m_InsertedSceneObjects = new(); + + // Legacy fallback for BuildPipeline.BuildAssetBundles scene bundles, which do not emit + // m_SceneHashes: extracts the scene name from a container entry like "Assets/Foo/Scene1.unity". + // The Scene object for those is created by SerializedFileSQLiteWriter (keyed on this same name). private Regex m_SceneNameRegex = new Regex(@"([^//]+)\.unity"); public void Init(SqliteConnection db) @@ -41,6 +49,15 @@ public void Init(SqliteConnection db) m_InsertDepCommand.CommandText = "INSERT INTO preload_dependencies(object, dependency) VALUES(@object, @dependency)"; m_InsertDepCommand.Parameters.Add("@object", SqliteType.Integer); m_InsertDepCommand.Parameters.Add("@dependency", SqliteType.Integer); + + // Synthetic Scene object (type -1 = "Scene"): object_id 0, no game_object/size/crc. + m_InsertSceneObjectCommand = db.CreateCommand(); + m_InsertSceneObjectCommand.CommandText = + "INSERT INTO objects(id, object_id, serialized_file, type, name, game_object, size, crc32) " + + "VALUES(@id, 0, @serialized_file, -1, @name, '', 0, 0)"; + m_InsertSceneObjectCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertSceneObjectCommand.Parameters.Add("@serialized_file", SqliteType.Integer); + m_InsertSceneObjectCommand.Parameters.Add("@name", SqliteType.Text); } public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) @@ -69,8 +86,32 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s m_InsertDepCommand.ExecuteNonQuery(); } } + else if (assetBundle.SceneToFile.TryGetValue(asset.Name, out var sceneFile)) + { + // Scriptable Build Pipeline / Addressables: key the scene on its SerializedFile + // (from m_SceneHashes) and create the synthetic Scene object here, since the writer + // cannot recognise a "CAB-" scene file by name. + var sceneFileId = ctx.SerializedFileIdProvider.GetId(sceneFile.ToLower()); + var objId = ctx.ObjectIdProvider.GetId((sceneFileId, 0)); + + if (m_InsertedSceneObjects.Add(objId)) + { + m_InsertSceneObjectCommand.Transaction = ctx.Transaction; + m_InsertSceneObjectCommand.Parameters["@id"].Value = objId; + m_InsertSceneObjectCommand.Parameters["@serialized_file"].Value = sceneFileId; + m_InsertSceneObjectCommand.Parameters["@name"].Value = asset.Name; + m_InsertSceneObjectCommand.ExecuteNonQuery(); + + m_InsertCommand.Transaction = ctx.Transaction; + m_InsertCommand.Parameters["@object"].Value = objId; + m_InsertCommand.Parameters["@name"].Value = asset.Name; + m_InsertCommand.ExecuteNonQuery(); + } + } else { + // Legacy BuildPipeline.BuildAssetBundles: the Scene object is created by + // SerializedFileSQLiteWriter (keyed on the scene name); here we add only the asset row. var match = m_SceneNameRegex.Match(asset.Name); if (match.Success) @@ -104,5 +145,6 @@ void IDisposable.Dispose() { m_InsertCommand?.Dispose(); m_InsertDepCommand?.Dispose(); + m_InsertSceneObjectCommand?.Dispose(); } } diff --git a/Analyzer/SQLite/Handlers/PreloadDataHandler.cs b/Analyzer/SQLite/Handlers/PreloadDataHandler.cs index 588b9a7..716267f 100644 --- a/Analyzer/SQLite/Handlers/PreloadDataHandler.cs +++ b/Analyzer/SQLite/Handlers/PreloadDataHandler.cs @@ -8,8 +8,9 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; // Processes the PreloadData object, recording its m_Assets list as preload_dependencies. The // "object" the dependencies hang off of depends on the build: -// * Scene bundle: the synthetic Scene object (ctx.SceneId), which also aggregates the scene's -// own content. +// * Scene bundle: the synthetic Scene object (ctx.SceneId). SerializedFileSQLiteWriter resolves +// it for both BuildPipeline ("BuildPlayer-.sharedAssets") and Scriptable Build Pipeline +// / Addressables ("CAB-.sharedAssets") scene bundles. // * Player build: there is no scene object (ctx.SceneId == -1), so the dependencies are hung off // the PreloadData object itself. A player build has one PreloadData per scene (in its // sharedassetsN.assets) plus one in globalgamemanagers.assets for the always-loaded set. diff --git a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs index 0d36685..2fd35fb 100644 --- a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs +++ b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs @@ -37,15 +37,12 @@ public class SerializedFileSQLiteWriter : IDisposable private HashSet m_PropertyPathSet = new(); private HashSet m_PropertyTypeSet = new(); - // Detects the SerializedFiles of a scene bundle and captures the scene name. - // LIMITATION (issue 81): this only matches the BuildPipeline.BuildAssetBundles naming convention - // ("BuildPlayer-"). It does NOT match scene bundles produced by the Scriptable - // Build Pipeline / Addressables ("CAB-") or the Multi-Process Build - // Pipeline ("CAB-"), nor player-build scenes ("level0", "level1", ...), so no - // synthetic Scene object is created for those. For Scriptable Build Pipeline / Addressables - // scene bundles that still leaves the assetbundle_assets rows AssetBundleHandler writes - // dangling (unfixed part of issue 81). Player builds have no AssetBundle object; their - // PreloadData dependencies are attributed to the PreloadData object itself (PreloadDataHandler). + // Detects the SerializedFiles of a BuildPipeline.BuildAssetBundles scene bundle + // ("BuildPlayer-") and captures the scene name. Scene bundles from the Scriptable + // Build Pipeline / Addressables instead use "CAB-" / "CAB-.sharedAssets" and are + // handled by the .sharedAssets branch in WriteSerializedFile plus AssetBundleHandler. + // Player-build scenes ("level0", ...) have no scene object; their PreloadData dependencies are + // attributed to the PreloadData object itself (PreloadDataHandler). private Regex m_RegexSceneFile = new(@"BuildPlayer-([^\.]+)(?:\.sharedAssets)?"); // Rebuilt for each serialized file: maps a PPtr's local m_FileID (0 = this file, 1..N = an @@ -165,22 +162,19 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con m_CurrentTransaction = transaction; // A scene has no single Unity object to represent it, yet a scene bundle lists the scene - // (by its .unity path) as the bundle's asset and other objects/preloads need something to - // hang off of. So for scene bundles we synthesize one "Scene" object per scene and use it - // as the target of the assetbundle_assets row (AssetBundleHandler) and of the scene's content and - // preload dependencies (below and PreloadDataHandler). This only happens when the file - // name matches m_RegexSceneFile; see its LIMITATION note for the builds this misses (issue 81). + // (by its .unity path) as the bundle's asset and its preloads need something to hang off + // of. So for scene bundles we synthesize one "Scene" object per scene, keyed on the + // scene's SerializedFile so every place that references it computes the same id. It is the + // target of the assetbundle_assets row (AssetBundleHandler) and of the scene's preload + // dependencies (the content loop below and PreloadDataHandler). var match = m_RegexSceneFile.Match(relativePath); if (match.Success) { + // BuildPipeline.BuildAssetBundles scene bundle. The scene name comes from the file name + // itself, and the scene object is keyed on that name (AssetBundleHandler matches it + // from the ".unity" container entry). var sceneName = match.Groups[1].Value; - - // There is no Scene object in Unity (a Scene is the full content of a - // SerializedFile), so we synthesize one. Treat the scene name as if it were a - // serialized file name to get a file id, then pair it with pathId 0 to get a - // stable object id for the scene. AssetBundleHandler builds the scene's object id - // the same way, so the two agree. var sceneFileId = m_SerializedFileIdProvider.GetId(sceneName); sceneId = m_ObjectIdProvider.GetId((sceneFileId, 0)); @@ -202,6 +196,19 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con m_AddObjectCommand.ExecuteNonQuery(); } } + else if (Path.GetFileName(fullPath).EndsWith(".sharedAssets", StringComparison.OrdinalIgnoreCase)) + { + // Scriptable Build Pipeline / Addressables scene bundle: a scene's shared-assets file is + // ".sharedAssets" (e.g. "CAB-.sharedAssets"). Resolve the scene object + // id from the scene file name so the content loop below and PreloadDataHandler attach + // the scene's dependencies to it. Unlike the BuildPipeline case above, the Scene object + // itself (and its readable name) is created by AssetBundleHandler, which gets the scene + // path -> file mapping from the AssetBundle object's m_SceneHashes. + var fileName = Path.GetFileName(fullPath); + var sceneFileName = fileName.Substring(0, fileName.Length - ".sharedAssets".Length); + var sceneFileId = m_SerializedFileIdProvider.GetId(sceneFileName.ToLower()); + sceneId = m_ObjectIdProvider.GetId((sceneFileId, 0)); + } m_LocalToDbFileId.Clear(); diff --git a/Analyzer/SerializedObjects/AssetBundle.cs b/Analyzer/SerializedObjects/AssetBundle.cs index bd8b58d..c20e26b 100644 --- a/Analyzer/SerializedObjects/AssetBundle.cs +++ b/Analyzer/SerializedObjects/AssetBundle.cs @@ -10,6 +10,12 @@ public class AssetBundle public IReadOnlyList PreloadTable { get; init; } public bool IsSceneAssetBundle { get; init; } + // For scene bundles built by the Scriptable Build Pipeline / Addressables: maps each scene's + // container path (e.g. "Assets/.../Foo.unity") to the SerializedFile that holds the scene + // (e.g. "CAB-"). Empty when the m_SceneHashes field is absent (older builds, or + // BuildPipeline.BuildAssetBundles, which does not emit it). + public IReadOnlyDictionary SceneToFile { get; init; } + public class Asset { public string Name { get; init; } @@ -50,12 +56,22 @@ public static AssetBundle Read(RandomAccessReader reader) assets.Add(Asset.Read(asset)); } + var sceneToFile = new Dictionary(); + if (reader.HasChild("m_SceneHashes")) + { + foreach (var pair in reader["m_SceneHashes"]) + { + sceneToFile[pair["first"].GetValue()] = pair["second"].GetValue(); + } + } + return new AssetBundle() { Name = name, Assets = assets, PreloadTable = preloadTable, - IsSceneAssetBundle = isSceneAssetBundle + IsSceneAssetBundle = isSceneAssetBundle, + SceneToFile = sceneToFile }; } } diff --git a/Documentation/analyzer.md b/Documentation/analyzer.md index 2d5aeea..c3cb057 100644 --- a/Documentation/analyzer.md +++ b/Documentation/analyzer.md @@ -60,13 +60,13 @@ the container path of the asset. This data comes from the AssetBundle Unity object, so the view is only populated for AssetBundle builds; Player and ContentDirectory builds have no such object and it is empty for them. For a scene bundle the container entry names the scene (its `.unity` path) and points at a synthetic object of -type `Scene` (there is no single Unity object that represents a scene). +type `Scene` (there is no single Unity object that represents a scene). Scene objects are created +for both BuildPipeline.BuildAssetBundles and Scriptable Build Pipeline / Addressables scene bundles. -The view is built with an INNER JOIN to *object_view*, so any container entry whose object is not in -the `objects` table is silently omitted. This currently happens for scene bundles built by the -Scriptable Build Pipeline / Addressables (and the Multi-Process Build Pipeline), where the synthetic -Scene object is not created (see issue #81); the underlying `assetbundle_assets` table still holds -those rows. +The view is built with an INNER JOIN to *object_view*, so a container entry whose object is not in +the `objects` table is omitted. In practice that only happens if the object was genuinely not +analyzed (for example it lives in a bundle that was not part of the analyzed set); the underlying +`assetbundle_assets` table still holds those rows. ## preload_dependencies @@ -79,7 +79,11 @@ What the `object` is depends on the build: - **AssetBundle asset**: the explicitly-assigned asset, with its dependencies taken from the AssetBundle object's preload table. -- **Scene bundle**: a synthetic `Scene` object that also aggregates the scene's own content objects. +- **Scene bundle**: a synthetic `Scene` object (a scene has no single Unity object). Its + dependencies are the scene's shared assets and the entries of the scene's PreloadData. This works + for both BuildPipeline.BuildAssetBundles and Scriptable Build Pipeline / Addressables scene + bundles. The scene's own content objects (GameObjects, etc.) are not listed as dependencies but + share the scene object's `serialized_file`. - **Player build**: the `PreloadData` object itself, because a player build has no scene object to attach the dependencies to. A player build has one `PreloadData` per scene (in its `sharedassetsN.assets`) plus one in `globalgamemanagers.assets` for the always-loaded set. diff --git a/UnityDataTool.Tests/AnalyzeSceneBundleTests.cs b/UnityDataTool.Tests/AnalyzeSceneBundleTests.cs new file mode 100644 index 0000000..d823bef --- /dev/null +++ b/UnityDataTool.Tests/AnalyzeSceneBundleTests.cs @@ -0,0 +1,65 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using NUnit.Framework; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +// Tests that analyze represents scenes in a scene AssetBundle correctly (issue #81): a synthetic +// Scene object is created, its assetbundle_assets row resolves (no dangling reference), and its +// dependencies show up in preload_dependencies_view. +// +// The available scene-bundle test data (LegacyFormats/AssetBundles/v22.scene.bundle) is a +// BuildPipeline.BuildAssetBundles bundle ("BuildPlayer-Scene1"). The Scriptable Build Pipeline / +// Addressables path shares the same scene machinery but is exercised manually against large +// Addressables builds, as the repo has no small SBP scene-bundle fixture. +public class AnalyzeSceneBundleTests +{ + private string m_TestOutputFolder; + private string m_SceneBundlePath; + + [OneTimeSetUp] + public void OneTimeSetup() + { + m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "scene_bundle_test_folder"); + m_SceneBundlePath = Path.Combine(TestContext.CurrentContext.TestDirectory, + "Data", "LegacyFormats", "AssetBundles", "v22.scene.bundle"); + Directory.CreateDirectory(m_TestOutputFolder); + Directory.SetCurrentDirectory(m_TestOutputFolder); + } + + [TearDown] + public void Teardown() + { + SqliteConnection.ClearAllPools(); + new DirectoryInfo(m_TestOutputFolder).EnumerateFiles().ToList().ForEach(f => f.Delete()); + } + + [Test] + public async Task Analyze_SceneBundle_SceneResolvesWithDependencies() + { + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + Assert.AreEqual(0, await Program.Main(new string[] { "analyze", m_SceneBundlePath, "-o", databasePath })); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + // A synthetic Scene object is created for the scene, and its assetbundle_assets row resolves + // to it (no dangling reference), so the scene appears in assetbundle_asset_view. + Assert.Greater(SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = -1"), 0, + "expected at least one synthetic Scene object"); + SQLTestHelper.AssertQueryInt(db, + "SELECT COUNT(*) FROM assetbundle_assets a LEFT JOIN objects o ON o.id = a.object WHERE o.id IS NULL", + 0, "no assetbundle_assets row should be dangling (issue #81)"); + Assert.Greater(SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM assetbundle_asset_view WHERE type = 'Scene'"), 0, + "the scene should appear in assetbundle_asset_view"); + + // The scene's dependencies are attached to the scene object and show up in the view. + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM preload_dependencies WHERE object = -1", + 0, "preload_dependencies.object should never be -1 (issue #81)"); + Assert.Greater(SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM preload_dependencies_view WHERE type = 'Scene'"), 0, + "the scene should have dependencies in preload_dependencies_view"); + } +} From 87d7feea02e2c8a45cb23c5a8bf02a044908083c Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Mon, 6 Jul 2026 08:54:42 -0400 Subject: [PATCH 3/5] Add AssetBundle Format documentation topic Add Documentation/assetbundle-format.md, a technical, hands-on companion to the Unity content overview that covers the AssetBundle object (m_Container, m_PreloadTable, m_Dependencies), asset preload, and how scenes are laid out inside a bundle (paired SerializedFiles, PreloadData, and m_SceneHashes for SBP vs the BuildPlayer- naming convention for BuildPipeline.BuildAssetBundles). Linked from unity-content-format.md. --- Documentation/assetbundle-format.md | 238 ++++++++++++++++++++++++++ Documentation/unity-content-format.md | 2 + 2 files changed, 240 insertions(+) create mode 100644 Documentation/assetbundle-format.md diff --git a/Documentation/assetbundle-format.md b/Documentation/assetbundle-format.md new file mode 100644 index 0000000..a690be9 --- /dev/null +++ b/Documentation/assetbundle-format.md @@ -0,0 +1,238 @@ +# AssetBundle Format + +This topic digs into the internal layout of AssetBundles, with a focus on the parts that are useful +when inspecting an AssetBundle with UnityDataTool. It complements the higher-level +[Overview of Unity Content](unity-content-format.md), which introduces SerializedFiles and Unity +Archives. + +For Unity's official reference on the container format, see +[AssetBundle file format](https://docs.unity3d.com/Manual/assetbundles-file-format.html). +This page builds on that information with more technical information and examples of how +these data structures can be examined. + +## AssetBundles are Unity Archives + +An AssetBundle is a [Unity Archive](unity-content-format.md#unity-archive) with some conventions for +what lives inside. The [Addressables](https://docs.unity3d.com/Manual/com.unity.addressables.html) +package builds its content as AssetBundles too, so the same layout applies there. + +An AssetBundle always contains at least one SerializedFile, and may contain auxiliary files such as +`.resS` (Textures and Meshes) and `.resource` (audio/video). + +### SerializedFile names inside a bundle + +The names of the SerializedFiles inside a bundle are technical and hash-based. You do not need to +understand them for normal use, but they show up throughout UnityDataTool output: + +- **Regular (non-scene) bundles** contain one SerializedFile named `CAB-`, where the hash is + the **MD4** hash of the AssetBundle name (not the `Hash128` / spooky hash exposed in the C# API). +- **Scene bundles** name their scene files differently depending on the build pipeline: + - `BuildPipeline.BuildAssetBundles` uses `BuildPlayer-`. + - The Scriptable Build Pipeline / Addressables uses `CAB-`. + - The Multi-Process Build Pipeline (2023.1+) uses `CAB-`. + +## Inspecting a bundle with UnityDataTool + +The [`archive`](command-archive.md) command lists or extracts the files inside a bundle, and +[`dump`](command-dump.md) / [`serialized-file`](command-serialized-file.md) inspect the +SerializedFiles. A typical workflow is to extract the bundle into a folder and then dump specific +objects: + +``` +UnityDataTool archive extract mybundle.bundle -o extracted +cd extracted +UnityDataTool sf objectlist CAB- +UnityDataTool dump --stdout CAB- --type AssetBundle +``` + +## The AssetBundle object + +Every bundle has one **AssetBundle** object (ClassID 142). It records which assets the bundle +exposes and what each of them needs preloaded. Its important fields are: + +- `m_Container` - a map from an asset's address/path to the object it maps to, plus the + `preloadIndex`/`preloadSize` range describing that asset's dependencies (see below). +- `m_PreloadTable` - a flat list of `PPtr`s. Each container entry references a contiguous slice of + this list. +- `m_Dependencies` - the names of other bundles this bundle depends on. +- `m_IsStreamedSceneAssetBundle` - true for scene bundles. +- `m_SceneHashes` - for scene bundles, a map from each scene path to the SerializedFile that holds + it (populated by SBP/Addressables only; see [Scenes in AssetBundles](#scenes-in-assetbundles)). + +In a regular bundle the AssetBundle object is typically at local file id (LFID) 1. In a scene bundle +it is at LFID 2, because the PreloadData object takes LFID 1. + +### Preload for assets (non-scene bundles) + +For a regular asset, `m_Container` names the asset and points at its main object, and +`preloadIndex` + `preloadSize` select the slice of `m_PreloadTable` listing every object that must +be loaded for that asset (including objects found in SerializedFiles in other dependent AssetBundles). This is how Unity knows the full set of objects to load in order to fully load an asset. + +For example, a bundle whose single ScriptableObject references an AudioClip stored in another +bundle: + +``` +ID: 1 (ClassID: 142) AssetBundle + m_Name (string) directaudioclipreference + m_PreloadTable (vector) + Array>[4] + data[0] (PPtr) + m_FileID (int) 1 + m_PathID (SInt64) -895285400835485219 + data[1] (PPtr) + m_FileID (int) 0 + m_PathID (SInt64) -602721743313719314 + data[2] (PPtr) + m_FileID (int) 0 + m_PathID (SInt64) 8127315791103748070 + data[3] (PPtr) + m_FileID (int) 2 + m_PathID (SInt64) -5151917721481642524 + m_Container (map) + Array[1] + data[0] (pair) + first (string) assets/scriptableobjects/directaudioclipreference.asset + second (AssetInfo) + preloadIndex (int) 0 + preloadSize (int) 4 + asset (PPtr) + m_FileID (int) 0 + m_PathID (SInt64) -602721743313719314 + m_Dependencies (vector) + Array[2] + data[0] (string) 6 + data[1] (string) a + m_IsStreamedSceneAssetBundle (bool) False +``` + +Here the single asset `directaudioclipreference.asset` has `preloadIndex 0` and `preloadSize 4`, so +its dependencies are `m_PreloadTable[0..3]`. Those `PPtr`s use `m_FileID` to say which file each +object lives in: `0` is this file, and `1`/`2` are entries in the file's external reference table - +in this case objects in the dependency bundles `6` and `a` listed in `m_Dependencies`. + +> [!NOTE] +> `m_Container` only records the assets that were **explicitly** added to the bundle. Assets that +> are pulled in implicitly (because an explicit asset references them) appear in `m_PreloadTable` +> and the dependency bundles, but are not listed as their own container entries. + +## Scenes in AssetBundles + +Scenes are a special case, where the component hierarchy requires a dedicated serialized file that is entirely +loaded each time a scene is loaded. Unity's build pipeline emits **two SerializedFiles +per scene**: + +- `` - the scene's own contents (the GameObject/Transform hierarchy, RenderSettings, + LightmapSettings, and so on). +- `.sharedAssets` - the objects referenced by the scene (Materials, Meshes, MonoScripts, + etc.). To avoid duplication, a scene's files may reference objects in *other* scenes' `.sharedAssets` + files, but there is never an external reference **into** a scene's own contents file. Note: in some cases a scene will have no additional external references apart from the references already saved in other .sharedAssets files. So in that case there will be no sharedAssets file for that scene. + +Extracting a simple single-scene bundle shows the pair: + +``` +UnityDataTool archive list scene1.bundle +``` +``` +BuildPlayer-Scene1.sharedAssets + ... + Flags: SerializedFile + +BuildPlayer-Scene1 + ... + Flags: SerializedFile +``` + +A scene bundle contains exactly **one AssetBundle object**, in one of the `.sharedAssets` files. +Its `m_Container` lists every scene in the bundle by `.unity` path, and each scene's container entry +has a null `asset` PPtr (`m_FileID 0`, `m_PathID 0`) because there is no object to point at: + +``` +UnityDataTool dump --stdout BuildPlayer-Scene1.sharedAssets --type AssetBundle +``` +``` +ID: 2 (ClassID: 142) AssetBundle + m_Name (string) scene1.bundle + m_PreloadTable (vector) + Array>[0] + m_Container (map) + Array[1] + data[0] (pair) + first (string) Assets/AssetDuplication/Scene1.unity + second (AssetInfo) + preloadIndex (int) 0 + preloadSize (int) 0 + asset (PPtr) + m_FileID (int) 0 + m_PathID (SInt64) 0 + m_IsStreamedSceneAssetBundle (bool) True + m_SceneHashes (map) + Array[0] +``` + +Note: the layout for scenes inside an AssetBundle is basically the same as how a Player build builds scenes (except different naming convention for the serialized files). In fact BuildPipeline.BuildAssetBundles and BuildPipeline.BuildPlayer() share much of the same code for processing scenes. + +### m_SceneHashes: mapping a scene to its SerializedFile + +Because SBP/Addressables name their scene files `CAB-`, the file name alone does not reveal +which scene it holds. The AssetBundle object records the mapping in `m_SceneHashes`, from the scene +path to the scene's SerializedFile name: + +``` + m_SceneHashes (map) + Array[145] + data[0] (pair) + first (string) Assets/Scenes/Dungeons/BaneChamber/DUN_BaneChamber_DESIGN.unity + second (string) CAB-256771f7b55e388852b84baa22aeb5b2 +``` + +`BuildPipeline.BuildAssetBundles` leaves `m_SceneHashes` **empty** (as in the single-scene example +above) and instead encodes the scene name directly in the file name (`BuildPlayer-`). + +### PreloadData + +Each scene's `.sharedAssets` file contains a **PreloadData** object (ClassID 150) at LFID 1. Its +`m_Assets` vector lists the `PPtr`s of everything the scene depends on; at runtime Unity walks this +list to load all required objects before the scene loads. + +``` +UnityDataTool dump --stdout BuildPlayer-Scene1.sharedAssets --type PreloadData +``` +``` +ID: 1 (ClassID: 150) PreloadData + m_Name (string) + m_Assets (vector) + Array>[2] + data[0] (PPtr) + m_FileID (int) 1 + m_PathID (SInt64) 10001 + data[1] (PPtr) + m_FileID (int) 2 + m_PathID (SInt64) 4900368479417156912 + m_Dependencies (vector) + Array[1] + data[0] (string) imagelist.bundle +``` + +As with the AssetBundle preload table, each `m_Assets` entry's `m_FileID` selects the file: `0` is +this file and `1`, `2`, ... are entries in the file's external reference table (other bundles, or +Unity's built-in resource files). Here `data[1]` points into another bundle (`imagelist.bundle`). + +Because PreloadData maps out the whole dependency graph of the objects referenced from a scene, its +size grows with the number of dependencies. Projects with very large or numerous hard references +(for example a MonoBehaviour in a scene directly referencing thousands of prefabs) can end up with +PreloadData tables containing huge numbers of entries, adding significant metadata overhead to the +build. If PreloadData is contributing excessive size or load time, the usual fix is to break up +large dependency graphs by loading some assets on demand (via Addressables, AssetBundles, or +`Resources.Load`) instead of using direct hard references. + +## How `analyze` represents this + +The [`analyze`](analyzer.md) command turns the above into queryable tables: + +- Each explicit `m_Container` asset becomes a row in `assetbundle_assets`, and its preload slice + becomes rows in `preload_dependencies`. +- Because a scene has no Unity object, `analyze` synthesizes a "Scene" object per scene to stand in + for it, so scenes appear in `assetbundle_asset_view` and their PreloadData dependencies appear in + `preload_dependencies_view`. + +See [Analyzer](analyzer.md) for the full schema and the exact behaviour of those views. diff --git a/Documentation/unity-content-format.md b/Documentation/unity-content-format.md index 8225aaf..efac1e2 100644 --- a/Documentation/unity-content-format.md +++ b/Documentation/unity-content-format.md @@ -26,6 +26,8 @@ AssetBundles always contain at least one SerializedFile. In the case of an Asse UnityDataTools supports opening Archive files, so it can analyze AssetBundles. +For a more technical, hands-on look at the internals of AssetBundles - the AssetBundle object, preload tables, and how scenes are laid out inside a bundle - see [AssetBundle Format](assetbundle-format.md). + ## Player Builds A player build produces content as well as compiled code (assemblies, executables) and various configuration files. UnityDataTool only concerns itself with the content portion of that output. From 7540a2495f84300d9fb95a3ff92359497d0c06ce Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Mon, 6 Jul 2026 09:38:55 -0400 Subject: [PATCH 4/5] [#81] Address review: decouple scene asset row, use ToLowerInvariant - AssetBundleHandler: only the synthetic Scene object insert is deduped; the assetbundle_assets container row is always written, so a scene exposed by more than one bundle still gets its row. - Normalize all SerializedFile-name keys with ToLowerInvariant instead of ToLower so the derived ids are stable across locales (e.g. Turkish-I). --- Analyzer/SQLite/Handlers/AssetBundleHandler.cs | 15 +++++++++------ .../SQLite/Writers/SerializedFileSQLiteWriter.cs | 6 +++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Analyzer/SQLite/Handlers/AssetBundleHandler.cs b/Analyzer/SQLite/Handlers/AssetBundleHandler.cs index d0a5760..2d5552c 100644 --- a/Analyzer/SQLite/Handlers/AssetBundleHandler.cs +++ b/Analyzer/SQLite/Handlers/AssetBundleHandler.cs @@ -91,9 +91,12 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s // Scriptable Build Pipeline / Addressables: key the scene on its SerializedFile // (from m_SceneHashes) and create the synthetic Scene object here, since the writer // cannot recognise a "CAB-" scene file by name. - var sceneFileId = ctx.SerializedFileIdProvider.GetId(sceneFile.ToLower()); + var sceneFileId = ctx.SerializedFileIdProvider.GetId(sceneFile.ToLowerInvariant()); var objId = ctx.ObjectIdProvider.GetId((sceneFileId, 0)); + // The synthetic Scene object is inserted once (objects.id is a primary key), but the + // container entry is always recorded, so a scene exposed by more than one bundle + // still gets its assetbundle_assets row. if (m_InsertedSceneObjects.Add(objId)) { m_InsertSceneObjectCommand.Transaction = ctx.Transaction; @@ -101,12 +104,12 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s m_InsertSceneObjectCommand.Parameters["@serialized_file"].Value = sceneFileId; m_InsertSceneObjectCommand.Parameters["@name"].Value = asset.Name; m_InsertSceneObjectCommand.ExecuteNonQuery(); - - m_InsertCommand.Transaction = ctx.Transaction; - m_InsertCommand.Parameters["@object"].Value = objId; - m_InsertCommand.Parameters["@name"].Value = asset.Name; - m_InsertCommand.ExecuteNonQuery(); } + + m_InsertCommand.Transaction = ctx.Transaction; + m_InsertCommand.Parameters["@object"].Value = objId; + m_InsertCommand.Parameters["@name"].Value = asset.Name; + m_InsertCommand.ExecuteNonQuery(); } else { diff --git a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs index 2fd35fb..907212f 100644 --- a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs +++ b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs @@ -155,7 +155,7 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con using var sf = UnityFileSystem.OpenSerializedFile(fullPath); using var reader = new UnityFileReader(fullPath, 64 * 1024 * 1024); using var pptrReader = new PPtrAndCrcProcessor(sf, reader, containingFolder, m_SkipCrc, AddReference); - int serializedFileId = m_SerializedFileIdProvider.GetId(Path.GetFileName(fullPath).ToLower()); + int serializedFileId = m_SerializedFileIdProvider.GetId(Path.GetFileName(fullPath).ToLowerInvariant()); int sceneId = -1; using var transaction = m_Database.BeginTransaction(); @@ -206,7 +206,7 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con // path -> file mapping from the AssetBundle object's m_SceneHashes. var fileName = Path.GetFileName(fullPath); var sceneFileName = fileName.Substring(0, fileName.Length - ".sharedAssets".Length); - var sceneFileId = m_SerializedFileIdProvider.GetId(sceneFileName.ToLower()); + var sceneFileId = m_SerializedFileIdProvider.GetId(sceneFileName.ToLowerInvariant()); sceneId = m_ObjectIdProvider.GetId((sceneFileId, 0)); } @@ -240,7 +240,7 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con foreach (var extRef in sf.ExternalReferences) { m_LocalToDbFileId.Add(localId++, - m_SerializedFileIdProvider.GetId(extRef.Path.Substring(extRef.Path.LastIndexOf('/') + 1).ToLower())); + m_SerializedFileIdProvider.GetId(extRef.Path.Substring(extRef.Path.LastIndexOf('/') + 1).ToLowerInvariant())); } foreach (var obj in sf.Objects) From edc70ae59740e153f02eb51d525ba06965f80ee5 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Mon, 6 Jul 2026 09:54:07 -0400 Subject: [PATCH 5/5] Add documentation links to main README --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 04d0daf..2a052ef 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The UnityDataTool is a command line tool and showcase of the UnityFileSystemApi The main purpose is for analysis of the content of Unity data files, for example AssetBundles and Player content. -The [command line tool](./Documentation/unitydatatool.md) runs directly on Unity data files, without requiring the Editor to be running. It covers most functionality of the Unity tools WebExtract and binary2text, with better performance. And it adds a lot of additional functionality, for example the ability to create a SQLite database for detailed analysis of build content. See [examples](./Documentation/analyze-examples.md) and [comparing builds](./Documentation/comparing-builds.md) for examples of how to use the command line tool. +The [command line tool](./Documentation/unitydatatool.md) runs directly on Unity data files, without requiring the Editor to be running. It covers most functionality of the Unity tools WebExtract and binary2text, with better performance. And it adds a lot of additional functionality, for example the ability to create a SQLite database for detailed analysis of build content. See the [Documentation](#documentation) section below for guides to Unity's content formats and to using the tool. It is designed to scale for large build outputs and has been used to fine-tune big Unity-based games. @@ -12,6 +12,29 @@ The tool also provides comprehensive analysis of **Unity Addressables build repo The command line tool uses the UnityFileSystemApi library to access the content of Unity Archives and Serialized files, which are Unity's primary binary formats. This repository also serves as a reference for how this library could be used as part of incorporating functionality into your own tools. +## Documentation + +New to Unity's data files or to UnityDataTool? These topics are a good place to start. + +**Understanding Unity content** + +| Topic | Description | +| --- | --- | +| [Overview of Unity Content](./Documentation/unity-content-format.md) | The core file types (SerializedFiles and Unity Archives), TypeTrees, and how Player and AssetBundle builds are laid out. | +| [AssetBundle Format](./Documentation/assetbundle-format.md) | A hands-on look inside AssetBundles: the AssetBundle object, preload tables, and how scenes are stored. | +| [ContentLayout.json](./Documentation/contentlayout.md) | The build layout file that maps content-directory build output back to source assets. | + +**Using UnityDataTool** + +| Topic | Description | +| --- | --- | +| [Command-line tool](./Documentation/unitydatatool.md) | All commands and their options. | +| [Analyzer & database schema](./Documentation/analyzer.md) | The SQLite database that `analyze` produces, including its tables and views. | +| [Example queries](./Documentation/analyze-examples.md) | Worked examples of querying the analyze database. | +| [Comparing builds](./Documentation/comparing-builds.md) | Finding what changed between two builds. | +| [Addressables build reports](./Documentation/addressables-build-reports.md) | Analyzing Addressables JSON build reports. | +| [Build reports](./Documentation/buildreport.md) | Importing a Unity BuildReport to map build output back to source assets. | + ## Repository content The repository contains the following items: