fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
|
|
@ -100,6 +101,7 @@ public class Issue93TownNetworkFountainRoomLightInspectionTests
|
|||
var datDir = ResolveDatDir();
|
||||
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
using var boundedDats = new DatCollectionAdapter(dats);
|
||||
|
||||
const uint setupId = 0x02000365u;
|
||||
var setup = dats.Get<Setup>(setupId);
|
||||
|
|
@ -115,7 +117,7 @@ public class Issue93TownNetworkFountainRoomLightInspectionTests
|
|||
int survivors = 0, markerSkipped = 0, gfxNull = 0;
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
if (AcDream.Core.Meshing.GfxObjDegradeResolver.IsRuntimeHiddenMarker(dats, mr.GfxObjId))
|
||||
if (AcDream.Core.Meshing.GfxObjDegradeResolver.IsRuntimeHiddenMarker(boundedDats, mr.GfxObjId))
|
||||
{
|
||||
markerSkipped++;
|
||||
_out.WriteLine($" part gfx=0x{mr.GfxObjId:X8} -> MARKER (skipped)");
|
||||
|
|
@ -164,6 +166,7 @@ public class Issue93TownNetworkFountainRoomLightInspectionTests
|
|||
var datDir = ResolveDatDir();
|
||||
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
using var boundedDats = new DatCollectionAdapter(dats);
|
||||
|
||||
var setup = dats.Get<Setup>(setupId);
|
||||
Assert.NotNull(setup);
|
||||
|
|
@ -174,7 +177,7 @@ public class Issue93TownNetworkFountainRoomLightInspectionTests
|
|||
int survivors = 0;
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
if (AcDream.Core.Meshing.GfxObjDegradeResolver.IsRuntimeHiddenMarker(dats, mr.GfxObjId)) continue;
|
||||
if (AcDream.Core.Meshing.GfxObjDegradeResolver.IsRuntimeHiddenMarker(boundedDats, mr.GfxObjId)) continue;
|
||||
if (dats.Get<GfxObj>(mr.GfxObjId) is null) continue;
|
||||
survivors++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -331,7 +331,6 @@ public class EntityClassificationCacheTests
|
|||
uint ibo, uint firstIndex, int indexCount, ulong texHandle)
|
||||
{
|
||||
var key = new GroupKey(
|
||||
Ibo: ibo,
|
||||
FirstIndex: firstIndex,
|
||||
BaseVertex: 0,
|
||||
IndexCount: indexCount,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Physics;
|
||||
|
|
@ -16,8 +15,8 @@ public sealed class EntitySpawnAdapterTests
|
|||
[Fact]
|
||||
public void OnCreate_ServerSpawnedEntity_RegistersAnimatedEntityState()
|
||||
{
|
||||
var cache = new CapturingTextureCache();
|
||||
var adapter = MakeAdapter(cache);
|
||||
var lifetime = new RecordingTextureLifetime();
|
||||
var adapter = MakeAdapter(lifetime);
|
||||
var entity = MakeEntity(id: 1, serverGuid: 0xDEAD0001u);
|
||||
|
||||
var state = adapter.OnCreate(entity);
|
||||
|
|
@ -42,8 +41,8 @@ public sealed class EntitySpawnAdapterTests
|
|||
[Fact]
|
||||
public void OnCreate_ProceduralEntity_ReturnsNullAndRegistersNothing()
|
||||
{
|
||||
var cache = new CapturingTextureCache();
|
||||
var adapter = MakeAdapter(cache);
|
||||
var lifetime = new RecordingTextureLifetime();
|
||||
var adapter = MakeAdapter(lifetime);
|
||||
// ServerGuid == 0 → atlas-tier, must not be processed here.
|
||||
var entity = MakeEntity(id: 2, serverGuid: 0u);
|
||||
|
||||
|
|
@ -51,101 +50,19 @@ public sealed class EntitySpawnAdapterTests
|
|||
|
||||
Assert.Null(state);
|
||||
Assert.Null(adapter.GetState(0u));
|
||||
// No texture decode should have been triggered.
|
||||
Assert.Empty(cache.Calls);
|
||||
}
|
||||
|
||||
// ── Palette-override texture decode ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void OnCreate_WithPaletteOverrideAndSurfaceOverrides_TriggersTextureCacheDecode()
|
||||
{
|
||||
var cache = new CapturingTextureCache();
|
||||
var adapter = MakeAdapter(cache);
|
||||
|
||||
var palette = new PaletteOverride(
|
||||
BasePaletteId: 0x04001234u,
|
||||
SubPalettes: new[]
|
||||
{
|
||||
new PaletteOverride.SubPaletteRange(0x04002000u, 0, 2),
|
||||
});
|
||||
|
||||
// Entity carries two parts each with one surface override.
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 10,
|
||||
ServerGuid = 0xBEEF0001u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
PaletteOverride = palette,
|
||||
MeshRefs = new[]
|
||||
{
|
||||
new MeshRef(0x01000010u, Matrix4x4.Identity)
|
||||
{
|
||||
SurfaceOverrides = new Dictionary<uint, uint>
|
||||
{
|
||||
{ 0x08000100u, 0u }, // surfaceId → origTex (0 = none)
|
||||
},
|
||||
},
|
||||
new MeshRef(0x01000020u, Matrix4x4.Identity)
|
||||
{
|
||||
SurfaceOverrides = new Dictionary<uint, uint>
|
||||
{
|
||||
{ 0x08000200u, 0x05000300u }, // with origTex override
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
adapter.OnCreate(entity);
|
||||
|
||||
// One call per surface-with-override: (0x08000100, null) and (0x08000200, 0x05000300).
|
||||
Assert.Equal(2, cache.Calls.Count);
|
||||
|
||||
Assert.Contains(cache.Calls, c => c.SurfaceId == 0x08000100u
|
||||
&& c.OrigTexOverride == null
|
||||
&& c.Palette == palette);
|
||||
Assert.Contains(cache.Calls, c => c.SurfaceId == 0x08000200u
|
||||
&& c.OrigTexOverride == 0x05000300u
|
||||
&& c.Palette == palette);
|
||||
Assert.Empty(lifetime.ReleasedOwnerIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnCreate_WithPaletteOverrideButNoSurfaceOverrides_DoesNotCallCache()
|
||||
public void OnCreate_DoesNotAcquireTexturesBeforeTheSurfaceIsDrawn()
|
||||
{
|
||||
// Surfaces without SurfaceOverrides == null are decoded lazily at draw
|
||||
// time; the adapter only pre-warms what it knows at spawn time.
|
||||
var cache = new CapturingTextureCache();
|
||||
var adapter = MakeAdapter(cache);
|
||||
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 11,
|
||||
ServerGuid = 0xBEEF0002u,
|
||||
SourceGfxObjOrSetupId = 0x02000002u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
PaletteOverride = new PaletteOverride(0x04001235u, Array.Empty<PaletteOverride.SubPaletteRange>()),
|
||||
// MeshRef with NO SurfaceOverrides.
|
||||
MeshRefs = new[] { new MeshRef(0x01000011u, Matrix4x4.Identity) },
|
||||
};
|
||||
var lifetime = new RecordingTextureLifetime();
|
||||
var adapter = MakeAdapter(lifetime);
|
||||
var entity = MakeEntity(id: 10, serverGuid: 0xBEEF0001u);
|
||||
|
||||
adapter.OnCreate(entity);
|
||||
|
||||
Assert.Empty(cache.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnCreate_WithoutPaletteOverride_DoesNotCallCache()
|
||||
{
|
||||
var cache = new CapturingTextureCache();
|
||||
var adapter = MakeAdapter(cache);
|
||||
var entity = MakeEntity(id: 12, serverGuid: 0xBEEF0003u);
|
||||
|
||||
adapter.OnCreate(entity);
|
||||
|
||||
Assert.Empty(cache.Calls);
|
||||
Assert.Empty(lifetime.ReleasedOwnerIds);
|
||||
}
|
||||
|
||||
// ── OnRemove ─────────────────────────────────────────────────────────
|
||||
|
|
@ -153,14 +70,17 @@ public sealed class EntitySpawnAdapterTests
|
|||
[Fact]
|
||||
public void OnRemove_ReleasesPerEntityState()
|
||||
{
|
||||
var adapter = MakeAdapter();
|
||||
var lifetime = new RecordingTextureLifetime();
|
||||
var adapter = MakeAdapter(lifetime);
|
||||
var entity = MakeEntity(id: 20, serverGuid: 0xCAFE0001u);
|
||||
|
||||
adapter.OnCreate(entity);
|
||||
adapter.SetPresentationResident(entity, resident: true);
|
||||
Assert.NotNull(adapter.GetState(0xCAFE0001u));
|
||||
|
||||
adapter.OnRemove(0xCAFE0001u);
|
||||
Assert.Null(adapter.GetState(0xCAFE0001u));
|
||||
Assert.Equal([20u], lifetime.ReleasedOwnerIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -208,10 +128,10 @@ public sealed class EntitySpawnAdapterTests
|
|||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private static EntitySpawnAdapter MakeAdapter(ITextureCachePerInstance? cache = null)
|
||||
private static EntitySpawnAdapter MakeAdapter(IEntityTextureLifetime? lifetime = null)
|
||||
{
|
||||
cache ??= new CapturingTextureCache();
|
||||
return new EntitySpawnAdapter(cache, _ => MakeSequencer());
|
||||
lifetime ??= new RecordingTextureLifetime();
|
||||
return new EntitySpawnAdapter(lifetime, _ => MakeSequencer());
|
||||
}
|
||||
|
||||
private static WorldEntity MakeEntity(uint id, uint serverGuid)
|
||||
|
|
@ -230,23 +150,11 @@ public sealed class EntitySpawnAdapterTests
|
|||
|
||||
// ── Mocks / stubs ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Capture every call to GetOrUploadWithPaletteOverride so tests can
|
||||
/// assert without a live GL context.
|
||||
/// </summary>
|
||||
private sealed class CapturingTextureCache : ITextureCachePerInstance
|
||||
private sealed class RecordingTextureLifetime : IEntityTextureLifetime
|
||||
{
|
||||
public readonly record struct Call(uint SurfaceId, uint? OrigTexOverride, PaletteOverride Palette);
|
||||
public List<Call> Calls { get; } = new();
|
||||
public List<uint> ReleasedOwnerIds { get; } = new();
|
||||
|
||||
public uint GetOrUploadWithPaletteOverride(
|
||||
uint surfaceId,
|
||||
uint? overrideOrigTextureId,
|
||||
PaletteOverride paletteOverride)
|
||||
{
|
||||
Calls.Add(new Call(surfaceId, overrideOrigTextureId, paletteOverride));
|
||||
return 1u; // Fake GL handle.
|
||||
}
|
||||
public void ReleaseOwner(uint localEntityId) => ReleasedOwnerIds.Add(localEntityId);
|
||||
}
|
||||
|
||||
private sealed class NullAnimationLoader : IAnimationLoader
|
||||
|
|
|
|||
|
|
@ -421,7 +421,6 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
Vector3? localSortCenter = null)
|
||||
{
|
||||
var key = new GroupKey(
|
||||
Ibo: ibo,
|
||||
FirstIndex: firstIndex,
|
||||
BaseVertex: 0,
|
||||
IndexCount: indexCount,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue