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
|
|
@ -6,6 +6,7 @@ using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public class ManagedGLTextureArray : ITextureArray {
|
||||
|
|
@ -18,14 +19,15 @@ namespace AcDream.App.Rendering.Wb {
|
|||
private readonly bool _isCompressed;
|
||||
private int _mipmapDirtyCount = 0;
|
||||
private readonly object _mipmapLock = new object();
|
||||
private uint _pboId;
|
||||
private int _pboSize;
|
||||
private readonly List<TextureLayerUpdate> _pendingUpdates = new();
|
||||
private int _disposeQueued;
|
||||
private int _disposePublicationQueued;
|
||||
private int _disposeRetirementAccepted;
|
||||
private RetryableGpuResourceRelease? _disposeRelease;
|
||||
|
||||
private struct TextureLayerUpdate {
|
||||
public int Layer;
|
||||
public int Offset;
|
||||
public int Size;
|
||||
public required byte[] Data;
|
||||
public PixelFormat? UploadPixelFormat;
|
||||
public PixelType? UploadPixelType;
|
||||
}
|
||||
|
|
@ -36,13 +38,12 @@ namespace AcDream.App.Rendering.Wb {
|
|||
public int Size { get; private set; }
|
||||
public TextureFormat Format { get; private set; }
|
||||
public nint NativePtr { get; private set; }
|
||||
public ulong BindlessHandle { get; private set; }
|
||||
public ulong BindlessWrapHandle { get; private set; }
|
||||
public ulong BindlessClampHandle { get; private set; }
|
||||
public long TotalSizeInBytes => CalculateTotalSize();
|
||||
|
||||
/// <summary>
|
||||
/// #105 diagnostic: staged layer updates (PBO writes + pending list) not yet
|
||||
/// #105 diagnostic: staged layer updates (retained decoded payloads) not yet
|
||||
/// applied to the GL texture by <see cref="ProcessDirtyUpdates"/>. Layers with
|
||||
/// a pending update sample UNDEFINED content (TexStorage3D contents) until the
|
||||
/// flush runs — a stuck non-zero count at standstill is the white-walls mechanism.
|
||||
|
|
@ -69,67 +70,121 @@ namespace AcDream.App.Rendering.Wb {
|
|||
_isCompressed = IsCompressedFormat(format);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
NativePtr = (nint)GL.GenTexture();
|
||||
if (NativePtr == 0) {
|
||||
throw new InvalidOperationException("Failed to generate texture array.");
|
||||
}
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
|
||||
uint textureName = 0;
|
||||
ulong wrapHandle = 0;
|
||||
ulong clampHandle = 0;
|
||||
bool textureTracked = false;
|
||||
bool textureBytesTracked = false;
|
||||
bool wrapResident = false;
|
||||
bool clampResident = false;
|
||||
long textureBytes = CalculateTotalSize();
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
try {
|
||||
textureName = GL.GenTexture();
|
||||
if (textureName == 0)
|
||||
throw new InvalidOperationException("Failed to generate texture array.");
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
|
||||
textureTracked = true;
|
||||
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, textureName);
|
||||
|
||||
int maxDimension = Math.Max(width, height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
int maxDimension = Math.Max(width, height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
|
||||
GL.TexStorage3D(GLEnum.Texture2DArray, (uint)mipLevels, format.ToGL(), (uint)width, (uint)height,
|
||||
(uint)size);
|
||||
GLHelpers.CheckErrorsWithContext(GL,
|
||||
$"Creating texture array storage (Format={format}, Size={width}x{height}x{size}, MipLevels={mipLevels})");
|
||||
GL.TexStorage3D(GLEnum.Texture2DArray, (uint)mipLevels, format.ToGL(), (uint)width, (uint)height,
|
||||
(uint)size);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMinFilter,
|
||||
(int)p.MinFilter);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMaxLevel, mipLevels - 1);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapS, (int)p.WrapS);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapT, (int)p.WrapT);
|
||||
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMinFilter,
|
||||
(int)p.MinFilter);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMaxLevel, (int)mipLevels - 1);
|
||||
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
|
||||
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapS, (int)p.WrapS);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapT, (int)p.WrapT);
|
||||
|
||||
if (p.EnableAnisotropicFiltering && graphicsDevice.RenderSettings.EnableAnisotropicFiltering) {
|
||||
float maxAnisotropy = 0f;
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out maxAnisotropy);
|
||||
|
||||
if (maxAnisotropy > 0) {
|
||||
GL.TexParameter(GLEnum.Texture2DArray, GLEnum.TextureMaxAnisotropy, maxAnisotropy);
|
||||
if (p.EnableAnisotropicFiltering
|
||||
&& graphicsDevice.RenderSettings.EnableAnisotropicFiltering
|
||||
&& graphicsDevice.MaxSupportedAnisotropy > 0) {
|
||||
GL.TexParameter(
|
||||
GLEnum.Texture2DArray,
|
||||
GLEnum.TextureMaxAnisotropy,
|
||||
graphicsDevice.MaxSupportedAnisotropy);
|
||||
}
|
||||
|
||||
if (format == TextureFormat.A8) {
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleR, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleG, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleB, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleA, (int)GLEnum.Red);
|
||||
}
|
||||
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
GL,
|
||||
$"creating texture array {format} {width}x{height}x{size} ({mipLevels} mip levels)");
|
||||
GpuMemoryTracker.TrackAllocation(textureBytes, GpuResourceType.Texture);
|
||||
textureBytesTracked = true;
|
||||
|
||||
if (_device.HasBindless && _device.BindlessExtension != null) {
|
||||
wrapHandle = _device.BindlessExtension.GetTextureSamplerHandle(textureName, _device.WrapSampler);
|
||||
clampHandle = _device.BindlessExtension.GetTextureSamplerHandle(textureName, _device.ClampSampler);
|
||||
_device.BindlessExtension.MakeTextureHandleResident(wrapHandle);
|
||||
wrapResident = true;
|
||||
_device.BindlessExtension.MakeTextureHandleResident(clampHandle);
|
||||
clampResident = true;
|
||||
GLHelpers.ThrowOnResourceError(GL, "making texture-array sampler handles resident");
|
||||
}
|
||||
|
||||
NativePtr = (nint)textureName;
|
||||
BindlessWrapHandle = wrapHandle;
|
||||
BindlessClampHandle = clampHandle;
|
||||
}
|
||||
|
||||
// Set texture swizzle for single-channel formats
|
||||
if (format == TextureFormat.A8) {
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleR, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleG, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleB, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleA, (int)GLEnum.Red);
|
||||
catch (Exception constructionFailure) {
|
||||
// Constructor failure cannot use Dispose: the object was never
|
||||
// published and queued teardown would make retries accumulate
|
||||
// invalid resident handles. Attempt every independent cleanup.
|
||||
List<Exception>? cleanupFailures = null;
|
||||
void Attempt(Action cleanup) {
|
||||
try { cleanup(); }
|
||||
catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
|
||||
}
|
||||
if (_device.BindlessExtension != null) {
|
||||
if (clampResident)
|
||||
Attempt(() => {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(clampHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "rolling back clamp texture-array handle");
|
||||
clampResident = false;
|
||||
});
|
||||
if (wrapResident)
|
||||
Attempt(() => {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(wrapHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "rolling back wrap texture-array handle");
|
||||
wrapResident = false;
|
||||
});
|
||||
}
|
||||
// Deleting a texture while either bindless sampler handle is
|
||||
// still resident is undefined. A pre-commit residency failure
|
||||
// therefore retains the texture instead of risking a driver
|
||||
// reset during constructor rollback.
|
||||
if (textureName != 0 && !clampResident && !wrapResident)
|
||||
Attempt(() => {
|
||||
GL.DeleteTexture(textureName);
|
||||
GLHelpers.ThrowOnResourceError(GL, "rolling back texture array");
|
||||
if (textureBytesTracked)
|
||||
GpuMemoryTracker.TrackDeallocation(textureBytes, GpuResourceType.Texture);
|
||||
if (textureTracked)
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
});
|
||||
if (cleanupFailures is not null) {
|
||||
cleanupFailures.Insert(0, constructionFailure);
|
||||
throw new AggregateException(
|
||||
"Texture-array construction and rollback both failed.",
|
||||
cleanupFailures);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(CalculateTotalSize(), GpuResourceType.Texture);
|
||||
|
||||
if (_device.HasBindless && _device.BindlessExtension != null) {
|
||||
BindlessHandle = _device.BindlessExtension.GetTextureHandle((uint)NativePtr);
|
||||
BindlessWrapHandle = _device.BindlessExtension.GetTextureSamplerHandle((uint)NativePtr, _device.WrapSampler);
|
||||
BindlessClampHandle = _device.BindlessExtension.GetTextureSamplerHandle((uint)NativePtr, _device.ClampSampler);
|
||||
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessWrapHandle);
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessClampHandle);
|
||||
finally {
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
}
|
||||
|
||||
_pboId = GL.GenBuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
public long CalculateTotalSize() {
|
||||
|
|
@ -151,7 +206,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
return totalSize;
|
||||
}
|
||||
|
||||
private bool IsCompressedFormat(TextureFormat format) {
|
||||
private static bool IsCompressedFormat(TextureFormat format) {
|
||||
return format == TextureFormat.DXT1 ||
|
||||
format == TextureFormat.DXT3 ||
|
||||
format == TextureFormat.DXT5;
|
||||
|
|
@ -220,127 +275,156 @@ namespace AcDream.App.Rendering.Wb {
|
|||
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
||||
}
|
||||
|
||||
int currentPboOffset = 0;
|
||||
ValidateUploadPayload(
|
||||
Format,
|
||||
Width,
|
||||
Height,
|
||||
data.Length,
|
||||
uploadPixelFormat,
|
||||
uploadPixelType);
|
||||
|
||||
lock (_mipmapLock) {
|
||||
if (_pendingUpdates.Count > 0) {
|
||||
var lastUpdate = _pendingUpdates[^1];
|
||||
currentPboOffset = lastUpdate.Offset + lastUpdate.Size;
|
||||
}
|
||||
|
||||
// Align to 4 bytes for safety
|
||||
currentPboOffset = (currentPboOffset + 3) & ~3;
|
||||
|
||||
if (currentPboOffset + data.Length > _pboSize) {
|
||||
// Flush existing updates first because BufferData will orphan/clear the PBO
|
||||
if (_pendingUpdates.Count > 0) {
|
||||
ProcessDirtyUpdatesInternal();
|
||||
}
|
||||
currentPboOffset = 0;
|
||||
|
||||
int newSize = Math.Max(_pboSize * 2, data.Length);
|
||||
newSize = Math.Max(newSize, GetExpectedDataSize() * 4); // Initial size 4 layers
|
||||
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
|
||||
GL.BufferData(GLEnum.PixelUnpackBuffer, (nuint)newSize, (void*)0, GLEnum.StreamDraw);
|
||||
|
||||
if (_pboSize > 0) {
|
||||
GpuMemoryTracker.TrackDeallocation(_pboSize, GpuResourceType.Buffer);
|
||||
}
|
||||
_pboSize = newSize;
|
||||
GpuMemoryTracker.TrackAllocation(_pboSize, GpuResourceType.Buffer);
|
||||
}
|
||||
else {
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
|
||||
}
|
||||
|
||||
fixed (byte* ptr = data) {
|
||||
GL.BufferSubData(GLEnum.PixelUnpackBuffer, (nint)currentPboOffset, (nuint)data.Length, ptr);
|
||||
}
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
||||
|
||||
_pendingUpdates.Add(new TextureLayerUpdate {
|
||||
// Retain the immutable decoded payload until the once-per-frame
|
||||
// atlas flush. The former per-atlas PBO permanently reserved
|
||||
// several MiB for every array and duplicated each upload
|
||||
// through BufferSubData before TexSubImage3D.
|
||||
var update = new TextureLayerUpdate {
|
||||
Layer = layer,
|
||||
Offset = currentPboOffset,
|
||||
Size = data.Length,
|
||||
Data = data,
|
||||
UploadPixelFormat = uploadPixelFormat,
|
||||
UploadPixelType = uploadPixelType
|
||||
});
|
||||
};
|
||||
int existingIndex = _pendingUpdates.FindLastIndex(pending => pending.Layer == layer);
|
||||
if (existingIndex >= 0)
|
||||
_pendingUpdates[existingIndex] = update;
|
||||
else
|
||||
_pendingUpdates.Add(update);
|
||||
|
||||
_needsMipmapRegeneration = true;
|
||||
_mipmapDirtyCount++;
|
||||
if (existingIndex < 0)
|
||||
_mipmapDirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessDirtyUpdates() {
|
||||
public long ProcessDirtyUpdates() {
|
||||
lock (_mipmapLock) {
|
||||
ProcessDirtyUpdatesInternal();
|
||||
return ProcessDirtyUpdatesInternal(generateMipmaps: true);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ProcessDirtyUpdatesInternal() {
|
||||
if (_pendingUpdates.Count == 0 && !_needsMipmapRegeneration) return;
|
||||
private unsafe long ProcessDirtyUpdatesInternal(bool generateMipmaps) {
|
||||
if (_pendingUpdates.Count == 0
|
||||
&& (!generateMipmaps || !_needsMipmapRegeneration)) return 0;
|
||||
|
||||
long generatedBytes = 0;
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
// This runs in WbMeshAdapter.Tick before any draw pass. Establish
|
||||
// the upload phase's canonical texture state directly instead of
|
||||
// synchronously querying driver state for every dirty array.
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
|
||||
GL.GetInteger(GLEnum.TextureBinding2DArray, out int oldBinding);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
bool mipmapWorkCompleted = false;
|
||||
try {
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
|
||||
bool wasResident = false;
|
||||
if (BindlessHandle != 0 && _device.BindlessExtension != null && _device.BindlessExtension.IsTextureHandleResident(BindlessHandle)) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
|
||||
wasResident = true;
|
||||
}
|
||||
if (_pendingUpdates.Count > 0) {
|
||||
// A non-zero pixel-unpack binding changes pointer arguments
|
||||
// into byte offsets. Direct client-memory uploads therefore
|
||||
// establish the canonical zero binding once for the batch.
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackRowLength, 0);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackSkipRows, 0);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackSkipPixels, 0);
|
||||
|
||||
if (_pendingUpdates.Count > 0) {
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
|
||||
foreach (var update in _pendingUpdates) {
|
||||
fixed (byte* data = update.Data) {
|
||||
if (_isCompressed) {
|
||||
var internalFormat = Format.ToCompressedGL();
|
||||
GL.CompressedTexSubImage3D(
|
||||
GLEnum.Texture2DArray,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
update.Layer,
|
||||
(uint)Width,
|
||||
(uint)Height,
|
||||
1,
|
||||
internalFormat,
|
||||
(uint)update.Data.Length,
|
||||
data);
|
||||
}
|
||||
else {
|
||||
var pixelFormat = update.UploadPixelFormat ?? Format.ToPixelFormat();
|
||||
var pixelType = update.UploadPixelType ?? Format.ToPixelType();
|
||||
GL.TexSubImage3D(
|
||||
GLEnum.Texture2DArray,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
update.Layer,
|
||||
(uint)Width,
|
||||
(uint)Height,
|
||||
1,
|
||||
pixelFormat,
|
||||
pixelType,
|
||||
data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var update in _pendingUpdates) {
|
||||
if (generateMipmaps && _needsMipmapRegeneration && _mipmapDirtyCount > 0) {
|
||||
if (_isCompressed) {
|
||||
var internalFormat = Format.ToCompressedGL();
|
||||
GL.CompressedTexSubImage3D(GLEnum.Texture2DArray, 0, 0, 0, update.Layer,
|
||||
(uint)Width, (uint)Height, 1, internalFormat, (uint)update.Size, (void*)update.Offset);
|
||||
_logger.LogDebug("Skipping automatic mipmap generation for compressed texture array (Slot={Slot})", Slot);
|
||||
}
|
||||
else {
|
||||
var pixelFormat = update.UploadPixelFormat ?? Format.ToPixelFormat();
|
||||
var pixelType = update.UploadPixelType ?? Format.ToPixelType();
|
||||
GL.TexSubImage3D(GLEnum.Texture2DArray, 0, 0, 0, update.Layer, (uint)Width, (uint)Height, 1,
|
||||
pixelFormat, pixelType, (void*)update.Offset);
|
||||
try {
|
||||
// Width, height and format were validated when the
|
||||
// immutable storage was allocated. Re-reading them
|
||||
// here forced three CPU/GPU synchronization points
|
||||
// for every dirty atlas without adding safety.
|
||||
GL.GenerateMipmap(GLEnum.Texture2DArray);
|
||||
generatedBytes = TotalSizeInBytes;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogWarning(ex, "Failed to generate mipmaps for texture array (Slot={Slot}); retaining upload state for retry.", Slot);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release builds must observe transfer/OOM/context errors
|
||||
// before the pending offsets and dirty mip state are cleared.
|
||||
// One check covers every layer in this array plus its single
|
||||
// mip generation, keeping the synchronization cost bounded by
|
||||
// dirty arrays rather than uploaded textures.
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
GL,
|
||||
$"committing texture-array updates (Slot={Slot}, Layers={_pendingUpdates.Count})");
|
||||
mipmapWorkCompleted = generateMipmaps
|
||||
&& _needsMipmapRegeneration
|
||||
&& _mipmapDirtyCount > 0;
|
||||
}
|
||||
finally {
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
||||
_pendingUpdates.Clear();
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
}
|
||||
|
||||
if (_needsMipmapRegeneration && _mipmapDirtyCount > 0) {
|
||||
if (_isCompressed) {
|
||||
_logger.LogDebug("Skipping automatic mipmap generation for compressed texture array (Slot={Slot})", Slot);
|
||||
}
|
||||
else if (!GLHelpers.ValidateTextureMipmapStatus(GL, GLEnum.Texture2DArray, out var errorMessage)) {
|
||||
_logger.LogWarning("Mipmap validation failed for texture array (Slot={Slot}): {Error}", Slot, errorMessage);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
GL.GenerateMipmap(GLEnum.Texture2DArray);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogWarning(ex, "Failed to generate mipmaps for texture array (Slot={Slot}).", Slot);
|
||||
}
|
||||
}
|
||||
// Commit CPU-side completion only after glGetError confirms the
|
||||
// uploads/mipmap work succeeded. If the driver rejects an
|
||||
// operation, the retained payloads and dirty flags remain intact and the
|
||||
// atlas stays in ObjectMeshManager's dirty set for a later retry.
|
||||
_pendingUpdates.Clear();
|
||||
if (mipmapWorkCompleted) {
|
||||
_mipmapDirtyCount = 0;
|
||||
_needsMipmapRegeneration = false;
|
||||
}
|
||||
|
||||
if (wasResident && BindlessHandle != 0 && _device.BindlessExtension != null) {
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
|
||||
}
|
||||
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)oldBinding);
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
return generatedBytes;
|
||||
}
|
||||
|
||||
private void ClearLayerForMipmap(int layer) {
|
||||
|
|
@ -351,17 +435,51 @@ namespace AcDream.App.Rendering.Wb {
|
|||
}
|
||||
|
||||
private int GetExpectedDataSize() {
|
||||
if (_isCompressed) {
|
||||
return TextureHelpers.GetCompressedLayerSize(Width, Height, Format);
|
||||
return CalculateExpectedDataSize(Format, Width, Height);
|
||||
}
|
||||
|
||||
internal static int CalculateExpectedDataSize(TextureFormat format, int width, int height) {
|
||||
if (IsCompressedFormat(format))
|
||||
return TextureHelpers.GetCompressedLayerSize(width, height, format);
|
||||
|
||||
return format switch {
|
||||
TextureFormat.RGBA8 => checked(width * height * 4),
|
||||
TextureFormat.RGB8 => checked(width * height * 3),
|
||||
TextureFormat.A8 => checked(width * height),
|
||||
TextureFormat.Rgba32f => checked(width * height * 16),
|
||||
_ => throw new NotSupportedException($"Unsupported format {format}")
|
||||
};
|
||||
}
|
||||
|
||||
internal static void ValidateUploadPayload(
|
||||
TextureFormat format,
|
||||
int width,
|
||||
int height,
|
||||
int dataLength,
|
||||
PixelFormat? uploadPixelFormat,
|
||||
PixelType? uploadPixelType) {
|
||||
int expectedBytes = CalculateExpectedDataSize(format, width, height);
|
||||
if (dataLength != expectedBytes) {
|
||||
throw new ArgumentException(
|
||||
$"Texture-array layer payload has {dataLength} bytes; expected exactly {expectedBytes} "
|
||||
+ $"for {format} {width}x{height}.",
|
||||
nameof(dataLength));
|
||||
}
|
||||
|
||||
return Format switch {
|
||||
TextureFormat.RGBA8 => Width * Height * 4,
|
||||
TextureFormat.RGB8 => Width * Height * 3,
|
||||
TextureFormat.A8 => Width * Height * 1,
|
||||
TextureFormat.Rgba32f => Width * Height * 16,
|
||||
_ => throw new NotSupportedException($"Unsupported format {Format}")
|
||||
};
|
||||
if (IsCompressedFormat(format)) {
|
||||
if (uploadPixelFormat.HasValue || uploadPixelType.HasValue)
|
||||
throw new ArgumentException("Compressed texture uploads cannot specify pixel format/type overrides.");
|
||||
return;
|
||||
}
|
||||
|
||||
PixelFormat expectedFormat = format.ToPixelFormat();
|
||||
PixelType expectedType = format.ToPixelType();
|
||||
if ((uploadPixelFormat ?? expectedFormat) != expectedFormat
|
||||
|| (uploadPixelType ?? expectedType) != expectedType) {
|
||||
throw new ArgumentException(
|
||||
$"Upload descriptor {uploadPixelFormat}/{uploadPixelType} does not match "
|
||||
+ $"the {expectedFormat}/{expectedType} transfer required by {format}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveLayer(int layer) {
|
||||
|
|
@ -376,15 +494,8 @@ namespace AcDream.App.Rendering.Wb {
|
|||
|
||||
_usedLayers[layer] = false;
|
||||
|
||||
// Make layer defined for mipmap completeness (uncompressed only)
|
||||
if (!_isCompressed) {
|
||||
ClearLayerForMipmap(layer);
|
||||
}
|
||||
|
||||
lock (_mipmapLock) {
|
||||
_mipmapDirtyCount++; // Mark dirty to regen
|
||||
_needsMipmapRegeneration = true;
|
||||
}
|
||||
// An unreferenced layer needs no clear or whole-array mip
|
||||
// regeneration before AddTexture overwrites it on reuse.
|
||||
}
|
||||
|
||||
public bool IsLayerUsed(int layer) {
|
||||
|
|
@ -396,6 +507,22 @@ namespace AcDream.App.Rendering.Wb {
|
|||
return _usedLayers.Count(x => x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True once disposal is durably owned by a queued GL publication,
|
||||
/// the frame-retirement queue, or a completed retained release. A
|
||||
/// caller may only commit its own logical disposal after this becomes
|
||||
/// true; otherwise a synchronous enqueue failure still needs retry.
|
||||
/// </summary>
|
||||
internal bool HasDurableDisposeOwnership {
|
||||
get {
|
||||
if (Volatile.Read(ref _disposeQueued) == 0)
|
||||
return false;
|
||||
return Volatile.Read(ref _disposePublicationQueued) != 0
|
||||
|| Volatile.Read(ref _disposeRetirementAccepted) != 0
|
||||
|| Volatile.Read(ref _disposeRelease) is null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
|
@ -409,37 +536,95 @@ namespace AcDream.App.Rendering.Wb {
|
|||
}
|
||||
|
||||
public void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (_device.BindlessExtension != null) {
|
||||
if (BindlessHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
|
||||
BindlessHandle = 0;
|
||||
if (Interlocked.CompareExchange(ref _disposeQueued, 1, 0) != 0) {
|
||||
ScheduleDisposeRelease();
|
||||
return;
|
||||
}
|
||||
|
||||
uint textureName = (uint)NativePtr;
|
||||
ulong bindlessWrapHandle = BindlessWrapHandle;
|
||||
ulong bindlessClampHandle = BindlessClampHandle;
|
||||
long textureBytes = CalculateTotalSize();
|
||||
|
||||
NativePtr = 0;
|
||||
BindlessWrapHandle = 0;
|
||||
BindlessClampHandle = 0;
|
||||
|
||||
_disposeRelease = new RetryableGpuResourceRelease(
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessWrapHandle != 0)
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing wrap texture-array handle (precondition)");
|
||||
},
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessWrapHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(bindlessWrapHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing wrap texture-array handle");
|
||||
}
|
||||
if (BindlessWrapHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessWrapHandle);
|
||||
BindlessWrapHandle = 0;
|
||||
},
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessClampHandle != 0)
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing clamp texture-array handle (precondition)");
|
||||
},
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessClampHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(bindlessClampHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing clamp texture-array handle");
|
||||
}
|
||||
if (BindlessClampHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessClampHandle);
|
||||
BindlessClampHandle = 0;
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0)
|
||||
GLHelpers.ThrowOnResourceError(GL, $"deleting texture array {textureName} (precondition)");
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0) {
|
||||
GL.DeleteTexture(textureName);
|
||||
GLHelpers.ThrowOnResourceError(GL, $"deleting texture array {textureName}");
|
||||
}
|
||||
}
|
||||
if (NativePtr != 0) {
|
||||
GL.DeleteTexture((uint)NativePtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(CalculateTotalSize(), GpuResourceType.Texture);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
NativePtr = 0;
|
||||
}
|
||||
if (_pboId != 0) {
|
||||
GL.DeleteBuffer(_pboId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
if (_pboSize > 0) {
|
||||
GpuMemoryTracker.TrackDeallocation(_pboSize, GpuResourceType.Buffer);
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(textureBytes, GpuResourceType.Texture);
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0)
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
},
|
||||
() => _disposeRelease = null);
|
||||
|
||||
ScheduleDisposeRelease();
|
||||
}
|
||||
|
||||
private void ScheduleDisposeRelease(bool forNextPass = false) {
|
||||
RetryableGpuResourceRelease? release = _disposeRelease;
|
||||
if (release is null || release.IsComplete || Volatile.Read(ref _disposeRetirementAccepted) != 0)
|
||||
return;
|
||||
if (Interlocked.CompareExchange(ref _disposePublicationQueued, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
try {
|
||||
Action<GL> publish = GL => {
|
||||
Volatile.Write(ref _disposePublicationQueued, 0);
|
||||
try {
|
||||
_device.RetireGpuResource(release.Run);
|
||||
Volatile.Write(ref _disposeRetirementAccepted, 1);
|
||||
}
|
||||
_pboId = 0;
|
||||
}
|
||||
});
|
||||
catch {
|
||||
// Retire may fail before accepting the callback, or an
|
||||
// immediate queue may surface a partial release. The
|
||||
// release cursor makes this next-pass retry exact.
|
||||
ScheduleDisposeRelease(forNextPass: true);
|
||||
throw;
|
||||
}
|
||||
};
|
||||
if (forNextPass)
|
||||
_device.QueueGLActionForNextPass(publish);
|
||||
else
|
||||
_device.QueueGLAction(publish);
|
||||
}
|
||||
catch {
|
||||
Volatile.Write(ref _disposePublicationQueued, 0);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue