Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Analyzer/Resources/AssetBundle.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 51 additions & 6 deletions Analyzer/SQLite/Handlers/AssetBundleHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Microsoft.Data.Sqlite;
using UnityDataTools.Analyzer.SerializedObjects;
Expand All @@ -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<long> 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)
Expand All @@ -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)
Expand Down Expand Up @@ -69,8 +86,35 @@ 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-<hash>" scene file by name.
var sceneFileId = ctx.SerializedFileIdProvider.GetId(sceneFile.ToLowerInvariant());
var objId = ctx.ObjectIdProvider.GetId((sceneFileId, 0));
Comment thread
Copilot marked this conversation as resolved.

// 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;
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)
Expand Down Expand Up @@ -104,5 +148,6 @@ void IDisposable.Dispose()
{
m_InsertCommand?.Dispose();
m_InsertDepCommand?.Dispose();
m_InsertSceneObjectCommand?.Dispose();
}
}
20 changes: 14 additions & 6 deletions Analyzer/SQLite/Handlers/PreloadDataHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@

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). SerializedFileSQLiteWriter resolves
// it for both BuildPipeline ("BuildPlayer-<Scene>.sharedAssets") and Scriptable Build Pipeline
// / Addressables ("CAB-<hash>.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.
// 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;
Expand All @@ -28,7 +34,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)
{
Expand Down
50 changes: 29 additions & 21 deletions Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,12 @@ public class SerializedFileSQLiteWriter : IDisposable
private HashSet<int> m_PropertyPathSet = new();
private HashSet<int> 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-<SceneName>"). It does NOT match scene bundles produced by the Scriptable
// Build Pipeline / Addressables ("CAB-<hash of scene path>") or the Multi-Process Build
// Pipeline ("CAB-<scene GUID>"), 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.
// Detects the SerializedFiles of a BuildPipeline.BuildAssetBundles scene bundle
// ("BuildPlayer-<SceneName>") and captures the scene name. Scene bundles from the Scriptable
// Build Pipeline / Addressables instead use "CAB-<hash>" / "CAB-<hash>.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
Expand Down Expand Up @@ -157,29 +155,26 @@ 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();
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 "<sceneName>.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));

Expand All @@ -201,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
// "<sceneFile>.sharedAssets" (e.g. "CAB-<hash>.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.ToLowerInvariant());
sceneId = m_ObjectIdProvider.GetId((sceneFileId, 0));
Comment thread
Copilot marked this conversation as resolved.
}

m_LocalToDbFileId.Clear();

Expand Down Expand Up @@ -232,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)
Expand Down
18 changes: 17 additions & 1 deletion Analyzer/SerializedObjects/AssetBundle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ public class AssetBundle
public IReadOnlyList<PPtr> 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-<hash>"). Empty when the m_SceneHashes field is absent (older builds, or
// BuildPipeline.BuildAssetBundles, which does not emit it).
public IReadOnlyDictionary<string, string> SceneToFile { get; init; }

public class Asset
{
public string Name { get; init; }
Expand Down Expand Up @@ -50,12 +56,22 @@ public static AssetBundle Read(RandomAccessReader reader)
assets.Add(Asset.Read(asset));
}

var sceneToFile = new Dictionary<string, string>();
if (reader.HasChild("m_SceneHashes"))
{
foreach (var pair in reader["m_SceneHashes"])
{
sceneToFile[pair["first"].GetValue<string>()] = pair["second"].GetValue<string>();
}
}

return new AssetBundle()
{
Name = name,
Assets = assets,
PreloadTable = preloadTable,
IsSceneAssetBundle = isSceneAssetBundle
IsSceneAssetBundle = isSceneAssetBundle,
SceneToFile = sceneToFile
};
}
}
Loading
Loading