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>
630 lines
28 KiB
C#
630 lines
28 KiB
C#
using AcDream.Core.Rendering.Wb;
|
|
using Chorizite.Core.Render;
|
|
using Chorizite.Core.Render.Enums;
|
|
// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly
|
|
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 {
|
|
private readonly bool[] _usedLayers;
|
|
private readonly GL GL;
|
|
private readonly OpenGLGraphicsDevice _device;
|
|
private readonly ILogger _logger;
|
|
private static int _nextId = 0;
|
|
private bool _needsMipmapRegeneration = false;
|
|
private readonly bool _isCompressed;
|
|
private int _mipmapDirtyCount = 0;
|
|
private readonly object _mipmapLock = new object();
|
|
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 required byte[] Data;
|
|
public PixelFormat? UploadPixelFormat;
|
|
public PixelType? UploadPixelType;
|
|
}
|
|
|
|
public int Slot { get; } = _nextId++;
|
|
public int Width { get; private set; }
|
|
public int Height { get; private set; }
|
|
public int Size { get; private set; }
|
|
public TextureFormat Format { get; private set; }
|
|
public nint NativePtr { 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 (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.
|
|
/// </summary>
|
|
public int PendingUpdateCount {
|
|
get { lock (_mipmapLock) { return _pendingUpdates.Count; } }
|
|
}
|
|
|
|
public ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
|
|
int size, ILogger logger, TextureParameters? texParams = null) {
|
|
var p = texParams ?? TextureParameters.Default;
|
|
if (width <= 0 || height <= 0 || size <= 0) {
|
|
throw new ArgumentException($"Invalid texture array dimensions: {width}x{height}x{size}");
|
|
}
|
|
|
|
Format = format;
|
|
Width = width;
|
|
Height = height;
|
|
Size = size;
|
|
_usedLayers = new bool[size];
|
|
_device = graphicsDevice;
|
|
GL = graphicsDevice.GL;
|
|
_logger = logger;
|
|
_isCompressed = IsCompressedFormat(format);
|
|
GLHelpers.CheckErrors(GL);
|
|
|
|
uint textureName = 0;
|
|
ulong wrapHandle = 0;
|
|
ulong clampHandle = 0;
|
|
bool textureTracked = false;
|
|
bool textureBytesTracked = false;
|
|
bool wrapResident = false;
|
|
bool clampResident = false;
|
|
long textureBytes = CalculateTotalSize();
|
|
|
|
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, textureName);
|
|
|
|
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);
|
|
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);
|
|
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
finally {
|
|
GL.ActiveTexture(TextureUnit.Texture0);
|
|
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
|
RenderStateCache.CurrentAtlas = 0;
|
|
}
|
|
}
|
|
|
|
public long CalculateTotalSize() {
|
|
int maxDimension = Math.Max(Width, Height);
|
|
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
|
long layerSize = GetExpectedDataSize();
|
|
long totalSize = 0;
|
|
|
|
for (int i = 0; i < mipLevels; i++) {
|
|
int w = Math.Max(1, Width >> i);
|
|
int h = Math.Max(1, Height >> i);
|
|
if (_isCompressed) {
|
|
totalSize += TextureHelpers.GetCompressedLayerSize(w, h, Format) * Size;
|
|
}
|
|
else {
|
|
totalSize += (long)w * h * (layerSize / (Width * Height)) * Size;
|
|
}
|
|
}
|
|
return totalSize;
|
|
}
|
|
|
|
private static bool IsCompressedFormat(TextureFormat format) {
|
|
return format == TextureFormat.DXT1 ||
|
|
format == TextureFormat.DXT3 ||
|
|
format == TextureFormat.DXT5;
|
|
}
|
|
|
|
public void Bind(int slot = 0) {
|
|
if (NativePtr == 0) {
|
|
return;
|
|
}
|
|
|
|
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
|
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
|
|
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
|
|
|
|
if (changedUnit) {
|
|
GL.ActiveTexture(targetTextureUnit);
|
|
}
|
|
|
|
GL.BindSampler((uint)slot, 0);
|
|
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
|
|
|
if (changedUnit) {
|
|
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
|
}
|
|
GLHelpers.CheckErrors(GL);
|
|
}
|
|
|
|
public unsafe int AddLayer(byte[] data) {
|
|
return AddLayer(data, null, null);
|
|
}
|
|
|
|
public unsafe int AddLayer(byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
|
for (int i = 0; i < _usedLayers.Length; i++) {
|
|
if (!_usedLayers[i]) {
|
|
UpdateLayerInternal(i, data, uploadPixelFormat, uploadPixelType);
|
|
_usedLayers[i] = true;
|
|
return i;
|
|
}
|
|
}
|
|
|
|
throw new InvalidOperationException(
|
|
$"No free layers available in texture array (Slot={Slot}, Size={Width}x{Height}x{Size}).");
|
|
}
|
|
|
|
public unsafe int AddLayer(Span<byte> data) {
|
|
return AddLayer(data.ToArray());
|
|
}
|
|
|
|
public void UpdateLayer(int layer, byte[] data) {
|
|
UpdateLayer(layer, data, null, null);
|
|
}
|
|
|
|
public void UpdateLayer(int layer, byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
|
UpdateLayerInternal(layer, data, uploadPixelFormat, uploadPixelType);
|
|
_usedLayers[layer] = true;
|
|
}
|
|
|
|
private unsafe void UpdateLayerInternal(int layer, byte[] data, PixelFormat? uploadPixelFormat,
|
|
PixelType? uploadPixelType) {
|
|
if (NativePtr == 0) {
|
|
throw new InvalidOperationException("Texture array not created.");
|
|
}
|
|
|
|
if (layer < 0 || layer >= Size) {
|
|
throw new ArgumentOutOfRangeException(nameof(layer),
|
|
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
|
}
|
|
|
|
ValidateUploadPayload(
|
|
Format,
|
|
Width,
|
|
Height,
|
|
data.Length,
|
|
uploadPixelFormat,
|
|
uploadPixelType);
|
|
|
|
lock (_mipmapLock) {
|
|
// 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,
|
|
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;
|
|
if (existingIndex < 0)
|
|
_mipmapDirtyCount++;
|
|
}
|
|
}
|
|
|
|
public long ProcessDirtyUpdates() {
|
|
lock (_mipmapLock) {
|
|
return ProcessDirtyUpdatesInternal(generateMipmaps: true);
|
|
}
|
|
}
|
|
|
|
private unsafe long ProcessDirtyUpdatesInternal(bool generateMipmaps) {
|
|
if (_pendingUpdates.Count == 0
|
|
&& (!generateMipmaps || !_needsMipmapRegeneration)) return 0;
|
|
|
|
long generatedBytes = 0;
|
|
|
|
GLHelpers.CheckErrors(GL);
|
|
|
|
// 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;
|
|
|
|
bool mipmapWorkCompleted = false;
|
|
try {
|
|
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
|
|
|
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);
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (generateMipmaps && _needsMipmapRegeneration && _mipmapDirtyCount > 0) {
|
|
if (_isCompressed) {
|
|
_logger.LogDebug("Skipping automatic mipmap generation for compressed texture array (Slot={Slot})", Slot);
|
|
}
|
|
else {
|
|
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);
|
|
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
|
GL.ActiveTexture(TextureUnit.Texture0);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
return generatedBytes;
|
|
}
|
|
|
|
private void ClearLayerForMipmap(int layer) {
|
|
// Upload a single black/transparent pixel to make layer defined
|
|
byte[] clearData = new byte[GetExpectedDataSize()];
|
|
Array.Clear(clearData, 0, clearData.Length); // Zero-fill (black/transparent)
|
|
UpdateLayerInternal(layer, clearData, null, null);
|
|
}
|
|
|
|
private int GetExpectedDataSize() {
|
|
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));
|
|
}
|
|
|
|
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) {
|
|
if (layer < 0 || layer >= Size) {
|
|
throw new ArgumentOutOfRangeException(nameof(layer),
|
|
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
|
}
|
|
|
|
if (!_usedLayers[layer]) {
|
|
throw new InvalidOperationException($"Layer {layer} is already free (Slot={Slot}).");
|
|
}
|
|
|
|
_usedLayers[layer] = false;
|
|
|
|
// An unreferenced layer needs no clear or whole-array mip
|
|
// regeneration before AddTexture overwrites it on reuse.
|
|
}
|
|
|
|
public bool IsLayerUsed(int layer) {
|
|
if (layer < 0 || layer >= Size) return false;
|
|
return _usedLayers[layer];
|
|
}
|
|
|
|
public int GetUsedLayerCount() {
|
|
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);
|
|
}
|
|
|
|
public void GenerateMipmaps() {
|
|
_needsMipmapRegeneration = true;
|
|
lock (_mipmapLock) {
|
|
_mipmapDirtyCount++;
|
|
}
|
|
}
|
|
|
|
public void Dispose() {
|
|
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 (_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 (textureName != 0)
|
|
GLHelpers.ThrowOnResourceError(GL, $"deleting texture array {textureName} (precondition)");
|
|
},
|
|
() => {
|
|
if (textureName != 0) {
|
|
GL.DeleteTexture(textureName);
|
|
GLHelpers.ThrowOnResourceError(GL, $"deleting texture array {textureName}");
|
|
}
|
|
},
|
|
() => {
|
|
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);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|