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
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue