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:
Erik 2026-07-18 21:35:16 +02:00
parent 3971997689
commit 749e8ceeb1
225 changed files with 29107 additions and 3914 deletions

View file

@ -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,

View file

@ -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

View file

@ -421,7 +421,6 @@ public sealed class WbDrawDispatcherBucketingTests
Vector3? localSortCenter = null)
{
var key = new GroupKey(
Ibo: ibo,
FirstIndex: firstIndex,
BaseVertex: 0,
IndexCount: indexCount,