perf(content): CellStruct clone guard, solid-color texture cache, drop double ToList (2026-07-24 audit review)
Three small verified MeshExtractor.cs fixes from an allocation audit: - CellStruct clip-map clone guard: the CellStruct polygon-surface path cloned decoded texture data unconditionally before applying clip-map transparency, even though the comment above it says "if we got this from the cache, we need to clone it." The GfxObj path already had the correct pattern (clone only when textureDataIsCached). Wired the same textureDataIsCached flag through CellStruct's TryGet/GetOrCreate/ RetainOrUse call sites and gated the clone on it, matching GfxObj exactly — freshly-decoded arrays (the common case) now skip a redundant clone + BlockCopy. - Solid-color texture cache: isSolid surfaces (Base1Solid / NoPos- stippled polys) allocated a fresh 32x32 RGBA array (4 KiB) on every polygon, even for repeated colors. Added a MeshExtractor-scoped ConcurrentDictionary<uint, byte[]> keyed by packed ARGB, bounded by distinct colors observed (a few hundred at most) rather than call volume. Traced every downstream consumer of the solid-color array (clip-map clone-guard, translucency clone-guard, GL upload, pak serialization) — none currently mutate a solid-color array in place, since isSolid is mutually exclusive with the texture-decode branch those guards live in — but wired textureDataIsCached = true at both call sites anyway so the existing clone-before-mutate machinery protects the shared array if that ever changes. - Dropped the redundant .ToList() on both _dats.ResolveId(...) calls: DatCollectionAdapter.ResolveId already returns a materialized List<T> typed as IEnumerable; the immediate .OrderByDescending().FirstOrDefault() enumerates once, so the extra copy was pure waste. Added SolidColorTextureCacheTests (dat-gated, matching suite convention) exercising the new cache directly via internal visibility. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit e9f9d7539966cc26343e45d653a751c5d1045810)
This commit is contained in:
parent
a4b1214e0f
commit
60bc313917
2 changed files with 109 additions and 9 deletions
|
|
@ -40,6 +40,15 @@ public sealed class MeshExtractor {
|
|||
maximumEntries: 128);
|
||||
private readonly ThreadLocal<BcDecoder> _bcDecoder = new(() => new BcDecoder());
|
||||
|
||||
// Solid-color surfaces (isSolid: Base1Solid surfaces / NoPos-stippled polys)
|
||||
// bake a flat ARGB fill to a fixed 32x32 RGBA array. Distinct surface colors
|
||||
// across an install are a few hundred at most, so this is bounded by DISTINCT
|
||||
// COLORS observed rather than by call count — unlike DecodedTextureCache's
|
||||
// byte-bounded LRU, which exists to cap large per-surface pixel arrays, not a
|
||||
// handful of 4 KiB solid fills. ConcurrentDictionary: MeshExtractor is shared
|
||||
// by up to MaxParallelLoads (4) decode workers.
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<uint, byte[]> _solidColorTextureCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// MP1a mechanical seam: receives particle-preload meshes staged
|
||||
/// mid-extraction (see <see cref="CollectEmittersFromScript"/>). The App
|
||||
|
|
@ -74,7 +83,7 @@ public sealed class MeshExtractor {
|
|||
try {
|
||||
// Use the low 32 bits as the DAT file ID
|
||||
var datId = (uint)(id & 0xFFFFFFFFu);
|
||||
var resolutions = _dats.ResolveId(datId).ToList();
|
||||
var resolutions = _dats.ResolveId(datId);
|
||||
var selectedResolution = resolutions.OrderByDescending(r => r.Database == _dats.Portal).FirstOrDefault();
|
||||
if (selectedResolution == null) return null;
|
||||
|
||||
|
|
@ -191,7 +200,7 @@ public sealed class MeshExtractor {
|
|||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var resolutions = _dats.ResolveId(id).ToList();
|
||||
var resolutions = _dats.ResolveId(id);
|
||||
var selectedResolution = resolutions.OrderByDescending(r => r.Database == _dats.Portal).FirstOrDefault();
|
||||
if (selectedResolution == null) return;
|
||||
|
||||
|
|
@ -377,9 +386,10 @@ public sealed class MeshExtractor {
|
|||
|
||||
if (isSolid) {
|
||||
texWidth = texHeight = 32;
|
||||
textureData = TextureHelpers.CreateSolidColorTexture(surface.ColorValue, texWidth, texHeight);
|
||||
textureData = GetOrCreateSolidColorTexture(surface.ColorValue, texWidth, texHeight);
|
||||
textureFormat = TextureFormat.RGBA8;
|
||||
uploadPixelFormat = UploadPixelFormat.Rgba;
|
||||
textureDataIsCached = true;
|
||||
}
|
||||
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
|
||||
var renderSurfaceId = surfaceTexture.Textures.First();
|
||||
|
|
@ -735,15 +745,17 @@ 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;
|
||||
|
||||
if (isSolid) {
|
||||
texWidth = texHeight = 32;
|
||||
textureData = TextureHelpers.CreateSolidColorTexture(surface.ColorValue, texWidth, texHeight);
|
||||
textureData = GetOrCreateSolidColorTexture(surface.ColorValue, texWidth, texHeight);
|
||||
textureFormat = TextureFormat.RGBA8;
|
||||
uploadPixelFormat = UploadPixelFormat.Rgba;
|
||||
textureDataIsCached = true;
|
||||
}
|
||||
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
|
||||
var renderSurfaceId = surfaceTexture.Textures.First();
|
||||
|
|
@ -773,6 +785,7 @@ public sealed class MeshExtractor {
|
|||
textureData = cachedData;
|
||||
textureFormat = TextureFormat.RGBA8;
|
||||
uploadPixelFormat = UploadPixelFormat.Rgba;
|
||||
textureDataIsCached = true;
|
||||
}
|
||||
else {
|
||||
if (TextureHelpers.IsCompressedFormat(renderSurface.Format)) {
|
||||
|
|
@ -783,7 +796,7 @@ public sealed class MeshExtractor {
|
|||
textureData = _decodedTextureCache.GetOrCreate(
|
||||
decodedTextureKey,
|
||||
() => DecodeCompressedTexture(renderSurface, texWidth, texHeight),
|
||||
out _);
|
||||
out textureDataIsCached);
|
||||
}
|
||||
else {
|
||||
textureFormat = TextureFormat.RGBA8;
|
||||
|
|
@ -841,15 +854,18 @@ public sealed class MeshExtractor {
|
|||
textureData = _decodedTextureCache.RetainOrUse(
|
||||
decodedTextureKey,
|
||||
textureData,
|
||||
out _);
|
||||
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
|
||||
var clonedData = new byte[textureData.Length];
|
||||
System.Buffer.BlockCopy(textureData, 0, clonedData, 0, textureData.Length);
|
||||
textureData = clonedData;
|
||||
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) {
|
||||
if (textureData[i] == 0 && textureData[i + 1] == 0 && textureData[i + 2] == 0) {
|
||||
|
|
@ -1006,6 +1022,24 @@ public sealed class MeshExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the shared 32x32 RGBA fill for <paramref name="color"/>, decoding
|
||||
/// once per distinct ARGB value. Both call sites (GfxObj and CellStruct isSolid
|
||||
/// surfaces) hardcode 32x32 immediately before calling this, so keying purely
|
||||
/// on the packed ARGB value cannot collide with a different-sized request.
|
||||
/// The returned array is shared and must be treated as immutable by callers,
|
||||
/// same contract as <see cref="DecodedTextureCache"/> pixels.
|
||||
/// internal (not private): exercised directly by
|
||||
/// SolidColorTextureCacheTests (AcDream.Content.csproj grants
|
||||
/// InternalsVisibleTo to AcDream.Content.Tests).
|
||||
/// </summary>
|
||||
internal byte[] GetOrCreateSolidColorTexture(DatReaderWriter.Types.ColorARGB color, int width, int height) {
|
||||
uint key = ((uint)color.Alpha << 24) | ((uint)color.Red << 16) | ((uint)color.Green << 8) | color.Blue;
|
||||
return _solidColorTextureCache.GetOrAdd(
|
||||
key,
|
||||
_ => TextureHelpers.CreateSolidColorTexture(color, width, height));
|
||||
}
|
||||
|
||||
private static DecodedTextureKey CreateDecodedTextureKey(
|
||||
uint renderSurfaceId,
|
||||
DatReaderWriter.Enums.PixelFormat format,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue