Skip to content

Commit 07a7774

Browse files
[#81] Fix scene/preload dependencies for Player and Scriptable Build Pipeline builds (#87)
SBP/Addressables name scene files "CAB-<hash>" (and "CAB-<hash>.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 "<file>.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. * 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. * Add documentation links to main README
1 parent 33fcf4a commit 07a7774

11 files changed

Lines changed: 525 additions & 45 deletions

File tree

Analyzer/Resources/AssetBundle.sql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ CREATE TABLE IF NOT EXISTS assetbundle_assets(
1818
-- * AssetBundleHandler: an asset's slice of the AssetBundle object's m_PreloadTable.
1919
-- * SerializedFileSQLiteWriter: a scene object -> each object in the scene's SerializedFiles.
2020
-- * PreloadDataHandler: the PreloadData object's m_Assets. PreloadData is a *separate* Unity
21-
-- object (not part of the AssetBundle object) and also exists in Player builds (one per scene in its sharedAsset file),
22-
-- so this table is NOT empty there; but those builds have no scene object, so PreloadDataHandler currently
23-
-- attributes the rows to object id -1 (issue 81).
21+
-- object (not part of the AssetBundle object) and also exists in Player builds (one per scene
22+
-- in its sharedAssetsN.assets, plus one in globalgamemanagers.assets), so this table is NOT
23+
-- empty there. Player builds have no scene object, so those rows hang off the PreloadData
24+
-- object itself; scene bundles hang them off the synthetic Scene object.
2425
CREATE TABLE IF NOT EXISTS preload_dependencies(
2526
object INTEGER,
2627
dependency INTEGER

Analyzer/SQLite/Handlers/AssetBundleHandler.cs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Text.RegularExpressions;
34
using Microsoft.Data.Sqlite;
45
using UnityDataTools.Analyzer.SerializedObjects;
@@ -11,17 +12,24 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers;
1112
// assets) and adds their dependencies from m_PreloadTable. Player and ContentDirectory builds
1213
// have no AssetBundle object, so this handler never runs for them (preload_dependencies can still
1314
// get rows from other sources there - see AssetBundle.sql).
15+
//
16+
// Scenes have no single Unity object, so for scene bundles we synthesize one "Scene" object per
17+
// scene and point the assetbundle_assets row at it. The scene object is keyed on the scene's
18+
// SerializedFile so that PreloadDataHandler (processing that scene's .sharedAssets file) resolves
19+
// the same id and attaches the scene's preload dependencies to it - see SerializedFileSQLiteWriter.
1420
public class AssetBundleHandler : ISQLiteHandler
1521
{
1622
SqliteCommand m_InsertCommand;
1723
private SqliteCommand m_InsertDepCommand;
24+
private SqliteCommand m_InsertSceneObjectCommand;
1825

19-
// Extracts the scene name from a container entry like "Assets/Foo/Scene1.unity".
20-
// This name must match the one SerializedFileSQLiteWriter derives from the scene's file name
21-
// so both compute the same synthetic Scene object id. They only agree for
22-
// BuildPipeline.BuildAssetBundles; for Scriptable Build Pipeline / Addressables and other
23-
// pipelines the writer never creates the Scene object, so the assetbundle_assets row written
24-
// below points at an object id that has no objects-table row (a dangling reference).
26+
// Scene object ids already inserted, so a scene referenced by more than one bundle is not
27+
// inserted twice (objects.id is a primary key). Persists across files for the whole run.
28+
private HashSet<long> m_InsertedSceneObjects = new();
29+
30+
// Legacy fallback for BuildPipeline.BuildAssetBundles scene bundles, which do not emit
31+
// m_SceneHashes: extracts the scene name from a container entry like "Assets/Foo/Scene1.unity".
32+
// The Scene object for those is created by SerializedFileSQLiteWriter (keyed on this same name).
2533
private Regex m_SceneNameRegex = new Regex(@"([^//]+)\.unity");
2634

2735
public void Init(SqliteConnection db)
@@ -41,6 +49,15 @@ public void Init(SqliteConnection db)
4149
m_InsertDepCommand.CommandText = "INSERT INTO preload_dependencies(object, dependency) VALUES(@object, @dependency)";
4250
m_InsertDepCommand.Parameters.Add("@object", SqliteType.Integer);
4351
m_InsertDepCommand.Parameters.Add("@dependency", SqliteType.Integer);
52+
53+
// Synthetic Scene object (type -1 = "Scene"): object_id 0, no game_object/size/crc.
54+
m_InsertSceneObjectCommand = db.CreateCommand();
55+
m_InsertSceneObjectCommand.CommandText =
56+
"INSERT INTO objects(id, object_id, serialized_file, type, name, game_object, size, crc32) " +
57+
"VALUES(@id, 0, @serialized_file, -1, @name, '', 0, 0)";
58+
m_InsertSceneObjectCommand.Parameters.Add("@id", SqliteType.Integer);
59+
m_InsertSceneObjectCommand.Parameters.Add("@serialized_file", SqliteType.Integer);
60+
m_InsertSceneObjectCommand.Parameters.Add("@name", SqliteType.Text);
4461
}
4562

4663
public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize)
@@ -69,8 +86,35 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s
6986
m_InsertDepCommand.ExecuteNonQuery();
7087
}
7188
}
89+
else if (assetBundle.SceneToFile.TryGetValue(asset.Name, out var sceneFile))
90+
{
91+
// Scriptable Build Pipeline / Addressables: key the scene on its SerializedFile
92+
// (from m_SceneHashes) and create the synthetic Scene object here, since the writer
93+
// cannot recognise a "CAB-<hash>" scene file by name.
94+
var sceneFileId = ctx.SerializedFileIdProvider.GetId(sceneFile.ToLowerInvariant());
95+
var objId = ctx.ObjectIdProvider.GetId((sceneFileId, 0));
96+
97+
// The synthetic Scene object is inserted once (objects.id is a primary key), but the
98+
// container entry is always recorded, so a scene exposed by more than one bundle
99+
// still gets its assetbundle_assets row.
100+
if (m_InsertedSceneObjects.Add(objId))
101+
{
102+
m_InsertSceneObjectCommand.Transaction = ctx.Transaction;
103+
m_InsertSceneObjectCommand.Parameters["@id"].Value = objId;
104+
m_InsertSceneObjectCommand.Parameters["@serialized_file"].Value = sceneFileId;
105+
m_InsertSceneObjectCommand.Parameters["@name"].Value = asset.Name;
106+
m_InsertSceneObjectCommand.ExecuteNonQuery();
107+
}
108+
109+
m_InsertCommand.Transaction = ctx.Transaction;
110+
m_InsertCommand.Parameters["@object"].Value = objId;
111+
m_InsertCommand.Parameters["@name"].Value = asset.Name;
112+
m_InsertCommand.ExecuteNonQuery();
113+
}
72114
else
73115
{
116+
// Legacy BuildPipeline.BuildAssetBundles: the Scene object is created by
117+
// SerializedFileSQLiteWriter (keyed on the scene name); here we add only the asset row.
74118
var match = m_SceneNameRegex.Match(asset.Name);
75119

76120
if (match.Success)
@@ -104,5 +148,6 @@ void IDisposable.Dispose()
104148
{
105149
m_InsertCommand?.Dispose();
106150
m_InsertDepCommand?.Dispose();
151+
m_InsertSceneObjectCommand?.Dispose();
107152
}
108153
}

Analyzer/SQLite/Handlers/PreloadDataHandler.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66

77
namespace UnityDataTools.Analyzer.SQLite.Handlers;
88

9-
// Processes the PreloadData object found in scene bundles. Its m_Assets list is recorded as the
10-
// scene's dependencies (preload_dependencies), so it is meaningful only alongside a synthetic
11-
// Scene object. This is AssetBundle-specific in practice: Player builds also contain a PreloadData
12-
// object but have no scene object, so ctx.SceneId is -1 there and the rows below are written
13-
// against object id -1 (a limitation, not intended output). ContentDirectory builds have neither.
9+
// Processes the PreloadData object, recording its m_Assets list as preload_dependencies. The
10+
// "object" the dependencies hang off of depends on the build:
11+
// * Scene bundle: the synthetic Scene object (ctx.SceneId). SerializedFileSQLiteWriter resolves
12+
// it for both BuildPipeline ("BuildPlayer-<Scene>.sharedAssets") and Scriptable Build Pipeline
13+
// / Addressables ("CAB-<hash>.sharedAssets") scene bundles.
14+
// * Player build: there is no scene object (ctx.SceneId == -1), so the dependencies are hung off
15+
// the PreloadData object itself. A player build has one PreloadData per scene (in its
16+
// sharedassetsN.assets) plus one in globalgamemanagers.assets for the always-loaded set.
17+
// A dependency may resolve to an object that analyze never tracked (e.g. objects in
18+
// "unity default resources", which normally has no TypeTrees), leaving a row whose dependency has
19+
// no objects-table entry. ContentDirectory builds have no PreloadData object.
1420
public class PreloadDataHandler : ISQLiteHandler
1521
{
1622
private SqliteCommand m_InsertDepCommand;
@@ -28,7 +34,9 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s
2834
{
2935
var preloadData = PreloadData.Read(reader);
3036
m_InsertDepCommand.Transaction = ctx.Transaction;
31-
m_InsertDepCommand.Parameters["@object"].Value = ctx.SceneId;
37+
// Scene bundles hang the dependencies off the synthetic Scene object; player builds have
38+
// none, so they hang off the PreloadData object itself.
39+
m_InsertDepCommand.Parameters["@object"].Value = ctx.SceneId != -1 ? ctx.SceneId : objectId;
3240

3341
foreach (var asset in preloadData.Assets)
3442
{

Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,12 @@ public class SerializedFileSQLiteWriter : IDisposable
3737
private HashSet<int> m_PropertyPathSet = new();
3838
private HashSet<int> m_PropertyTypeSet = new();
3939

40-
// Detects the SerializedFiles of a scene bundle and captures the scene name.
41-
// LIMITATION (issue 81): this only matches the BuildPipeline.BuildAssetBundles naming convention
42-
// ("BuildPlayer-<SceneName>"). It does NOT match scene bundles produced by the Scriptable
43-
// Build Pipeline / Addressables ("CAB-<hash of scene path>") or the Multi-Process Build
44-
// Pipeline ("CAB-<scene GUID>"), nor player-build scenes ("level0", "level1", ...). For
45-
// those builds no synthetic Scene object is created (see the scene handling in
46-
// WriteSerializedFile), so the scene rows AssetBundleHandler writes into assetbundle_assets
47-
// end up dangling and PreloadDataHandler attributes preload dependencies to SceneId -1.
40+
// Detects the SerializedFiles of a BuildPipeline.BuildAssetBundles scene bundle
41+
// ("BuildPlayer-<SceneName>") and captures the scene name. Scene bundles from the Scriptable
42+
// Build Pipeline / Addressables instead use "CAB-<hash>" / "CAB-<hash>.sharedAssets" and are
43+
// handled by the .sharedAssets branch in WriteSerializedFile plus AssetBundleHandler.
44+
// Player-build scenes ("level0", ...) have no scene object; their PreloadData dependencies are
45+
// attributed to the PreloadData object itself (PreloadDataHandler).
4846
private Regex m_RegexSceneFile = new(@"BuildPlayer-([^\.]+)(?:\.sharedAssets)?");
4947

5048
// Rebuilt for each serialized file: maps a PPtr's local m_FileID (0 = this file, 1..N = an
@@ -157,29 +155,26 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con
157155
using var sf = UnityFileSystem.OpenSerializedFile(fullPath);
158156
using var reader = new UnityFileReader(fullPath, 64 * 1024 * 1024);
159157
using var pptrReader = new PPtrAndCrcProcessor(sf, reader, containingFolder, m_SkipCrc, AddReference);
160-
int serializedFileId = m_SerializedFileIdProvider.GetId(Path.GetFileName(fullPath).ToLower());
158+
int serializedFileId = m_SerializedFileIdProvider.GetId(Path.GetFileName(fullPath).ToLowerInvariant());
161159
int sceneId = -1;
162160

163161
using var transaction = m_Database.BeginTransaction();
164162
m_CurrentTransaction = transaction;
165163

166164
// A scene has no single Unity object to represent it, yet a scene bundle lists the scene
167-
// (by its .unity path) as the bundle's asset and other objects/preloads need something to
168-
// hang off of. So for scene bundles we synthesize one "Scene" object per scene and use it
169-
// as the target of the assetbundle_assets row (AssetBundleHandler) and of the scene's content and
170-
// preload dependencies (below and PreloadDataHandler). This only happens when the file
171-
// name matches m_RegexSceneFile; see its LIMITATION note for the builds this misses (issue 81).
165+
// (by its .unity path) as the bundle's asset and its preloads need something to hang off
166+
// of. So for scene bundles we synthesize one "Scene" object per scene, keyed on the
167+
// scene's SerializedFile so every place that references it computes the same id. It is the
168+
// target of the assetbundle_assets row (AssetBundleHandler) and of the scene's preload
169+
// dependencies (the content loop below and PreloadDataHandler).
172170
var match = m_RegexSceneFile.Match(relativePath);
173171

174172
if (match.Success)
175173
{
174+
// BuildPipeline.BuildAssetBundles scene bundle. The scene name comes from the file name
175+
// itself, and the scene object is keyed on that name (AssetBundleHandler matches it
176+
// from the "<sceneName>.unity" container entry).
176177
var sceneName = match.Groups[1].Value;
177-
178-
// There is no Scene object in Unity (a Scene is the full content of a
179-
// SerializedFile), so we synthesize one. Treat the scene name as if it were a
180-
// serialized file name to get a file id, then pair it with pathId 0 to get a
181-
// stable object id for the scene. AssetBundleHandler builds the scene's object id
182-
// the same way, so the two agree.
183178
var sceneFileId = m_SerializedFileIdProvider.GetId(sceneName);
184179
sceneId = m_ObjectIdProvider.GetId((sceneFileId, 0));
185180

@@ -201,6 +196,19 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con
201196
m_AddObjectCommand.ExecuteNonQuery();
202197
}
203198
}
199+
else if (Path.GetFileName(fullPath).EndsWith(".sharedAssets", StringComparison.OrdinalIgnoreCase))
200+
{
201+
// Scriptable Build Pipeline / Addressables scene bundle: a scene's shared-assets file is
202+
// "<sceneFile>.sharedAssets" (e.g. "CAB-<hash>.sharedAssets"). Resolve the scene object
203+
// id from the scene file name so the content loop below and PreloadDataHandler attach
204+
// the scene's dependencies to it. Unlike the BuildPipeline case above, the Scene object
205+
// itself (and its readable name) is created by AssetBundleHandler, which gets the scene
206+
// path -> file mapping from the AssetBundle object's m_SceneHashes.
207+
var fileName = Path.GetFileName(fullPath);
208+
var sceneFileName = fileName.Substring(0, fileName.Length - ".sharedAssets".Length);
209+
var sceneFileId = m_SerializedFileIdProvider.GetId(sceneFileName.ToLowerInvariant());
210+
sceneId = m_ObjectIdProvider.GetId((sceneFileId, 0));
211+
}
204212

205213
m_LocalToDbFileId.Clear();
206214

@@ -232,7 +240,7 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con
232240
foreach (var extRef in sf.ExternalReferences)
233241
{
234242
m_LocalToDbFileId.Add(localId++,
235-
m_SerializedFileIdProvider.GetId(extRef.Path.Substring(extRef.Path.LastIndexOf('/') + 1).ToLower()));
243+
m_SerializedFileIdProvider.GetId(extRef.Path.Substring(extRef.Path.LastIndexOf('/') + 1).ToLowerInvariant()));
236244
}
237245

238246
foreach (var obj in sf.Objects)

Analyzer/SerializedObjects/AssetBundle.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ public class AssetBundle
1010
public IReadOnlyList<PPtr> PreloadTable { get; init; }
1111
public bool IsSceneAssetBundle { get; init; }
1212

13+
// For scene bundles built by the Scriptable Build Pipeline / Addressables: maps each scene's
14+
// container path (e.g. "Assets/.../Foo.unity") to the SerializedFile that holds the scene
15+
// (e.g. "CAB-<hash>"). Empty when the m_SceneHashes field is absent (older builds, or
16+
// BuildPipeline.BuildAssetBundles, which does not emit it).
17+
public IReadOnlyDictionary<string, string> SceneToFile { get; init; }
18+
1319
public class Asset
1420
{
1521
public string Name { get; init; }
@@ -50,12 +56,22 @@ public static AssetBundle Read(RandomAccessReader reader)
5056
assets.Add(Asset.Read(asset));
5157
}
5258

59+
var sceneToFile = new Dictionary<string, string>();
60+
if (reader.HasChild("m_SceneHashes"))
61+
{
62+
foreach (var pair in reader["m_SceneHashes"])
63+
{
64+
sceneToFile[pair["first"].GetValue<string>()] = pair["second"].GetValue<string>();
65+
}
66+
}
67+
5368
return new AssetBundle()
5469
{
5570
Name = name,
5671
Assets = assets,
5772
PreloadTable = preloadTable,
58-
IsSceneAssetBundle = isSceneAssetBundle
73+
IsSceneAssetBundle = isSceneAssetBundle,
74+
SceneToFile = sceneToFile
5975
};
6076
}
6177
}

0 commit comments

Comments
 (0)