This reverts ceec3bc4. Two independent reasons, either sufficient.
The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.
The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.
This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.
The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.
Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.
Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
639 lines
28 KiB
C#
639 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// True only after every retained bindless-handle, GL-name, and memory
|
|
/// accounting release stage has completed. Logical disposal can become
|
|
/// durable earlier while the frame fence still owns the physical array.
|
|
/// </summary>
|
|
internal bool IsPhysicalRetirementComplete =>
|
|
Volatile.Read(ref _disposeQueued) != 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;
|
|
}
|
|
}
|
|
}
|
|
}
|