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

@ -10,7 +10,6 @@ using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
@ -34,10 +33,11 @@ public sealed class MeshExtractor {
private readonly ILogger _logger;
private readonly RetailPhysicsScriptLoader _physicsScripts;
// Cache for decoded textures to avoid redundant BCn decoding
private readonly ConcurrentQueue<uint> _decodedTextureLru = new();
private readonly ConcurrentDictionary<uint, byte[]> _decodedTextureCache = new();
private const int MaxDecodedTextures = 128;
// Canonical decoded pixels are immutable and bounded by bytes as well as
// count. A count-only cache can retain hundreds of MiB of large RGBA data.
private readonly DecodedTextureCache _decodedTextureCache = new(
maximumBytes: 64L * 1024 * 1024,
maximumEntries: 128);
private readonly ThreadLocal<BcDecoder> _bcDecoder = new(() => new BcDecoder());
/// <summary>
@ -370,6 +370,7 @@ public sealed class MeshExtractor {
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
uint paletteId = 0;
bool isDxt3or5 = false;
bool textureDataIsCached = false;
DatReaderWriter.Enums.PixelFormat? sourceFormat = null;
var isAdditive = false;
var isTransparent = false;
@ -395,37 +396,33 @@ public sealed class MeshExtractor {
texHeight = renderSurface.Height;
paletteId = renderSurface.DefaultPaletteId;
sourceFormat = renderSurface.Format;
isDxt3or5 = renderSurface.Format is
DatReaderWriter.Enums.PixelFormat.PFID_DXT3 or
DatReaderWriter.Enums.PixelFormat.PFID_DXT5;
var decodedTextureKey = CreateDecodedTextureKey(
renderSurfaceId,
renderSurface.Format,
isClipMap,
surface.Type.HasFlag(SurfaceType.Additive));
if (TextureHelpers.IsCompressedFormat(renderSurface.Format)) {
isDxt3or5 = renderSurface.Format == DatReaderWriter.Enums.PixelFormat.PFID_DXT3 || renderSurface.Format == DatReaderWriter.Enums.PixelFormat.PFID_DXT5;
textureFormat = TextureFormat.RGBA8;
uploadPixelFormat = UploadPixelFormat.Rgba;
if (_decodedTextureCache.TryGetValue(renderSurfaceId, out textureData!)) {
// use cached data
}
else {
textureData = new byte[texWidth * texHeight * 4];
CompressionFormat compressionFormat = renderSurface.Format switch {
DatReaderWriter.Enums.PixelFormat.PFID_DXT1 => CompressionFormat.Bc1,
DatReaderWriter.Enums.PixelFormat.PFID_DXT3 => CompressionFormat.Bc2,
DatReaderWriter.Enums.PixelFormat.PFID_DXT5 => CompressionFormat.Bc3,
_ => throw new NotSupportedException($"Unsupported compressed format: {renderSurface.Format}")
};
using (var image = _bcDecoder.Value!.DecodeRawToImageRgba32(renderSurface.SourceData, texWidth, texHeight, compressionFormat)) {
image.CopyPixelDataTo(textureData);
}
_decodedTextureCache.TryAdd(renderSurfaceId, textureData);
}
textureData = _decodedTextureCache.GetOrCreate(
decodedTextureKey,
() => DecodeCompressedTexture(renderSurface, texWidth, texHeight),
out textureDataIsCached);
if (isClipMap && textureData != null) {
// If we got this from the cache, we need to clone it so we don't scale the cached raw data
if (_decodedTextureCache.ContainsKey(renderSurfaceId)) {
// Cached pixels are canonical and immutable. Surface-local
// alpha processing must never modify the shared array.
if (textureDataIsCached) {
var clonedData = new byte[textureData.Length];
System.Buffer.BlockCopy(textureData, 0, clonedData, 0, textureData.Length);
textureData = clonedData;
textureDataIsCached = false;
}
for (int i = 0; i < textureData.Length; i += 4) {
@ -437,8 +434,16 @@ public sealed class MeshExtractor {
}
else {
textureFormat = TextureFormat.RGBA8;
textureData = renderSurface.SourceData;
switch (renderSurface.Format) {
uploadPixelFormat = UploadPixelFormat.Rgba;
if (_decodedTextureCache.TryGet(
decodedTextureKey,
out byte[] cachedPixels)) {
textureData = cachedPixels;
textureDataIsCached = true;
}
else {
textureData = renderSurface.SourceData;
switch (renderSurface.Format) {
case DatReaderWriter.Enums.PixelFormat.PFID_A8R8G8B8:
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillA8R8G8B8(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
@ -486,12 +491,18 @@ public sealed class MeshExtractor {
break;
default:
throw new NotSupportedException($"Unsupported surface format: {renderSurface.Format}");
}
textureData = _decodedTextureCache.RetainOrUse(
decodedTextureKey,
textureData,
out textureDataIsCached);
}
}
if (surface.Translucency > 0.0f && textureData != null) {
// If we got this from the cache, we need to clone it so we don't scale the cached raw data
if (sourceFormat.HasValue && TextureHelpers.IsCompressedFormat(sourceFormat.Value) && _decodedTextureCache.ContainsKey(renderSurfaceId)) {
if (textureDataIsCached) {
var clonedData = new byte[textureData.Length];
System.Buffer.BlockCopy(textureData, 0, clonedData, 0, textureData.Length);
textureData = clonedData;
@ -749,8 +760,16 @@ public sealed class MeshExtractor {
texHeight = renderSurface.Height;
paletteId = renderSurface.DefaultPaletteId;
sourceFormat = renderSurface.Format;
isDxt3or5 = renderSurface.Format is
DatReaderWriter.Enums.PixelFormat.PFID_DXT3 or
DatReaderWriter.Enums.PixelFormat.PFID_DXT5;
var decodedTextureKey = CreateDecodedTextureKey(
renderSurfaceId,
renderSurface.Format,
isClipMap,
surface.Type.HasFlag(SurfaceType.Additive));
if (_decodedTextureCache.TryGetValue(renderSurfaceId, out var cachedData)) {
if (_decodedTextureCache.TryGet(decodedTextureKey, out var cachedData)) {
textureData = cachedData;
textureFormat = TextureFormat.RGBA8;
uploadPixelFormat = UploadPixelFormat.Rgba;
@ -761,17 +780,10 @@ public sealed class MeshExtractor {
textureFormat = TextureFormat.RGBA8;
uploadPixelFormat = UploadPixelFormat.Rgba;
textureData = new byte[texWidth * texHeight * 4];
CompressionFormat compressionFormat = renderSurface.Format switch {
DatReaderWriter.Enums.PixelFormat.PFID_DXT1 => CompressionFormat.Bc1,
DatReaderWriter.Enums.PixelFormat.PFID_DXT3 => CompressionFormat.Bc2,
DatReaderWriter.Enums.PixelFormat.PFID_DXT5 => CompressionFormat.Bc3,
_ => throw new NotSupportedException($"Unsupported compressed format: {renderSurface.Format}")
};
using (var image = _bcDecoder.Value!.DecodeRawToImageRgba32(renderSurface.SourceData, texWidth, texHeight, compressionFormat)) {
image.CopyPixelDataTo(textureData);
}
textureData = _decodedTextureCache.GetOrCreate(
decodedTextureKey,
() => DecodeCompressedTexture(renderSurface, texWidth, texHeight),
out _);
}
else {
textureFormat = TextureFormat.RGBA8;
@ -824,14 +836,12 @@ public sealed class MeshExtractor {
}
}
// Add to cache with LRU logic
if (textureData != null && _decodedTextureCache.TryAdd(renderSurfaceId, textureData)) {
_decodedTextureLru.Enqueue(renderSurfaceId);
if (_decodedTextureCache.Count > MaxDecodedTextures) {
if (_decodedTextureLru.TryDequeue(out var evictedId)) {
_decodedTextureCache.TryRemove(evictedId, out _);
}
}
if (!TextureHelpers.IsCompressedFormat(renderSurface.Format))
{
textureData = _decodedTextureCache.RetainOrUse(
decodedTextureKey,
textureData,
out _);
}
}
@ -996,6 +1006,48 @@ public sealed class MeshExtractor {
}
}
private static DecodedTextureKey CreateDecodedTextureKey(
uint renderSurfaceId,
DatReaderWriter.Enums.PixelFormat format,
bool isClipMap,
bool isAdditive)
{
bool clipAffectsDecode = format is
DatReaderWriter.Enums.PixelFormat.PFID_INDEX16 or
DatReaderWriter.Enums.PixelFormat.PFID_P8;
bool additiveAffectsDecode = format is
DatReaderWriter.Enums.PixelFormat.PFID_A8 or
DatReaderWriter.Enums.PixelFormat.PFID_CUSTOM_LSCAPE_ALPHA;
return new DecodedTextureKey(
renderSurfaceId,
clipAffectsDecode && isClipMap,
additiveAffectsDecode && isAdditive);
}
private byte[] DecodeCompressedTexture(
RenderSurface renderSurface,
int width,
int height)
{
var textureData = new byte[width * height * 4];
CompressionFormat compressionFormat = renderSurface.Format switch
{
DatReaderWriter.Enums.PixelFormat.PFID_DXT1 => CompressionFormat.Bc1,
DatReaderWriter.Enums.PixelFormat.PFID_DXT3 => CompressionFormat.Bc2,
DatReaderWriter.Enums.PixelFormat.PFID_DXT5 => CompressionFormat.Bc3,
_ => throw new NotSupportedException(
$"Unsupported compressed format: {renderSurface.Format}"),
};
using var image = _bcDecoder.Value!.DecodeRawToImageRgba32(
renderSurface.SourceData,
width,
height,
compressionFormat);
image.CopyPixelDataTo(textureData);
return textureData;
}
private void BuildPolygonIndices(Polygon poly, GfxObj gfxObj, Vector3 scale,
Dictionary<(ushort vertId, ushort uvIdx, bool isNeg), ushort> UVLookup,
List<VertexPositionNormalTexture> vertices, List<ushort> indices, bool useNegSurface, ref bool hasWrappingUVs) {