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 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)
|
||||
|
|
|
|||
36
src/AcDream.Core/World/LandblockStaticEntityIdAllocator.cs
Normal file
36
src/AcDream.Core/World/LandblockStaticEntityIdAllocator.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue