Commit 2 deleted the GL rendering backend's implementations; this step removes the package references and shader vocabulary they leave behind, so nothing in the App project still spells Silk.NET.OpenGL. Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are used directly and extensively across the Wb texture/mesh pipeline, independent of the deleted GL IUniformBuffer implementers the package comment used to cite. The stale comment is corrected in place. IMeshPipelineDevice.Gl is removed along with the GL? gl parameter threaded through WbMeshAdapter's four constructors, WorldRenderComposition's CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null implementation — nothing read any of them once the legacy per-mesh upload bodies were gone (confirmed by grep: the sole non-doc-comment hit was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed a real bug along the way: its teardown still pattern-matched the deleted GL GpuFrameFlightController to decide whether to wait for submitted work, which VulkanFrameFlightController replaced at slice V6a without this site being updated — so the wait had been silently dead on every Vulkan run since then. Retargeted to VulkanFrameFlightController, which carries the same WaitForSubmittedWork(). The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for upload validation is replaced by AcDream.Content's existing Silk.NET-free UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake tool GL-free); two new members (Rgb, Red, Float) extend that enum with their GL ABI constants to cover the full vocabulary WorldTextureArray needs, since MP1a's original set only covered what the extractor itself emits. ObjectMeshManager's App-boundary cast `(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct pass-through now that both sides share the type. GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of the Vulkan texture table) is deleted and StorageBindingCount drops from 10 to 9; the descriptor-set-layout code that builds from that count (VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just allocates one fewer always-dummy-seeded, always-unused binding. Several fully dead GL-only classes came along for the ride, confirmed by zero construction sites: SilkFramebufferViewportTarget (NullFramebufferViewportTarget is the sole production IFramebufferViewportTarget), SilkRenderGlStateReader (NullRenderGlStateReader.Instance is the sole IRenderGlStateReader), RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a pass load-op instead), and GpuFrameTimer plus FrameProfiler's GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame bracket (RecordGpuSample is the only GPU-timing path any backend uses now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no longer applies, since WbDrawDispatcher's own diagnostic GPU sampling already moved to the device's Vulkan timer pool). GpuFrameFlightController itself stays (never constructed with a real fence API in production, but its retirement-ledger/serial-ring logic is backend-neutral and still covered by its own unit tests) — only its GL-specific parts (the public GL constructor overload, SilkGpuFenceApi) are deleted, since removing the whole class would mean restructuring the frozen Slice-8 composition shape's GpuFrameFlightController? threading, which is out of this commit's scope. TextureParameters.cs and BufferUsageExtensions.cs (zero callers each) are deleted outright. common.glsl is deleted: nothing in the actual Vulkan .spv build reads it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own complete self-contained preamble per file; common.glsl's textual concatenation was exclusively Shader.cs's GL-only mechanism, deleted at Commit 2. The five shader files that named it in comments (mesh_modern.vert, particle.vert, particle.frag, sky.frag, terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the mandatory modern path already made unreachable, with zero C# consumers and no compiled .spv — are deleted too. Regenerated via tools/compile-shaders.ps1: 9/9 remaining shader pairs compile (previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests doc comment's "nine of ten are not Vulkan-expressible" was already stale before this commit). Test fallout: dead-subject test methods/files are deleted rather than patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs, GpuResourceRetirementTransactionTests.cs's GL queue tests, one WorldRenderDiagnosticsTests source-order test, one RenderFrameResourceControllerTests clear-phase-order test); tests whose subject moved or was renamed are updated in place rather than deleted (GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests' pinned seven-member surface now reads six, ParticleBindlessInstanceTests' cross-dialect check now covers the one surviving dialect, WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was always the parameter that actually threw). Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors, with the Silk.NET.OpenGL/.Extensions.ARB package references physically removed from the csproj (not just unreferenced in code). Tests: full-solution `dotnet test` green across every project. Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
249 lines
11 KiB
C#
249 lines
11 KiB
C#
using AcDream.Content;
|
|
using Chorizite.Core.Render;
|
|
using Chorizite.Core.Render.Enums;
|
|
using DatReaderWriter.Enums;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using AcDream.App.Rendering;
|
|
|
|
namespace AcDream.App.Rendering.Wb {
|
|
internal sealed class TextureAtlasDisposeTransaction {
|
|
private bool _running;
|
|
|
|
public bool IsComplete { get; private set; }
|
|
public bool IsRunning => _running;
|
|
|
|
public void Advance(
|
|
Action retryLayerRetirements,
|
|
Action disposeTextureArray,
|
|
Action commitLogicalDisposal) {
|
|
ArgumentNullException.ThrowIfNull(retryLayerRetirements);
|
|
ArgumentNullException.ThrowIfNull(disposeTextureArray);
|
|
ArgumentNullException.ThrowIfNull(commitLogicalDisposal);
|
|
if (IsComplete || _running)
|
|
return;
|
|
|
|
_running = true;
|
|
try {
|
|
retryLayerRetirements();
|
|
disposeTextureArray();
|
|
commitLogicalDisposal();
|
|
IsComplete = true;
|
|
}
|
|
finally {
|
|
_running = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manages texture arrays grouped by (Width, Height, Format).
|
|
/// Deduplicates textures by a TextureKey and supports reference counting.
|
|
/// </summary>
|
|
public class TextureAtlasManager : IDisposable {
|
|
private static uint _nextSlot = 1;
|
|
private readonly int _textureWidth;
|
|
private readonly int _textureHeight;
|
|
private readonly TextureFormat _format;
|
|
private readonly Dictionary<TextureKey, int> _textureIndices = new();
|
|
private readonly Dictionary<int, int> _refCounts = new();
|
|
private readonly TextureAtlasSlotAllocator _slots;
|
|
private readonly TextureAtlasLayerRetirement _layerRetirement;
|
|
private readonly TextureAtlasDisposeTransaction _disposeTransaction = new();
|
|
private readonly Action<TextureAtlasManager>? _onGpuSafeEmpty;
|
|
private bool _disposed;
|
|
internal const long TargetArrayBytes = 8L * 1024 * 1024;
|
|
internal const int MaximumArrayLayers = 32;
|
|
|
|
public uint Slot { get; }
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6i-2: the physical array, no longer typed to a
|
|
/// backend. Which implementation this is was decided once, at
|
|
/// composition, by the <see cref="IWorldTextureArrayFactory"/> handed to
|
|
/// the constructor — nothing in this class or in
|
|
/// <c>ObjectMeshManager</c>'s atlas policy branches on it.
|
|
/// </summary>
|
|
internal IWorldTextureArray TextureArray { get; private set; } = null!;
|
|
|
|
public int UsedSlots => _textureIndices.Count;
|
|
public int TotalSlots => TextureArray?.Size ?? 0;
|
|
public int AvailableSlots => _slots.AvailableCount;
|
|
internal bool IsGpuSafeEmpty => UsedSlots == 0 && AvailableSlots == TotalSlots;
|
|
internal long AllocatedBytes {
|
|
get {
|
|
return TextureArray.TotalSizeInBytes;
|
|
}
|
|
}
|
|
internal bool IsPhysicalRetirementComplete =>
|
|
TextureArray.IsPhysicalRetirementComplete;
|
|
internal long LastUseSequence { get; set; }
|
|
internal int Width => _textureWidth;
|
|
internal int Height => _textureHeight;
|
|
internal TextureFormat Format => _format;
|
|
|
|
internal TextureAtlasManager(
|
|
IWorldTextureArrayFactory arrays,
|
|
int width,
|
|
int height,
|
|
TextureFormat format = TextureFormat.RGBA8,
|
|
Action<TextureAtlasManager>? onGpuSafeEmpty = null) {
|
|
ArgumentNullException.ThrowIfNull(arrays);
|
|
Slot = _nextSlot++;
|
|
_textureWidth = width;
|
|
_textureHeight = height;
|
|
_format = format;
|
|
_onGpuSafeEmpty = onGpuSafeEmpty;
|
|
_layerRetirement = new TextureAtlasLayerRetirement(arrays.Retirement);
|
|
int capacity = CalculateInitialCapacity(width, height, format);
|
|
TextureArray = arrays.CreateClampedArray(format, width, height, capacity);
|
|
_slots = new TextureAtlasSlotAllocator(TextureArray.Size);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keeps each physical array near an eight-MiB target instead of
|
|
/// reserving 32 layers for every size class. The old fixed capacity
|
|
/// made a single 1024x1024 RGBA texture allocate roughly 171 MiB once
|
|
/// its mip chain was included, and made glGenerateMipmap process all
|
|
/// 32 layers during destination streaming.
|
|
/// </summary>
|
|
internal static int CalculateInitialCapacity(int width, int height, TextureFormat format) {
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(width, 1);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(height, 1);
|
|
|
|
long bytesPerLayer = CalculateMipChainBytes(width, height, format);
|
|
long targetLayers = Math.Max(1L, TargetArrayBytes / bytesPerLayer);
|
|
return checked((int)Math.Min(targetLayers, MaximumArrayLayers));
|
|
}
|
|
|
|
internal static long CalculateMipChainBytes(int width, int height, TextureFormat format) {
|
|
long total = 0;
|
|
int w = width;
|
|
int h = height;
|
|
while (true) {
|
|
total = checked(total + CalculateLevelBytes(w, h, format));
|
|
if (w == 1 && h == 1) return total;
|
|
w = Math.Max(1, w >> 1);
|
|
h = Math.Max(1, h >> 1);
|
|
}
|
|
}
|
|
|
|
internal static long CalculateArrayBytes(int width, int height, TextureFormat format) =>
|
|
checked(CalculateMipChainBytes(width, height, format)
|
|
* CalculateInitialCapacity(width, height, format));
|
|
|
|
internal static long CalculateLevelBytes(int width, int height, TextureFormat format) => format switch {
|
|
TextureFormat.RGBA8 => checked((long)width * height * 4L),
|
|
TextureFormat.RGB8 => checked((long)width * height * 3L),
|
|
TextureFormat.A8 => checked((long)width * height),
|
|
TextureFormat.Rgba32f => checked((long)width * height * 16L),
|
|
TextureFormat.DXT1 => checked((long)Math.Max(1, (width + 3) / 4) * Math.Max(1, (height + 3) / 4) * 8L),
|
|
TextureFormat.DXT3 or TextureFormat.DXT5 => checked((long)Math.Max(1, (width + 3) / 4) * Math.Max(1, (height + 3) / 4) * 16L),
|
|
_ => throw new NotSupportedException($"Unsupported texture-atlas format {format}.")
|
|
};
|
|
|
|
public int AddTexture(TextureKey key, byte[] data, UploadPixelFormat? uploadPixelFormat = null, UploadPixelType? uploadPixelType = null) {
|
|
ObjectDisposedException.ThrowIf(_disposed || _disposeTransaction.IsRunning, this);
|
|
_layerRetirement.RetryPendingPublications();
|
|
if (_textureIndices.TryGetValue(key, out var existingIndex)) {
|
|
_refCounts[existingIndex]++;
|
|
return existingIndex;
|
|
}
|
|
|
|
int index = _slots.Rent();
|
|
|
|
try {
|
|
TextureArray.UpdateLayer(index, data, uploadPixelFormat, uploadPixelType);
|
|
_textureIndices[key] = index;
|
|
_refCounts[index] = 1;
|
|
return index;
|
|
}
|
|
catch (Exception) {
|
|
if (!_textureIndices.ContainsKey(key))
|
|
_slots.Return(index);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public void ReleaseTexture(TextureKey key) {
|
|
ObjectDisposedException.ThrowIf(_disposed || _disposeTransaction.IsRunning, this);
|
|
_layerRetirement.RetryPendingPublications();
|
|
if (!_textureIndices.TryGetValue(key, out var index)) return;
|
|
|
|
if (!_refCounts.ContainsKey(index)) return;
|
|
|
|
_refCounts[index]--;
|
|
if (_refCounts[index] <= 0) {
|
|
_textureIndices.Remove(key);
|
|
_refCounts.Remove(index);
|
|
// The CPU no longer references this layer, but previously
|
|
// submitted draws may still sample it. Recycle the slot only
|
|
// after their GPU fence has signaled; no clear or whole-array
|
|
// mip regeneration is needed for an unreferenced layer.
|
|
_layerRetirement.Retire(
|
|
() => {
|
|
if (!_disposed)
|
|
_slots.Return(index);
|
|
},
|
|
() => {
|
|
if (!_disposed && IsGpuSafeEmpty)
|
|
_onGpuSafeEmpty?.Invoke(this);
|
|
});
|
|
}
|
|
}
|
|
|
|
internal void RetryPendingRetirements() =>
|
|
_layerRetirement.RetryPendingPublications();
|
|
|
|
public bool HasTexture(TextureKey key) => _textureIndices.ContainsKey(key);
|
|
|
|
public int GetTextureIndex(TextureKey key) =>
|
|
_textureIndices.TryGetValue(key, out var index) ? index : -1;
|
|
|
|
public void Dispose() {
|
|
if (_disposed) return;
|
|
_disposeTransaction.Advance(
|
|
_layerRetirement.RetryPendingPublications,
|
|
() => {
|
|
TextureArray?.Dispose();
|
|
if (TextureArray is not null && !TextureArray.HasDurableDisposeOwnership)
|
|
throw new InvalidOperationException(
|
|
"Texture-array disposal returned without retaining or publishing its physical release.");
|
|
},
|
|
() => {
|
|
_textureIndices.Clear();
|
|
_refCounts.Clear();
|
|
_disposed = true;
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owns the publication and callback cursor for returned atlas layers.
|
|
/// Removing the logical texture key commits immediately; this owner keeps
|
|
/// the physical layer reachable until the frame queue accepts it and keeps
|
|
/// the empty-atlas observer separate from the slot return so it cannot make
|
|
/// a callback retry return the same slot twice.
|
|
/// </summary>
|
|
internal sealed class TextureAtlasLayerRetirement
|
|
{
|
|
private readonly GpuRetirementLedger _ledger;
|
|
|
|
public TextureAtlasLayerRetirement(IGpuResourceRetirementQueue queue) =>
|
|
_ledger = new GpuRetirementLedger(queue);
|
|
|
|
internal int AwaitingPublicationCount => _ledger.AwaitingPublicationCount;
|
|
|
|
public void Retire(Action returnLayer, Action notifyGpuSafeEmpty)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(returnLayer);
|
|
ArgumentNullException.ThrowIfNull(notifyGpuSafeEmpty);
|
|
_ledger.Retire(new RetryableGpuResourceRelease(
|
|
returnLayer,
|
|
notifyGpuSafeEmpty));
|
|
}
|
|
|
|
public void RetryPendingPublications() =>
|
|
_ledger.RetryPendingPublications();
|
|
}
|
|
}
|