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,34 +1,27 @@
|
|||
// src/AcDream.App/Rendering/TextureCache.cs
|
||||
using AcDream.Core.Textures;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Linq;
|
||||
using PixelFormatId = DatReaderWriter.Enums.PixelFormat;
|
||||
using SurfaceType = DatReaderWriter.Enums.SurfaceType;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposable
|
||||
public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly DatCollection _dats;
|
||||
private readonly Dictionary<uint, uint> _handlesBySurfaceId = new();
|
||||
private readonly Dictionary<uint, (int w, int h)> _sizeBySurfaceId = new();
|
||||
/// <summary>
|
||||
/// Composite cache for surface-with-override-origtex entries (Phase 5
|
||||
/// TextureChanges). Key = (baseSurfaceId, overrideOrigTextureId),
|
||||
/// value = GL texture handle.
|
||||
/// </summary>
|
||||
private readonly Dictionary<(uint surfaceId, uint origTexOverride), uint> _handlesByOverridden = new();
|
||||
/// <summary>
|
||||
/// Composite cache for palette-overridden entries (Phase 5 SubPalettes).
|
||||
/// Key = (baseSurfaceId, origTexOverride, paletteHash), value = handle.
|
||||
/// paletteHash is a cheap combined hash of the PaletteOverride's ids +
|
||||
/// offsets + lengths so two entities with equivalent palette setups
|
||||
/// share the same decoded texture.
|
||||
/// </summary>
|
||||
private readonly Dictionary<(uint surfaceId, uint origTexOverride, ulong paletteHash), uint> _handlesByPalette = new();
|
||||
private readonly IDatReaderWriter _dats;
|
||||
// Handle and decoded dimensions are one atomic cache entry. Keeping them
|
||||
// in separate dictionaries allowed GetOrUpload(surfaceId) followed by the
|
||||
// sized overload to upload a second GL texture and orphan the first.
|
||||
private readonly Dictionary<uint, (uint Handle, int Width, int Height)>
|
||||
_surfacesById = new();
|
||||
private readonly Dictionary<(uint SurfaceId, uint OrigTextureId), (int Width, int Height)>
|
||||
_decodedDimensionsByTexture = new();
|
||||
private uint _magentaHandle;
|
||||
|
||||
// Direct-RenderSurface caches for UI sprites: 0x06xxxxxx RenderSurface ids
|
||||
|
|
@ -44,15 +37,32 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
private readonly List<uint> _adhocHandles = new();
|
||||
|
||||
private readonly Wb.BindlessSupport? _bindless;
|
||||
private readonly CompositeTextureArrayCache? _compositeTextures;
|
||||
|
||||
// Bindless / Texture2DArray parallel caches. Keys mirror the legacy three
|
||||
// caches so a surface used by both the legacy (Texture2D, sampler2D) and
|
||||
// modern (Texture2DArray, sampler2DArray) paths is uploaded twice — once
|
||||
// per target. Each entry stores both the GL texture name (for Dispose
|
||||
// cleanup) and the resident bindless handle (returned to callers).
|
||||
private readonly Dictionary<uint, (uint Name, ulong Handle)> _bindlessBySurfaceId = new();
|
||||
private readonly Dictionary<(uint surfaceId, uint origTexOverride), (uint Name, ulong Handle)> _bindlessByOverridden = new();
|
||||
private readonly Dictionary<(uint surfaceId, uint origTexOverride, ulong paletteHash), (uint Name, ulong Handle)> _bindlessByPalette = new();
|
||||
// Standalone Texture2DArray caches. Shared world surfaces use WB's atlas;
|
||||
// this base cache remains for consumers such as particle rendering.
|
||||
// Per-entity override composites are owner-scoped but share pooled array
|
||||
// storage. Retail CSurface ownership releases immediately while ImgTex
|
||||
// residency remains separately purgeable; CompositeTextureArrayCache
|
||||
// mirrors that split without one GL object per material composite.
|
||||
private readonly StandaloneBindlessTextureCache? _particleTextures;
|
||||
private readonly Dictionary<(uint surfaceId, uint origTexOverride), bool> _paletteIndexedByTexture = new();
|
||||
|
||||
internal int OwnedBindlessTextureCount => _compositeTextures?.ActiveResourceCount ?? 0;
|
||||
internal int TextureOwnerCount => _compositeTextures?.OwnerCount ?? 0;
|
||||
internal int CachedCompositeTextureCount => _compositeTextures?.CachedEntryCount ?? 0;
|
||||
internal int CachedUnownedCompositeCount => _compositeTextures?.UnownedEntryCount ?? 0;
|
||||
internal long CachedUnownedCompositeBytes => _compositeTextures?.UnownedBytes ?? 0;
|
||||
internal int CompositeAtlasCount => _compositeTextures?.AtlasCount ?? 0;
|
||||
internal long CompositeAtlasBytes => _compositeTextures?.AllocatedBytes ?? 0;
|
||||
internal int CompositeFrameUploadCount => _compositeTextures?.FrameUploadCount ?? 0;
|
||||
internal long CompositeFrameUploadBytes => _compositeTextures?.FrameUploadBytes ?? 0;
|
||||
internal bool CanStartCompositeUpload => _compositeTextures?.CanStartUpload == true;
|
||||
internal int CachedParticleTextureCount => _particleTextures?.EntryCount ?? 0;
|
||||
internal int ActiveParticleTextureCount => _particleTextures?.ActiveResourceCount ?? 0;
|
||||
internal int ParticleTextureOwnerCount => _particleTextures?.OwnerCount ?? 0;
|
||||
internal int CachedUnownedParticleTextureCount => _particleTextures?.UnownedEntryCount ?? 0;
|
||||
internal long CachedUnownedParticleTextureBytes => _particleTextures?.UnownedBytes ?? 0;
|
||||
|
||||
// Phase N.6 slice 1 (2026-05-11): per-upload metadata for the
|
||||
// ACDREAM_DUMP_SURFACES=1 histogram dump path. Populated at upload
|
||||
|
|
@ -68,11 +78,28 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
private int _dumpFrameCounter;
|
||||
private bool _surfaceHistogramAlreadyDumped;
|
||||
|
||||
public TextureCache(GL gl, DatCollection dats, Wb.BindlessSupport? bindless = null)
|
||||
public TextureCache(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
||||
: this(gl, dats, bindless, ImmediateGpuResourceRetirementQueue.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
internal TextureCache(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
Wb.BindlessSupport? bindless,
|
||||
IGpuResourceRetirementQueue retirementQueue)
|
||||
{
|
||||
_gl = gl;
|
||||
_dats = dats;
|
||||
_bindless = bindless;
|
||||
ArgumentNullException.ThrowIfNull(retirementQueue);
|
||||
if (bindless is not null)
|
||||
{
|
||||
_compositeTextures = new CompositeTextureArrayCache(gl, bindless, retirementQueue);
|
||||
_particleTextures = new StandaloneBindlessTextureCache(
|
||||
new ParticleTextureBackend(this),
|
||||
retirementQueue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -81,17 +108,7 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
/// missing or uses an unsupported format.
|
||||
/// </summary>
|
||||
public uint GetOrUpload(uint surfaceId)
|
||||
{
|
||||
if (_handlesBySurfaceId.TryGetValue(surfaceId, out var h))
|
||||
return h;
|
||||
|
||||
var decoded = DecodeFromDats(surfaceId, origTextureOverride: null, paletteOverride: null);
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
|
||||
DumpAlphaHistogram(surfaceId, decoded);
|
||||
h = UploadRgba8(decoded);
|
||||
_handlesBySurfaceId[surfaceId] = h;
|
||||
return h;
|
||||
}
|
||||
=> GetOrUploadSurfaceCore(surfaceId, out _, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="GetOrUpload(uint)"/> but also returns the decoded
|
||||
|
|
@ -99,19 +116,27 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
/// compute slice UVs. Cached alongside the handle.
|
||||
/// </summary>
|
||||
public uint GetOrUpload(uint surfaceId, out int width, out int height)
|
||||
=> GetOrUploadSurfaceCore(surfaceId, out width, out height);
|
||||
|
||||
private uint GetOrUploadSurfaceCore(uint surfaceId, out int width, out int height)
|
||||
{
|
||||
if (_handlesBySurfaceId.TryGetValue(surfaceId, out var existing)
|
||||
&& _sizeBySurfaceId.TryGetValue(surfaceId, out var sz))
|
||||
if (_surfacesById.TryGetValue(surfaceId, out var existing))
|
||||
{
|
||||
width = sz.w; height = sz.h;
|
||||
return existing;
|
||||
width = existing.Width;
|
||||
height = existing.Height;
|
||||
return existing.Handle;
|
||||
}
|
||||
|
||||
var decoded = DecodeFromDats(surfaceId, origTextureOverride: null, paletteOverride: null);
|
||||
DecodedTexture decoded = DecodeFromDats(
|
||||
surfaceId,
|
||||
origTextureOverride: null,
|
||||
paletteOverride: null);
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
|
||||
DumpAlphaHistogram(surfaceId, decoded);
|
||||
uint h = UploadRgba8(decoded);
|
||||
_handlesBySurfaceId[surfaceId] = h;
|
||||
_sizeBySurfaceId[surfaceId] = (decoded.Width, decoded.Height);
|
||||
width = decoded.Width; height = decoded.Height;
|
||||
_surfacesById.Add(surfaceId, (h, decoded.Width, decoded.Height));
|
||||
width = decoded.Width;
|
||||
height = decoded.Height;
|
||||
return h;
|
||||
}
|
||||
|
||||
|
|
@ -200,129 +225,228 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or upload a texture for a Surface id but with its
|
||||
/// <c>OrigTextureId</c> replaced by <paramref name="overrideOrigTextureId"/>.
|
||||
/// The Surface's other properties (type flags, color, translucency,
|
||||
/// clipmap handling, default palette) are preserved — only the
|
||||
/// SurfaceTexture lookup is swapped. This is how the server's
|
||||
/// CreateObject.TextureChanges are applied at render time.
|
||||
/// Caches under a composite key so multiple entities can share.
|
||||
/// Acquires the exact DAT-decoded one-layer texture array for a live
|
||||
/// particle emitter. Equivalent surfaces are shared; the cache ownership
|
||||
/// ends with <see cref="ReleaseParticleTextureOwner"/>.
|
||||
/// </summary>
|
||||
public uint GetOrUploadWithOrigTextureOverride(uint surfaceId, uint overrideOrigTextureId)
|
||||
internal ulong AcquireParticleTexture(int emitterHandle, uint surfaceId)
|
||||
{
|
||||
var key = (surfaceId, overrideOrigTextureId);
|
||||
if (_handlesByOverridden.TryGetValue(key, out var h))
|
||||
return h;
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(emitterHandle);
|
||||
ArgumentOutOfRangeException.ThrowIfZero(surfaceId);
|
||||
StandaloneBindlessTextureCache textures = EnsureParticleTexturesAvailable();
|
||||
uint ownerId = checked((uint)emitterHandle);
|
||||
if (textures.TryAcquire(
|
||||
ownerId,
|
||||
surfaceId,
|
||||
out StandaloneBindlessTextureResource? existing))
|
||||
{
|
||||
return existing.Handle;
|
||||
}
|
||||
|
||||
var decoded = DecodeFromDats(surfaceId, origTextureOverride: overrideOrigTextureId, paletteOverride: null);
|
||||
h = UploadRgba8(decoded);
|
||||
_handlesByOverridden[key] = h;
|
||||
return h;
|
||||
DecodedTexture decoded = DecodeFromDats(
|
||||
surfaceId,
|
||||
origTextureOverride: null,
|
||||
paletteOverride: null);
|
||||
uint name = UploadRgba8AsLayer1Array(decoded);
|
||||
ulong handle = 0;
|
||||
try
|
||||
{
|
||||
handle = _bindless!.GetResidentHandle(name);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"making particle surface 0x{surfaceId:X8} resident");
|
||||
var resource = new StandaloneBindlessTextureResource
|
||||
{
|
||||
SurfaceId = surfaceId,
|
||||
Name = name,
|
||||
Handle = handle,
|
||||
Bytes = checked((long)decoded.Width * decoded.Height * 4L),
|
||||
};
|
||||
textures.AddAndAcquire(ownerId, resource);
|
||||
return handle;
|
||||
}
|
||||
catch (Exception residencyFailure)
|
||||
{
|
||||
List<Exception>? cleanupFailures = null;
|
||||
void Attempt(Action cleanup)
|
||||
{
|
||||
try { cleanup(); }
|
||||
catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
|
||||
}
|
||||
|
||||
bool residencyReleased = handle == 0;
|
||||
if (handle != 0)
|
||||
{
|
||||
Attempt(() =>
|
||||
{
|
||||
_bindless!.MakeNonResident(handle);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
"rolling back particle texture residency");
|
||||
residencyReleased = true;
|
||||
});
|
||||
}
|
||||
if (residencyReleased)
|
||||
Attempt(() => DeleteUploadedTexture(name));
|
||||
if (cleanupFailures is not null)
|
||||
{
|
||||
cleanupFailures.Insert(0, residencyFailure);
|
||||
throw new AggregateException(
|
||||
"Particle texture residency and rollback both failed.",
|
||||
cleanupFailures);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
internal void ReleaseParticleTextureOwner(int emitterHandle)
|
||||
{
|
||||
if (emitterHandle <= 0 || _particleTextures is null)
|
||||
return;
|
||||
_particleTextures.ReleaseOwner(checked((uint)emitterHandle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full Phase 5 override: for palette-indexed textures (PFID_P8 /
|
||||
/// PFID_INDEX16), applies <paramref name="paletteOverride"/>'s
|
||||
/// subpalette overlays on top of the texture's default palette
|
||||
/// before decoding. Non-palette formats ignore the palette override.
|
||||
/// Also honors <paramref name="overrideOrigTextureId"/> if non-null.
|
||||
/// Owner-scoped bindless variant for a server-supplied original-texture
|
||||
/// replacement. Stores compatible composites in a pooled Texture2DArray
|
||||
/// and returns its resident handle plus the assigned layer. Equivalent
|
||||
/// composites are shared until their final live owner leaves. Throws if
|
||||
/// BindlessSupport wasn't provided.
|
||||
/// </summary>
|
||||
public uint GetOrUploadWithPaletteOverride(
|
||||
internal BindlessTextureLocation GetOrUploadWithOrigTextureOverrideBindless(
|
||||
uint ownerLocalId,
|
||||
uint surfaceId,
|
||||
uint? overrideOrigTextureId,
|
||||
PaletteOverride paletteOverride)
|
||||
=> GetOrUploadWithPaletteOverride(surfaceId, overrideOrigTextureId, paletteOverride,
|
||||
HashPaletteOverride(paletteOverride));
|
||||
|
||||
/// <summary>
|
||||
/// Overload that accepts a precomputed palette hash. Lets callers (e.g.
|
||||
/// the WB draw dispatcher) compute the hash ONCE per entity and reuse
|
||||
/// it across every (part, batch) lookup, avoiding the per-batch
|
||||
/// FNV-1a fold over <see cref="PaletteOverride.SubPalettes"/>.
|
||||
/// </summary>
|
||||
public uint GetOrUploadWithPaletteOverride(
|
||||
uint surfaceId,
|
||||
uint? overrideOrigTextureId,
|
||||
PaletteOverride paletteOverride,
|
||||
ulong precomputedPaletteHash)
|
||||
uint overrideOrigTextureId)
|
||||
{
|
||||
uint origTexKey = overrideOrigTextureId ?? 0;
|
||||
var key = (surfaceId, origTexKey, precomputedPaletteHash);
|
||||
if (_handlesByPalette.TryGetValue(key, out var h))
|
||||
return h;
|
||||
CompositeTextureArrayCache composites = EnsureCompositeTexturesAvailable();
|
||||
var key = new CompositeTextureKey(
|
||||
CompositeTextureKind.OriginalTextureOverride,
|
||||
surfaceId,
|
||||
overrideOrigTextureId,
|
||||
Palette: default);
|
||||
if (composites.TryAcquire(ownerLocalId, key, out BindlessTextureLocation existing))
|
||||
return existing;
|
||||
if (!composites.CanStartUpload)
|
||||
return default;
|
||||
(int width, int height) = ResolveDecodedDimensions(surfaceId, overrideOrigTextureId);
|
||||
if (!composites.CanPrepareUpload(width, height))
|
||||
return default;
|
||||
|
||||
var decoded = DecodeFromDats(surfaceId, origTextureOverride: overrideOrigTextureId, paletteOverride: paletteOverride);
|
||||
h = UploadRgba8(decoded);
|
||||
_handlesByPalette[key] = h;
|
||||
return h;
|
||||
DecodedTexture decoded = DecodeFromDats(
|
||||
surfaceId,
|
||||
origTextureOverride: overrideOrigTextureId,
|
||||
paletteOverride: null);
|
||||
return composites.TryAddAndAcquire(ownerLocalId, key, decoded, out BindlessTextureLocation added)
|
||||
? added
|
||||
: default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 64-bit bindless handle variant of <see cref="GetOrUpload"/> for the WB
|
||||
/// modern rendering path. Uploads the texture as a 1-layer Texture2DArray
|
||||
/// (so the shader's <c>sampler2DArray</c> can sample at layer 0) and returns
|
||||
/// a resident bindless handle. Caches by surfaceId in a separate dictionary
|
||||
/// from the legacy Texture2D path; the same surface may be uploaded twice
|
||||
/// if used by both paths (acceptable transition cost — N.6 deletes the legacy
|
||||
/// path).
|
||||
/// Owner-scoped bindless palette composite. Applies the palette override on
|
||||
/// top of the texture's default palette before decoding, stores compatible
|
||||
/// composites in a pooled Texture2DArray, and returns its resident handle
|
||||
/// plus the assigned layer. Structural identity is computed once per entity.
|
||||
/// Throws if BindlessSupport wasn't provided to the constructor.
|
||||
/// </summary>
|
||||
public ulong GetOrUploadBindless(uint surfaceId)
|
||||
{
|
||||
EnsureBindlessAvailable();
|
||||
if (_bindlessBySurfaceId.TryGetValue(surfaceId, out var entry))
|
||||
return entry.Handle;
|
||||
var decoded = DecodeFromDats(surfaceId, origTextureOverride: null, paletteOverride: null);
|
||||
uint name = UploadRgba8AsLayer1Array(decoded);
|
||||
ulong handle = _bindless!.GetResidentHandle(name);
|
||||
_bindlessBySurfaceId[surfaceId] = (name, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 64-bit bindless handle variant of <see cref="GetOrUploadWithOrigTextureOverride"/>
|
||||
/// for the WB modern rendering path. Uploads the texture as a 1-layer
|
||||
/// Texture2DArray with the override SurfaceTexture id and returns a resident
|
||||
/// bindless handle. Caches under a separate composite key from the legacy
|
||||
/// path. Throws if BindlessSupport wasn't provided to the constructor.
|
||||
/// </summary>
|
||||
public ulong GetOrUploadWithOrigTextureOverrideBindless(uint surfaceId, uint overrideOrigTextureId)
|
||||
{
|
||||
EnsureBindlessAvailable();
|
||||
var key = (surfaceId, overrideOrigTextureId);
|
||||
if (_bindlessByOverridden.TryGetValue(key, out var entry))
|
||||
return entry.Handle;
|
||||
var decoded = DecodeFromDats(surfaceId, origTextureOverride: overrideOrigTextureId, paletteOverride: null);
|
||||
uint name = UploadRgba8AsLayer1Array(decoded);
|
||||
ulong handle = _bindless!.GetResidentHandle(name);
|
||||
_bindlessByOverridden[key] = (name, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 64-bit bindless handle variant of <see cref="GetOrUploadWithPaletteOverride"/>
|
||||
/// for the WB modern rendering path. Applies the palette override on top of
|
||||
/// the texture's default palette before decoding, uploads as a 1-layer
|
||||
/// Texture2DArray, and returns a resident bindless handle. Takes a
|
||||
/// precomputed palette hash so the WB dispatcher can compute it once per
|
||||
/// entity. Throws if BindlessSupport wasn't provided to the constructor.
|
||||
/// </summary>
|
||||
public ulong GetOrUploadWithPaletteOverrideBindless(
|
||||
internal BindlessTextureLocation GetOrUploadWithPaletteOverrideBindless(
|
||||
uint ownerLocalId,
|
||||
uint surfaceId,
|
||||
uint? overrideOrigTextureId,
|
||||
PaletteOverride paletteOverride,
|
||||
ulong precomputedPaletteHash)
|
||||
PaletteCompositeIdentity paletteIdentity)
|
||||
{
|
||||
EnsureBindlessAvailable();
|
||||
CompositeTextureArrayCache composites = EnsureCompositeTexturesAvailable();
|
||||
uint origTexKey = overrideOrigTextureId ?? 0;
|
||||
var key = (surfaceId, origTexKey, precomputedPaletteHash);
|
||||
if (_bindlessByPalette.TryGetValue(key, out var entry))
|
||||
return entry.Handle;
|
||||
var decoded = DecodeFromDats(surfaceId, origTextureOverride: overrideOrigTextureId, paletteOverride: paletteOverride);
|
||||
uint name = UploadRgba8AsLayer1Array(decoded);
|
||||
ulong handle = _bindless!.GetResidentHandle(name);
|
||||
_bindlessByPalette[key] = (name, handle);
|
||||
return handle;
|
||||
var key = new CompositeTextureKey(
|
||||
CompositeTextureKind.PaletteComposite,
|
||||
surfaceId,
|
||||
origTexKey,
|
||||
paletteIdentity);
|
||||
if (composites.TryAcquire(ownerLocalId, key, out BindlessTextureLocation existing))
|
||||
return existing;
|
||||
if (!composites.CanStartUpload)
|
||||
return default;
|
||||
(int width, int height) = ResolveDecodedDimensions(surfaceId, overrideOrigTextureId);
|
||||
if (!composites.CanPrepareUpload(width, height))
|
||||
return default;
|
||||
|
||||
DecodedTexture decoded = DecodeFromDats(
|
||||
surfaceId,
|
||||
origTextureOverride: overrideOrigTextureId,
|
||||
paletteOverride: paletteOverride);
|
||||
return composites.TryAddAndAcquire(ownerLocalId, key, decoded, out BindlessTextureLocation added)
|
||||
? added
|
||||
: default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail applies a palette composite only to P8/INDEX16 image data.
|
||||
/// Cache the resolved source format so animated entities do not reopen the
|
||||
/// DAT chain every frame.
|
||||
/// </summary>
|
||||
internal bool IsPaletteIndexed(uint surfaceId, uint? overrideOrigTextureId)
|
||||
{
|
||||
uint origTexKey = overrideOrigTextureId ?? 0;
|
||||
var key = (surfaceId, origTexKey);
|
||||
if (_paletteIndexedByTexture.TryGetValue(key, out bool indexed))
|
||||
return indexed;
|
||||
|
||||
Surface? surface = _dats.Get<Surface>(surfaceId);
|
||||
if (surface is null || surface.Type.HasFlag(SurfaceType.Base1Solid))
|
||||
return _paletteIndexedByTexture[key] = false;
|
||||
|
||||
uint surfaceTextureId = overrideOrigTextureId ?? (uint)surface.OrigTextureId;
|
||||
SurfaceTexture? texture = _dats.Get<SurfaceTexture>(surfaceTextureId);
|
||||
if (texture is null || texture.Textures.Count == 0)
|
||||
return _paletteIndexedByTexture[key] = false;
|
||||
|
||||
uint renderSurfaceId = (uint)texture.Textures[0];
|
||||
if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out RenderSurface? renderSurface)
|
||||
&& !_dats.HighRes.TryGet<RenderSurface>(renderSurfaceId, out renderSurface))
|
||||
return _paletteIndexedByTexture[key] = false;
|
||||
|
||||
indexed = renderSurface.Format is PixelFormatId.PFID_P8 or PixelFormatId.PFID_INDEX16;
|
||||
_paletteIndexedByTexture[key] = indexed;
|
||||
return indexed;
|
||||
}
|
||||
|
||||
private (int Width, int Height) ResolveDecodedDimensions(
|
||||
uint surfaceId,
|
||||
uint? overrideOrigTextureId)
|
||||
{
|
||||
var key = (surfaceId, overrideOrigTextureId ?? 0);
|
||||
if (_decodedDimensionsByTexture.TryGetValue(key, out var cached))
|
||||
return cached;
|
||||
|
||||
Surface? surface = _dats.Get<Surface>(surfaceId);
|
||||
if (surface is null
|
||||
|| surface.Type.HasFlag(SurfaceType.Base1Solid)
|
||||
|| (uint)surface.OrigTextureId == 0)
|
||||
return _decodedDimensionsByTexture[key] = (1, 1);
|
||||
|
||||
uint surfaceTextureId = overrideOrigTextureId ?? (uint)surface.OrigTextureId;
|
||||
SurfaceTexture? texture = _dats.Get<SurfaceTexture>(surfaceTextureId);
|
||||
if (texture is null || texture.Textures.Count == 0)
|
||||
return _decodedDimensionsByTexture[key] = (1, 1);
|
||||
|
||||
uint renderSurfaceId = (uint)texture.Textures[0];
|
||||
if ((!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out RenderSurface? renderSurface)
|
||||
&& !_dats.HighRes.TryGet<RenderSurface>(renderSurfaceId, out renderSurface))
|
||||
|| renderSurface.Width <= 0
|
||||
|| renderSurface.Height <= 0
|
||||
|| renderSurface.SourceData is null)
|
||||
return _decodedDimensionsByTexture[key] = (1, 1);
|
||||
|
||||
return _decodedDimensionsByTexture[key] = (renderSurface.Width, renderSurface.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CSurface::Destroy</c> (0x005361F0) releases its current
|
||||
/// <c>ImgTex</c>. Mirror that ownership boundary for per-entity composites.
|
||||
/// </summary>
|
||||
public void ReleaseOwner(uint localEntityId)
|
||||
{
|
||||
EnsureCompositeTexturesAvailable().ReleaseOwner(localEntityId);
|
||||
}
|
||||
|
||||
private void EnsureBindlessAvailable()
|
||||
|
|
@ -333,6 +457,49 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
"WbDrawDispatcher requires the bindless-aware ctor overload (pass non-null BindlessSupport).");
|
||||
}
|
||||
|
||||
private CompositeTextureArrayCache EnsureCompositeTexturesAvailable()
|
||||
{
|
||||
EnsureBindlessAvailable();
|
||||
return _compositeTextures!;
|
||||
}
|
||||
|
||||
private StandaloneBindlessTextureCache EnsureParticleTexturesAvailable()
|
||||
{
|
||||
EnsureBindlessAvailable();
|
||||
return _particleTextures!;
|
||||
}
|
||||
|
||||
private sealed class ParticleTextureBackend(TextureCache owner)
|
||||
: IStandaloneBindlessTextureBackend
|
||||
{
|
||||
public void MakeNonResident(StandaloneBindlessTextureResource resource)
|
||||
{
|
||||
owner._bindless!.MakeNonResident(resource.Handle);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
owner._gl,
|
||||
$"releasing particle texture handle {resource.Handle}");
|
||||
}
|
||||
|
||||
public void Delete(StandaloneBindlessTextureResource resource)
|
||||
=> owner.DeleteUploadedTexture(resource.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances bounded composite-cache maintenance once per render frame.
|
||||
/// Logical owner release is immediate; at most one over-budget layer and
|
||||
/// one empty backing array are physically retired in this call.
|
||||
/// </summary>
|
||||
public void TickCompositeTextureCache() => _compositeTextures?.Tick();
|
||||
|
||||
/// <summary>
|
||||
/// Retires at most one over-budget standalone particle texture per render
|
||||
/// frame. Keeping this separate from owner release avoids portal-time GPU
|
||||
/// destruction bursts without changing live particle range or quality.
|
||||
/// </summary>
|
||||
public void TickParticleTextureCache() => _particleTextures?.Tick();
|
||||
|
||||
public void BeginCompositeTextureFrame() => _compositeTextures?.BeginFrame();
|
||||
|
||||
/// <summary>
|
||||
/// Cheap 64-bit hash over a palette override's identity so two
|
||||
/// entities with the same palette setup share a decode. Internal so
|
||||
|
|
@ -354,6 +521,9 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
return h;
|
||||
}
|
||||
|
||||
internal static PaletteCompositeIdentity GetPaletteIdentity(PaletteOverride palette) =>
|
||||
new(palette, HashPaletteOverride(palette));
|
||||
|
||||
/// <summary>
|
||||
/// Phase N.6 slice 1: one-shot surface-format histogram dump for the
|
||||
/// atlas-opportunity audit. Activated by ACDREAM_DUMP_SURFACES=1; fires
|
||||
|
|
@ -434,12 +604,19 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
bucketsByTriple[tripleKey] = bucketsByTriple.GetValueOrDefault(tripleKey) + 1;
|
||||
}
|
||||
|
||||
foreach (var kv in _handlesBySurfaceId) Emit(kv.Key, kv.Value);
|
||||
foreach (var kv in _handlesByOverridden) Emit(kv.Key.surfaceId, kv.Value);
|
||||
foreach (var kv in _handlesByPalette) Emit(kv.Key.surfaceId, kv.Value);
|
||||
foreach (var kv in _bindlessBySurfaceId) Emit(kv.Key, kv.Value.Name);
|
||||
foreach (var kv in _bindlessByOverridden) Emit(kv.Key.surfaceId, kv.Value.Name);
|
||||
foreach (var kv in _bindlessByPalette) Emit(kv.Key.surfaceId, kv.Value.Name);
|
||||
foreach (var kv in _surfacesById) Emit(kv.Key, kv.Value.Handle);
|
||||
_particleTextures?.VisitEntries(resource => Emit(resource.SurfaceId, resource.Name));
|
||||
_compositeTextures?.VisitEntries((surfaceId, width, height) =>
|
||||
{
|
||||
int bytes = checked(width * height * 4);
|
||||
totalBytes += bytes;
|
||||
sb.AppendLine($"0x{surfaceId:X8}, {width}, {height}, RGBA8_COMPOSITE_LAYER, {bytes}");
|
||||
bucketsByDim[(width, height)] = bucketsByDim.GetValueOrDefault((width, height)) + 1;
|
||||
bucketsByFormat["RGBA8_COMPOSITE_LAYER"] =
|
||||
bucketsByFormat.GetValueOrDefault("RGBA8_COMPOSITE_LAYER") + 1;
|
||||
bucketsByTriple[(width, height, "RGBA8_COMPOSITE_LAYER")] =
|
||||
bucketsByTriple.GetValueOrDefault((width, height, "RGBA8_COMPOSITE_LAYER")) + 1;
|
||||
});
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("# Rollups");
|
||||
|
|
@ -572,31 +749,47 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
|
||||
{
|
||||
uint tex = _gl.GenTexture();
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
if (tex == 0)
|
||||
throw new InvalidOperationException("OpenGL did not create a 2D texture.");
|
||||
try
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
fixed (byte* p = decoded.Rgba8)
|
||||
_gl.TexImage2D(
|
||||
TextureTarget.Texture2D,
|
||||
0,
|
||||
InternalFormat.Rgba8,
|
||||
(uint)decoded.Width,
|
||||
(uint)decoded.Height,
|
||||
0,
|
||||
PixelFormat.Rgba,
|
||||
PixelType.UnsignedByte,
|
||||
p);
|
||||
fixed (byte* p = decoded.Rgba8)
|
||||
_gl.TexImage2D(
|
||||
TextureTarget.Texture2D,
|
||||
0,
|
||||
InternalFormat.Rgba8,
|
||||
(uint)decoded.Width,
|
||||
(uint)decoded.Height,
|
||||
0,
|
||||
PixelFormat.Rgba,
|
||||
PixelType.UnsignedByte,
|
||||
p);
|
||||
|
||||
// Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat
|
||||
// font's small glyphs. Other surfaces use bilinear.
|
||||
int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear;
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, filter);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
// Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat
|
||||
// font's small glyphs. Other surfaces use bilinear.
|
||||
int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear;
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, filter);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"uploading 2D RGBA8 texture {decoded.Width}x{decoded.Height}");
|
||||
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
_uploadMetadata[tex] = (decoded.Width, decoded.Height, "RGBA8_DECODED");
|
||||
return tex;
|
||||
TrackUploadedTexture(tex, decoded.Width, decoded.Height);
|
||||
return tex;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_gl.DeleteTexture(tex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -607,88 +800,99 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
private uint UploadRgba8AsLayer1Array(DecodedTexture decoded)
|
||||
{
|
||||
uint tex = _gl.GenTexture();
|
||||
_gl.BindTexture(TextureTarget.Texture2DArray, tex);
|
||||
if (tex == 0)
|
||||
throw new InvalidOperationException("OpenGL did not create a one-layer texture array.");
|
||||
try
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2DArray, tex);
|
||||
|
||||
fixed (byte* p = decoded.Rgba8)
|
||||
_gl.TexImage3D(
|
||||
TextureTarget.Texture2DArray,
|
||||
0,
|
||||
InternalFormat.Rgba8,
|
||||
(uint)decoded.Width,
|
||||
(uint)decoded.Height,
|
||||
depth: 1,
|
||||
border: 0,
|
||||
PixelFormat.Rgba,
|
||||
PixelType.UnsignedByte,
|
||||
p);
|
||||
fixed (byte* p = decoded.Rgba8)
|
||||
_gl.TexImage3D(
|
||||
TextureTarget.Texture2DArray,
|
||||
0,
|
||||
InternalFormat.Rgba8,
|
||||
(uint)decoded.Width,
|
||||
(uint)decoded.Height,
|
||||
depth: 1,
|
||||
border: 0,
|
||||
PixelFormat.Rgba,
|
||||
PixelType.UnsignedByte,
|
||||
p);
|
||||
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
$"uploading one-layer RGBA8 array {decoded.Width}x{decoded.Height}");
|
||||
|
||||
_gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
_uploadMetadata[tex] = (decoded.Width, decoded.Height, "RGBA8_DECODED");
|
||||
return tex;
|
||||
TrackUploadedTexture(tex, decoded.Width, decoded.Height);
|
||||
return tex;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_gl.DeleteTexture(tex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackUploadedTexture(uint name, int width, int height)
|
||||
{
|
||||
_uploadMetadata[name] = (width, height, "RGBA8_DECODED");
|
||||
long bytes = checked((long)width * height * 4L);
|
||||
Wb.GpuMemoryTracker.TrackResourceAllocation(Wb.GpuResourceType.Texture);
|
||||
Wb.GpuMemoryTracker.TrackAllocation(bytes, Wb.GpuResourceType.Texture);
|
||||
}
|
||||
|
||||
private void DeleteUploadedTexture(uint name)
|
||||
{
|
||||
_gl.DeleteTexture(name);
|
||||
Wb.GLHelpers.ThrowOnResourceError(_gl, $"deleting uploaded texture {name}");
|
||||
|
||||
if (_uploadMetadata.Remove(name, out var metadata))
|
||||
{
|
||||
long bytes = checked((long)metadata.Width * metadata.Height * 4L);
|
||||
Wb.GpuMemoryTracker.TrackDeallocation(bytes, Wb.GpuResourceType.Texture);
|
||||
Wb.GpuMemoryTracker.TrackResourceDeallocation(Wb.GpuResourceType.Texture);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Phase 1: make all bindless handles non-resident BEFORE any
|
||||
// DeleteTexture call. ARB_bindless_texture requires that resident
|
||||
// handles be released before their backing texture is deleted —
|
||||
// interleaving per-entry is UB. Single null-guard around the whole
|
||||
// block (cleaner than per-call null-conditionals).
|
||||
if (_bindless is not null)
|
||||
{
|
||||
foreach (var (_, handle) in _bindlessBySurfaceId.Values)
|
||||
_bindless.MakeNonResident(handle);
|
||||
foreach (var (_, handle) in _bindlessByOverridden.Values)
|
||||
_bindless.MakeNonResident(handle);
|
||||
foreach (var (_, handle) in _bindlessByPalette.Values)
|
||||
_bindless.MakeNonResident(handle);
|
||||
}
|
||||
// GameWindow drains frame-flight fences before this teardown. The
|
||||
// bindless caches make every handle non-resident before deleting
|
||||
// their backing storage.
|
||||
_particleTextures?.Dispose();
|
||||
_compositeTextures?.Dispose();
|
||||
|
||||
// Phase 2: delete the Texture2DArray textures backing those handles.
|
||||
foreach (var (name, _) in _bindlessBySurfaceId.Values)
|
||||
_gl.DeleteTexture(name);
|
||||
_bindlessBySurfaceId.Clear();
|
||||
foreach (var (name, _) in _bindlessByOverridden.Values)
|
||||
_gl.DeleteTexture(name);
|
||||
_bindlessByOverridden.Clear();
|
||||
foreach (var (name, _) in _bindlessByPalette.Values)
|
||||
_gl.DeleteTexture(name);
|
||||
_bindlessByPalette.Clear();
|
||||
_paletteIndexedByTexture.Clear();
|
||||
|
||||
// Phase 3: legacy Texture2D textures.
|
||||
foreach (var h in _handlesBySurfaceId.Values)
|
||||
_gl.DeleteTexture(h);
|
||||
_handlesBySurfaceId.Clear();
|
||||
|
||||
foreach (var h in _handlesByOverridden.Values)
|
||||
_gl.DeleteTexture(h);
|
||||
_handlesByOverridden.Clear();
|
||||
|
||||
foreach (var h in _handlesByPalette.Values)
|
||||
_gl.DeleteTexture(h);
|
||||
_handlesByPalette.Clear();
|
||||
// Legacy Texture2D textures.
|
||||
foreach (var entry in _surfacesById.Values)
|
||||
DeleteUploadedTexture(entry.Handle);
|
||||
_surfacesById.Clear();
|
||||
|
||||
if (_magentaHandle != 0)
|
||||
{
|
||||
_gl.DeleteTexture(_magentaHandle);
|
||||
DeleteUploadedTexture(_magentaHandle);
|
||||
_magentaHandle = 0;
|
||||
}
|
||||
|
||||
// RenderSurface (UI sprite) handles — pre-existing gap: this dict was populated
|
||||
// by GetOrUploadRenderSurface but was not swept here before this fix.
|
||||
foreach (var h in _handlesByRenderSurfaceId.Values)
|
||||
_gl.DeleteTexture(h);
|
||||
DeleteUploadedTexture(h);
|
||||
_handlesByRenderSurfaceId.Clear();
|
||||
|
||||
// Ad-hoc handles from the public UploadRgba8(byte[],int,int,bool) wrapper
|
||||
// (IconComposer composited icons). Not stored in any keyed cache.
|
||||
foreach (var h in _adhocHandles)
|
||||
_gl.DeleteTexture(h);
|
||||
DeleteUploadedTexture(h);
|
||||
_adhocHandles.Clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue