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

@ -1,5 +1,6 @@
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using AcDream.Core.Content;
using DatReaderWriter.Types;
using AcDream.Core.Physics;
@ -15,7 +16,7 @@ public static class LandblockLoader
/// Load a single landblock (heightmap + static objects) from the dats.
/// </summary>
/// <returns>Null if the landblock is missing from the cell dat.</returns>
public static LoadedLandblock? Load(DatCollection dats, uint landblockId)
public static LoadedLandblock? Load(IDatObjectSource dats, uint landblockId)
{
var block = dats.Get<LandBlock>(landblockId);
if (block is null)
@ -40,7 +41,7 @@ public static class LandblockLoader
var result = new List<WorldEntity>(info.Objects.Count + info.Buildings.Count);
// When landblockId is non-zero, namespace stab Ids globally:
// 0xC0XXYY00 + n, where XX = lbX byte, YY = lbY byte
// 0xCXXYYIII, where XX = lbX, YY = lbY, III = 12-bit counter
// distinct from the 0x8XXYYIII scenery and 0x4XXYYIII interior
// namespaces in GameWindow.cs. The 0xC0 top byte distinguishes stabs.
//
@ -48,20 +49,21 @@ public static class LandblockLoader
// legacy starting-from-1 monotonic Ids — compatible with their assertions
// which check uniqueness within a single landblock.
//
// The low byte reserves 0 for the namespace base and 1..255 for
// entities. Never wrap into the Y byte: a collision here would corrupt
// every renderer/physics/effect table keyed by WorldEntity.Id.
uint stabIdBase = landblockId == 0
? 0u
: 0xC0000000u | ((landblockId >> 24) & 0xFFu) << 16 | ((landblockId >> 16) & 0xFFu) << 8;
uint nextId = stabIdBase == 0 ? 1u : stabIdBase + 1u;
// The 12-bit allocator fails before crossing into the adjacent
// landblock's Y range; no wrapped id can corrupt renderer, physics, or
// effect tables keyed by WorldEntity.Id.
uint landblockX = (landblockId >> 24) & 0xFFu;
uint landblockY = (landblockId >> 16) & 0xFFu;
uint nextId = landblockId == 0 ? 1u : 0u;
uint AllocateId()
{
if (stabIdBase != 0 && nextId > stabIdBase + 0xFFu)
throw new InvalidDataException(
$"Landblock 0x{landblockId:X8} exceeds the 255-entry static stab id namespace.");
return nextId++;
if (landblockId == 0)
return nextId++;
return LandblockStaticEntityIdAllocator.Allocate(
landblockX,
landblockY,
ref nextId);
}
foreach (var stab in info.Objects)

View file

@ -0,0 +1,36 @@
namespace AcDream.Core.World;
/// <summary>
/// Allocates stable, collision-free local ids for dat-authored
/// <c>LandBlockInfo.Objects</c> and building shells.
/// </summary>
/// <remarks>
/// The top nibble is fixed at <c>0xC</c>. The remaining 28 bits encode the
/// complete landblock X byte, Y byte, and a 12-bit per-landblock counter:
/// <c>0xCXXYYIII</c>. The former <c>0xC0XXYYII</c> packing had only eight
/// counter bits, so a legitimate dense retail landblock aborted after 255
/// entries and was retried every time it streamed into view.
/// </remarks>
public static class LandblockStaticEntityIdAllocator
{
public const uint MaxCounter = 0xFFFu;
public static uint Base(uint landblockX, uint landblockY) =>
0xC0000000u
| ((landblockX & 0xFFu) << 20)
| ((landblockY & 0xFFu) << 12);
public static uint Allocate(uint landblockX, uint landblockY, ref uint counter)
{
if (counter > MaxCounter)
{
throw new InvalidDataException(
$"Landblock ({landblockX & 0xFFu:X2},{landblockY & 0xFFu:X2}) exceeds the 4096-entry static entity id namespace.");
}
return Base(landblockX, landblockY) + counter++;
}
public static bool IsInNamespace(uint entityId) =>
(entityId & 0xF0000000u) == 0xC0000000u;
}

View file

@ -21,9 +21,8 @@ public readonly record struct MeshRef(uint GfxObjId, Matrix4x4 PartTransform)
/// bind a sub-mesh's texture, it consults this map. A hit means
/// "use the base Surface's colors/flags/palette but swap its
/// <c>OrigTextureId</c> for the value here." The renderer calls
/// <c>TextureCache.GetOrUploadWithOrigTextureOverride</c> which caches
/// the decoded result under a composite key so multiple entities can
/// share the same override without redecoding.
/// the owning renderer resolves an owner-scoped composite. Equivalent
/// live entities may share it, and the final owner releases it.
/// </para>
/// <para>
/// Null means "no overrides, use each sub-mesh's native surface as-is."

View file

@ -1,5 +1,6 @@
using System.Numerics;
using AcDream.Core.Rendering.Wb;
using AcDream.Core.Content;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
@ -50,7 +51,7 @@ public static class SceneryGenerator
/// see <c>docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md</c>.
/// </summary>
public static IReadOnlyList<ScenerySpawn> Generate(
DatCollection dats,
IDatObjectSource dats,
Region region,
LandBlock block,
uint landblockId,
@ -71,7 +72,7 @@ public static class SceneryGenerator
public static bool IsRoadVertex(ushort raw) => (raw & 0x3u) != 0;
private static IReadOnlyList<ScenerySpawn> GenerateInternal(
DatCollection dats,
IDatObjectSource dats,
Region region,
LandBlock block,
uint landblockId,

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using DatReaderWriter;
using AcDream.Core.Content;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
@ -357,7 +358,7 @@ public static class SkyDescLoader
/// Load + parse. Returns <c>null</c> if the Region doesn't have
/// <see cref="PartsMask.HasSkyInfo"/> or the dat is absent.
/// </summary>
public static LoadedSkyDesc? LoadFromDat(DatCollection dats)
public static LoadedSkyDesc? LoadFromDat(IDatObjectSource dats)
{
ArgumentNullException.ThrowIfNull(dats);
var region = dats.Get<Region>(RegionDatId);

View file

@ -1,5 +1,5 @@
// src/AcDream.Core/World/WorldView.cs
using DatReaderWriter;
using AcDream.Core.Content;
namespace AcDream.Core.World;
@ -19,7 +19,7 @@ public sealed class WorldView
/// Load the 3x3 grid of landblocks around <paramref name="centerLandblockId"/>.
/// Missing neighbors (edges of the world or absent from the cell dat) are silently skipped.
/// </summary>
public static WorldView Load(DatCollection dats, uint centerLandblockId)
public static WorldView Load(IDatObjectSource dats, uint centerLandblockId)
{
var loaded = new List<LoadedLandblock>();
foreach (var id in NeighborLandblockIds(centerLandblockId))