perf(content): remove exception-driven setup probes

This commit is contained in:
Erik 2026-07-24 15:12:21 +02:00
parent f05afc07c1
commit 230a7df454
11 changed files with 598 additions and 47 deletions

View file

@ -1,4 +1,5 @@
using System.Numerics;
using AcDream.Content;
using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.App.Interaction;
@ -196,11 +197,35 @@ internal sealed class LivePresentationCompositionPhase
var componentLifecycle =
new DeferredLiveEntityRuntimeComponentLifecycle();
var wbSpawnAdapter = new LandblockSpawnAdapter(foundation.MeshAdapter);
Setup? LoadPreparedSetup(uint sourceId)
{
if (!content.Dats.TryResolvePreferred(
sourceId,
out IDatDatabase? database,
out DatReaderWriter.Enums.DBObjType type)
|| type != DatReaderWriter.Enums.DBObjType.Setup)
{
return null;
}
return database.TryGet<Setup>(
sourceId,
out Setup? setup)
? setup
: null;
}
var setupResolver = new PreparedSetupResolver(
content.PreparedAssets,
LoadPreparedSetup,
message => Console.Error.WriteLine(
$"setup activation: {message}"));
AnimationSequencer SequencerFactory(WorldEntity entity)
{
Setup? setup = content.Dats.Get<Setup>(entity.SourceGfxObjOrSetupId);
if (setup is not null)
if (setupResolver.TryResolve(
entity.SourceGfxObjOrSetupId,
out Setup? setup))
{
uint motionTableId = (uint)setup.DefaultMotionTable;
if (motionTableId != 0
@ -247,38 +272,33 @@ internal sealed class LivePresentationCompositionPhase
ScriptActivationInfo? ResolveActivation(WorldEntity entity)
{
try
{
Setup? setup = content.Dats.Get<Setup>(
entity.SourceGfxObjOrSetupId);
if (setup is null)
return null;
uint scriptId = setup.DefaultScript.DataId;
if (entity.IndexedPartTransforms.Count == 0)
{
var indexed = IndexedSetupPartPoseBuilder.Build(setup, entity);
entity.SetIndexedPartPoses(indexed.Poses, indexed.Available);
}
bool usesStaticAnimationWorkset = entity.ServerGuid == 0
|| (liveEntities?.TryGetRecord(
entity.ServerGuid,
out LiveEntityRecord liveRecord) == true
&& (liveRecord.FinalPhysicsState
& PhysicsStateFlags.Static) != 0);
return new ScriptActivationInfo(
scriptId,
entity.IndexedPartTransforms,
EntityEffectProfile.CreateDatStatic(setup),
entity.IndexedPartAvailable,
setup,
(uint)setup.DefaultAnimation,
usesStaticAnimationWorkset);
}
catch
if (!setupResolver.TryResolve(
entity.SourceGfxObjOrSetupId,
out Setup? setup))
{
return null;
}
uint scriptId = setup.DefaultScript.DataId;
if (entity.IndexedPartTransforms.Count == 0)
{
var indexed = IndexedSetupPartPoseBuilder.Build(setup, entity);
entity.SetIndexedPartPoses(indexed.Poses, indexed.Available);
}
bool usesStaticAnimationWorkset = entity.ServerGuid == 0
|| (liveEntities?.TryGetRecord(
entity.ServerGuid,
out LiveEntityRecord liveRecord) == true
&& (liveRecord.FinalPhysicsState
& PhysicsStateFlags.Static) != 0);
return new ScriptActivationInfo(
scriptId,
entity.IndexedPartTransforms,
EntityEffectProfile.CreateDatStatic(setup),
entity.IndexedPartAvailable,
setup,
(uint)setup.DefaultAnimation,
usesStaticAnimationWorkset);
}
var entityScriptActivator = new EntityScriptActivator(

View file

@ -0,0 +1,101 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using AcDream.Content;
using AcDream.Content.Pak;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Composition;
/// <summary>
/// Resolves Setup presentation metadata only when the immutable prepared
/// catalog proves that the source DID is a Setup. Ordinary GfxObj IDs never
/// reach the DAT deserializer, so absence is ordinary control flow rather
/// than an exception.
/// </summary>
internal sealed class PreparedSetupResolver
{
private readonly IPreparedAssetSource _preparedAssets;
private readonly Func<uint, Setup?> _loadSetup;
private readonly Action<string> _diagnostic;
private readonly ConcurrentDictionary<uint, byte> _diagnosed = new();
public PreparedSetupResolver(
IPreparedAssetSource preparedAssets,
Func<uint, Setup?> loadSetup,
Action<string> diagnostic)
{
_preparedAssets = preparedAssets
?? throw new ArgumentNullException(nameof(preparedAssets));
_loadSetup = loadSetup
?? throw new ArgumentNullException(nameof(loadSetup));
_diagnostic = diagnostic
?? throw new ArgumentNullException(nameof(diagnostic));
}
public bool TryResolve(
uint sourceId,
[NotNullWhen(true)] out Setup? setup)
{
PreparedAssetPresence presence =
_preparedAssets.Probe(PakAssetType.SetupMesh, sourceId);
if (presence == PreparedAssetPresence.Missing)
{
setup = null;
return false;
}
if (presence == PreparedAssetPresence.Corrupt)
{
throw new InvalidDataException(
$"Prepared Setup entry 0x{sourceId:X8} is corrupt; " +
"activation cannot safely infer presentation metadata.");
}
try
{
setup = _loadSetup(sourceId);
if (setup is not null)
return true;
DiagnoseOnce(
sourceId,
$"Prepared Setup entry 0x{sourceId:X8} exists but its " +
"matching DAT record could not be loaded; activation skipped.");
return false;
}
catch (InvalidDataException ex)
{
return DiagnoseCorruptData(sourceId, ex, out setup);
}
catch (EndOfStreamException ex)
{
return DiagnoseCorruptData(sourceId, ex, out setup);
}
catch (ArgumentOutOfRangeException ex)
{
// DatReaderWriter's verified malformed-record failure:
// QualifiedDataId<T>.Unpack / Setup.Unpack can surface an
// ArgumentOutOfRangeException for invalid record contents.
return DiagnoseCorruptData(sourceId, ex, out setup);
}
}
private bool DiagnoseCorruptData(
uint sourceId,
Exception exception,
out Setup? setup)
{
setup = null;
DiagnoseOnce(
sourceId,
$"Setup DAT record 0x{sourceId:X8} is malformed " +
$"({exception.GetType().Name}: {exception.Message}); " +
"activation skipped.");
return false;
}
private void DiagnoseOnce(uint sourceId, string message)
{
if (_diagnosed.TryAdd(sourceId, 0))
_diagnostic(message);
}
}

View file

@ -146,6 +146,53 @@ public sealed class DatCollectionAdapter : IDatReaderWriter {
return results;
}
/// <summary>
/// Direct, allocation-free production lookup. Portal precedence is
/// load-bearing: the legacy ResolveId ordering is HighRes-first and its
/// callers historically re-sorted the materialized results to make Portal
/// win. Encode that rule once instead.
/// </summary>
public bool TryResolvePreferred(
uint id,
[NotNullWhen(true)] out IDatDatabase? database,
out DBObjType type) {
if (TryResolve(_portal, id, out type)) {
database = _portal;
return true;
}
if (TryResolve(_highRes, id, out type)) {
database = _highRes;
return true;
}
if (TryResolve(_language, id, out type)) {
database = _language;
return true;
}
if (TryResolve(_cell, id, out type)) {
database = _cell;
return true;
}
database = null;
type = DBObjType.Unknown;
return false;
}
private static bool TryResolve(
DatDatabaseWrapper database,
uint id,
out DBObjType type) {
DatDatabase raw = database.RawDatabase;
if (raw.Tree.TryGetFile(id, out _)) {
type = raw.TypeFromId(id);
if (type != DBObjType.Unknown)
return true;
}
type = DBObjType.Unknown;
return false;
}
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
throw new NotSupportedException("DatCollectionAdapter is read-only.");

View file

@ -103,6 +103,57 @@ public interface IDatReaderWriter : IDisposable, IDatObjectSource {
/// Resolves a data ID to all possible databases and types.
/// </summary>
public IEnumerable<IdResolution> ResolveId(uint id);
/// <summary>
/// Resolves one data ID using acdream's canonical precedence without
/// materializing a result collection: Portal, HighRes, Language, Cell.
/// Production's concrete adapter overrides this with direct B-tree probes.
/// </summary>
bool TryResolvePreferred(
uint id,
[NotNullWhen(true)] out IDatDatabase? database,
out DBObjType type)
{
IDatDatabase? highRes = null;
DBObjType highResType = DBObjType.Unknown;
IDatDatabase? language = null;
DBObjType languageType = DBObjType.Unknown;
IDatDatabase? cell = null;
DBObjType cellType = DBObjType.Unknown;
foreach (IdResolution resolution in ResolveId(id))
{
if (ReferenceEquals(resolution.Database, Portal))
{
database = resolution.Database;
type = resolution.Type;
return true;
}
if (ReferenceEquals(resolution.Database, HighRes))
{
highRes = resolution.Database;
highResType = resolution.Type;
}
else if (ReferenceEquals(resolution.Database, Language))
{
language = resolution.Database;
languageType = resolution.Type;
}
else if (ReferenceEquals(resolution.Database, Cell))
{
cell = resolution.Database;
cellType = resolution.Type;
}
}
database = highRes ?? language ?? cell;
type = highRes is not null
? highResType
: language is not null
? languageType
: cellType;
return database is not null;
}
}
/// <summary>

View file

@ -356,7 +356,11 @@ public sealed class DatPreparedAssetSource : IPreparedAssetSource
};
if (expected == DBObjType.Unknown)
return PreparedAssetPresence.Missing;
return _dats.ResolveId(sourceFileId).Any(r => r.Type == expected)
return _dats.TryResolvePreferred(
sourceFileId,
out _,
out DBObjType actual)
&& actual == expected
? PreparedAssetPresence.Available
: PreparedAssetPresence.Missing;
}

View file

@ -87,12 +87,11 @@ public sealed class MeshExtractor {
try {
// Use the low 32 bits as the DAT file ID
var datId = (uint)(id & 0xFFFFFFFFu);
var resolutions = _dats.ResolveId(datId);
var selectedResolution = resolutions.OrderByDescending(r => r.Database == _dats.Portal).FirstOrDefault();
if (selectedResolution == null) return null;
var type = selectedResolution.Type;
var db = selectedResolution.Database;
if (!_dats.TryResolvePreferred(
datId,
out IDatDatabase? db,
out DBObjType type))
return null;
if (type == DBObjType.Setup) {
if (!db.TryGet<Setup>(datId, out var setup)) return null;
@ -203,12 +202,11 @@ public sealed class MeshExtractor {
}
ct.ThrowIfCancellationRequested();
var resolutions = _dats.ResolveId(id);
var selectedResolution = resolutions.OrderByDescending(r => r.Database == _dats.Portal).FirstOrDefault();
if (selectedResolution == null) return;
var type = selectedResolution.Type;
var db = selectedResolution.Database;
if (!_dats.TryResolvePreferred(
id,
out IDatDatabase? db,
out DBObjType type))
return;
if (type == DBObjType.Setup) {
if (!db.TryGet<Setup>(id, out var setup)) return;