feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend

Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.

GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.

A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.

Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.

Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-29 02:19:53 +02:00
parent b70b9832ff
commit 8a7a0837e1
121 changed files with 1243 additions and 19840 deletions

View file

@ -1,13 +1,10 @@
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Gl;
using AcDream.Core.Textures;
using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using Silk.NET.OpenGL;
using DatPixelFormat = DatReaderWriter.Enums.PixelFormat;
using GLPixelFormat = Silk.NET.OpenGL.PixelFormat;
namespace AcDream.App.Rendering;
@ -29,12 +26,8 @@ namespace AcDream.App.Rendering;
/// The alpha atlas is built but not yet sampled by any shader — that wiring
/// lands in Phase 3c.4 along with the shader rewrite.
/// </summary>
public sealed unsafe class TerrainAtlas : IDisposable
public sealed class TerrainAtlas : IDisposable
{
private readonly GL? _gl;
// --- Terrain atlas (unchanged public API from Phase 2b) ---
public uint GlTexture { get; } // terrain atlas, kept as GlTexture for back-compat with TerrainRenderer
public IReadOnlyDictionary<uint, uint> TerrainTypeToLayer { get; }
public int LayerCount { get; }
/// <summary>
@ -45,7 +38,6 @@ public sealed unsafe class TerrainAtlas : IDisposable
public IReadOnlyList<float> TilingByLayer { get; }
// --- Alpha atlas (new in Phase 3c.2) ---
public uint GlAlphaTexture { get; }
public int AlphaLayerCount { get; }
/// <summary>Layer indices in the alpha atlas for CornerTerrainMaps (typically 4 entries).</summary>
public IReadOnlyList<byte> CornerAlphaLayers { get; }
@ -62,77 +54,12 @@ public sealed unsafe class TerrainAtlas : IDisposable
/// <summary>RCode for each RoadMap, parallel to <see cref="RoadAlphaLayers"/>.</summary>
public IReadOnlyList<uint> RoadAlphaRCodes { get; }
private readonly Wb.BindlessSupport? _bindless;
private readonly BindlessTexturePair? _bindlessHandles;
private readonly BindlessTextureMutationGuard? _bindlessMutation;
private readonly RestoredTextureBindingMutation _anisotropyBindingMutation = new();
private ResourceShutdownTransaction? _shutdown;
private ulong _registeredTerrainHandle;
private ulong _registeredAlphaHandle;
private GpuTextureSlot _terrainSlot = GpuTextureSlot.Unassigned;
private GpuTextureSlot _alphaSlot = GpuTextureSlot.Unassigned;
/// <summary>
/// Campaign V slice V4t: the device texture-table slots for the terrain and
/// alpha arrays — the backend-neutral replacement for the raw 64-bit
/// <c>ARB_bindless_texture</c> handles this used to return. Residency still
/// belongs to this atlas (acquired lazily here, released by
/// <see cref="Dispose"/>); the device owns only the two table entries.
///
/// <para>Throws <see cref="InvalidOperationException"/> if the atlas was
/// constructed without a <see cref="Wb.BindlessSupport"/> instance.</para>
///
/// <para><see cref="SetAnisotropic"/> makes both textures non-resident and
/// re-acquires them, which yields new handles. Re-registering is therefore
/// conditional on the handle actually having changed, and the superseded
/// table entry is retired in the same step — otherwise a quality-preset
/// change would leak a slot holding a non-resident handle. Registration is
/// idempotent, so the common per-draw call is two dictionary lookups.</para>
/// </summary>
internal (GpuTextureSlot Terrain, GpuTextureSlot Alpha) GetTextureSlots(GlGpuDevice device)
{
ArgumentNullException.ThrowIfNull(device);
if (_rhi is not null)
{
throw new InvalidOperationException(
"This TerrainAtlas owns IGpuTexture arrays; ask it for TextureSlots, not for GL handles.");
}
if (_bindless is null)
throw new InvalidOperationException(
"TerrainAtlas was constructed without BindlessSupport; cannot return texture slots.");
(ulong terrain, ulong alpha) = _bindlessHandles!.Acquire();
if (terrain != _registeredTerrainHandle)
{
device.ReleaseWorldTextureHandle(_registeredTerrainHandle);
_terrainSlot = device.RegisterWorldTextureHandle(terrain);
_registeredTerrainHandle = terrain;
}
if (alpha != _registeredAlphaHandle)
{
device.ReleaseWorldTextureHandle(_registeredAlphaHandle);
_alphaSlot = device.RegisterWorldTextureHandle(alpha);
_registeredAlphaHandle = alpha;
}
return (_terrainSlot, _alphaSlot);
}
/// <summary>
/// Campaign V slice V6i-2: the backend-neutral arm. When present, both
/// arrays are <see cref="IGpuTexture"/>s the device created and both slots
/// were registered at build time, so <see cref="TextureSlots"/> is a field
/// read rather than a residency negotiation. <see cref="GlTexture"/> and
/// <see cref="GlAlphaTexture"/> are 0 and no GL handle exists at all.
///
/// <para>V4t moved the TABLE ENTRY to the device but left CREATION with this
/// class, which is exactly the remainder plan §5.5.11 recorded: "Creating
/// world textures through IGpuTexture is real remaining work and it belongs
/// with the Vulkan world arm, which is the first thing that cannot use a GL
/// handle at all." This is that work, expressed as a second construction
/// path rather than a rewrite, so the GL path's calls are textually
/// unchanged.</para>
/// Campaign V slice V6i-2: both arrays are <see cref="IGpuTexture"/>s the
/// device created, and both slots were registered at build time, so
/// <see cref="TextureSlots"/> is a field read rather than a residency
/// negotiation. The GL construction path this used to sit alongside is
/// deleted as of Campaign V slice V11.
/// </summary>
private sealed class RhiArrays(
IGpuDevice device,
@ -149,30 +76,19 @@ public sealed unsafe class TerrainAtlas : IDisposable
public GpuTextureSlot AlphaSlot { get; set; } = GpuTextureSlot.Unassigned;
}
private readonly RhiArrays? _rhi;
private readonly RhiArrays _rhi;
/// <summary>
/// True when this atlas owns <see cref="IGpuTexture"/> arrays rather than raw
/// GL names. The two construction paths are mutually exclusive.
/// </summary>
internal bool IsBackendNeutral => _rhi is not null;
/// <summary>
/// The device-table slots for the terrain and alpha arrays on the
/// backend-neutral arm. Registered once at build time and re-registered only
/// by <see cref="SetAnisotropic"/>, which changes the sampler.
/// The device-table slots for the terrain and alpha arrays. Registered once
/// at build time and re-registered only by <see cref="SetAnisotropic"/>,
/// which changes the sampler.
/// </summary>
internal (GpuTextureSlot Terrain, GpuTextureSlot Alpha) TextureSlots =>
_rhi is null
? throw new InvalidOperationException(
"This TerrainAtlas owns GL names; ask it for GetTextureSlots(GlGpuDevice).")
: (_rhi.TerrainSlot, _rhi.AlphaSlot);
(_rhi.TerrainSlot, _rhi.AlphaSlot);
/// <summary>
/// Retail's terrain arrays are trilinear-filtered with the highest anisotropy
/// the quality preset allows; <see cref="SetAnisotropic"/> lowers it. The GL
/// path sets <c>GL_TEXTURE_MAX_ANISOTROPY</c> to 16 at build time, so the
/// backend-neutral path starts at the same value.
/// the quality preset allows; <see cref="SetAnisotropic"/> lowers it.
/// </summary>
private const float RetailMaxAnisotropy = 16f;
@ -192,11 +108,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
IReadOnlyList<uint> sideTCodes,
IReadOnlyList<uint> roadRCodes)
{
_gl = null;
_bindless = null;
_rhi = new RhiArrays(device, terrain, alpha, alphaSampler);
GlTexture = 0;
GlAlphaTexture = 0;
TerrainTypeToLayer = map;
LayerCount = layerCount;
TilingByLayer = tilingByLayer;
@ -211,72 +123,6 @@ public sealed unsafe class TerrainAtlas : IDisposable
ApplyAnisotropic(RetailMaxAnisotropy);
}
private TerrainAtlas(
GL gl,
Wb.BindlessSupport? bindless,
uint glTexture, IReadOnlyDictionary<uint, uint> map, int layerCount,
IReadOnlyList<float> tilingByLayer,
uint glAlphaTexture, int alphaLayerCount,
IReadOnlyList<byte> cornerLayers, IReadOnlyList<byte> sideLayers, IReadOnlyList<byte> roadLayers,
IReadOnlyList<uint> cornerTCodes, IReadOnlyList<uint> sideTCodes, IReadOnlyList<uint> roadRCodes)
{
_gl = gl;
_bindless = bindless;
GlTexture = glTexture;
TerrainTypeToLayer = map;
LayerCount = layerCount;
TilingByLayer = tilingByLayer;
GlAlphaTexture = glAlphaTexture;
AlphaLayerCount = alphaLayerCount;
CornerAlphaLayers = cornerLayers;
SideAlphaLayers = sideLayers;
RoadAlphaLayers = roadLayers;
CornerAlphaTCodes = cornerTCodes;
SideAlphaTCodes = sideTCodes;
RoadAlphaRCodes = roadRCodes;
if (bindless is not null)
{
_bindlessHandles = new BindlessTexturePair(
glTexture,
glAlphaTexture,
bindless.GetResidentHandle,
bindless.MakeNonResident);
_bindlessMutation = new BindlessTextureMutationGuard(_bindlessHandles);
}
}
/// <summary>
/// Build the atlas by walking Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc
/// for the mapping from TerrainTextureType to SurfaceTexture id, decoding each
/// to RGBA8, and uploading as layers in a single GL_TEXTURE_2D_ARRAY.
/// </summary>
public static TerrainAtlas Build(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
{
var textures = new GlTextureConstructionTransaction(new GlTextureNameApi(gl));
try
{
TerrainAtlas atlas = BuildCore(gl, dats, bindless, textures);
textures.Commit();
return atlas;
}
catch (Exception constructionFailure)
{
try
{
textures.Rollback();
}
catch (Exception cleanupFailure)
{
throw new GlResourceConstructionException(
"TerrainAtlas construction failed and its allocated OpenGL texture names did not cleanly roll back.",
textures,
[constructionFailure, cleanupFailure]);
}
throw;
}
}
/// <summary>
/// Campaign V slice V6i-2: the decode both construction paths share.
/// Splitting it out is what keeps the CPU logic single while the upload
@ -340,108 +186,6 @@ public sealed unsafe class TerrainAtlas : IDisposable
return new TerrainLayerDecode(decodedByType, tilingByType, maxW, maxH);
}
private static TerrainAtlas BuildCore(
GL gl,
IDatReaderWriter dats,
Wb.BindlessSupport? bindless,
GlTextureConstructionTransaction textures)
{
var region = dats.Get<Region>(0x13000000u)
?? throw new InvalidOperationException("Region dat id 0x13000000 missing");
var texMerge = region.TerrainInfo?.LandSurfaces?.TexMerge;
var terrainDesc = texMerge?.TerrainDesc;
if (terrainDesc is null || terrainDesc.Count == 0)
{
Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer");
return BuildFallback(gl, bindless, textures);
}
// ---- Terrain atlas (unchanged Phase 2b logic) ----
TerrainLayerDecode decode = DecodeTerrainLayers(dats, terrainDesc);
Dictionary<uint, DecodedTexture> decodedByType = decode.DecodedByType;
Dictionary<uint, uint> tilingByType = decode.TilingByType;
int maxW = decode.MaxWidth, maxH = decode.MaxHeight;
int layerCount = decodedByType.Count;
var map = new Dictionary<uint, uint>();
uint tex = TrackedTextureConstruction.Create(
textures,
gl,
"upload terrain atlas texture array",
texture =>
{
gl.BindTexture(TextureTarget.Texture2DArray, texture);
gl.TexImage3D(
TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8,
(uint)maxW, (uint)maxH, (uint)layerCount,
0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
int layerIdx = 0;
foreach (var kvp in decodedByType)
{
byte[] buffer = ResizeRgba8Nearest(kvp.Value, maxW, maxH);
fixed (byte* p = buffer)
{
gl.TexSubImage3D(
TextureTarget.Texture2DArray, 0,
0, 0, layerIdx,
(uint)maxW, (uint)maxH, 1,
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
}
map[kvp.Key] = (uint)layerIdx;
layerIdx++;
}
// A.5 T19: generate mipmaps + trilinear + 16x anisotropic for distant-LB quality.
gl.GenerateMipmap(TextureTarget.Texture2DArray);
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
// GL_TEXTURE_MAX_ANISOTROPY = 0x84FE (GL_EXT_texture_filter_anisotropic / ARB_texture_filter_anisotropic).
gl.TexParameter(TextureTarget.Texture2DArray, (TextureParameterName)0x84FE, 16.0f);
gl.BindTexture(TextureTarget.Texture2DArray, 0);
});
var tilingByLayer = TerrainTextureTilingTable.Build(
map.Select(entry =>
(entry.Value, tilingByType.TryGetValue(entry.Key, out uint repeatCount)
? repeatCount
: 1u)));
Console.WriteLine($"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH} (mipmaps+aniso16x)");
// ---- Alpha atlas (new in Phase 3c.2) ----
// texMerge is guaranteed non-null here: the early return above exited
// if texMerge?.TerrainDesc was null.
var alphaBuild = BuildAlphaAtlas(gl, dats, texMerge!, textures);
return new TerrainAtlas(
gl,
bindless,
tex, map, layerCount, tilingByLayer,
alphaBuild.gl, alphaBuild.layerCount,
alphaBuild.corner, alphaBuild.side, alphaBuild.road,
alphaBuild.cornerTCodes, alphaBuild.sideTCodes, alphaBuild.roadRCodes);
}
/// <summary>
/// Load corner, side, and road alpha maps from the TexMerge into a second
/// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures;
/// <see cref="SurfaceDecoder.DecodeRenderSurface"/> expands each alpha byte
/// into all four RGBA channels so the shader can sample from any channel.
///
/// Layers are appended in TexMerge insertion order: corners first, then
/// sides, then roads. The returned index lists tell
/// <c>TerrainBlending.BuildSurface</c> which layer to cite for each
/// corner/side/road alpha source.
/// </summary>
private readonly record struct AlphaAtlasBuildResult(
uint gl, int layerCount,
IReadOnlyList<byte> corner, IReadOnlyList<byte> side, IReadOnlyList<byte> road,
IReadOnlyList<uint> cornerTCodes, IReadOnlyList<uint> sideTCodes, IReadOnlyList<uint> roadRCodes);
/// <summary>
/// Slice V6i-2: the alpha-map decode, shared by both construction paths for
/// the same reason <see cref="DecodeTerrainLayers"/> is.
@ -530,100 +274,12 @@ public sealed unsafe class TerrainAtlas : IDisposable
decodedMaxH);
}
private static AlphaAtlasBuildResult BuildAlphaAtlas(
GL gl,
IDatReaderWriter dats,
DatReaderWriter.Types.TexMerge texMerge,
GlTextureConstructionTransaction textures)
{
AlphaLayerDecode decode = DecodeAlphaLayers(dats, texMerge);
List<DecodedTexture> decoded = decode.Decoded;
List<byte> cornerLayers = decode.CornerLayers;
List<byte> sideLayers = decode.SideLayers;
List<byte> roadLayers = decode.RoadLayers;
List<uint> cornerTCodes = decode.CornerTCodes;
List<uint> sideTCodes = decode.SideTCodes;
List<uint> roadRCodes = decode.RoadRCodes;
if (decoded.Count == 0)
{
Console.WriteLine("WARN: no alpha maps loaded; alpha atlas will be a 1x1 white fallback");
uint fallbackAlpha = TrackedTextureConstruction.Create(
textures,
gl,
"upload fallback terrain alpha texture array",
texture =>
{
gl.BindTexture(TextureTarget.Texture2DArray, texture);
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0,
GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
fixed (byte* p = white)
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1,
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
gl.BindTexture(TextureTarget.Texture2DArray, 0);
});
return new AlphaAtlasBuildResult(
fallbackAlpha, 1,
cornerLayers, sideLayers, roadLayers,
cornerTCodes, sideTCodes, roadRCodes);
}
int aMaxW = decode.MaxWidth, aMaxH = decode.MaxHeight;
uint glAlpha = TrackedTextureConstruction.Create(
textures,
gl,
"upload terrain alpha texture array",
texture =>
{
gl.BindTexture(TextureTarget.Texture2DArray, texture);
gl.TexImage3D(
TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8,
(uint)aMaxW, (uint)aMaxH, (uint)decoded.Count,
0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
for (int i = 0; i < decoded.Count; i++)
{
var buffer = ResizeRgba8Nearest(decoded[i], aMaxW, aMaxH);
fixed (byte* p = buffer)
{
gl.TexSubImage3D(
TextureTarget.Texture2DArray, 0,
0, 0, i,
(uint)aMaxW, (uint)aMaxH, 1,
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
}
}
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
gl.BindTexture(TextureTarget.Texture2DArray, 0);
});
Console.WriteLine(
$"AlphaAtlas: {decoded.Count} layers at {aMaxW}x{aMaxH} "
+ $"(corners={cornerLayers.Count}, sides={sideLayers.Count}, roads={roadLayers.Count})");
return new AlphaAtlasBuildResult(
glAlpha, decoded.Count,
cornerLayers, sideLayers, roadLayers,
cornerTCodes, sideTCodes, roadRCodes);
}
/// <summary>
/// Campaign V slice V6i-2: build both arrays through
/// <see cref="IGpuDevice"/>.
///
/// <para>Same DAT reads, same decode, same layer ordering and the same
/// resize-to-max policy as <see cref="Build"/> — only the upload differs,
/// which is the whole point of splitting the decode out. The terrain array
/// Builds both arrays through <see cref="IGpuDevice"/>. The terrain array
/// gets a full mip chain (<see cref="IGpuTexture.GenerateMipChain"/> blits
/// it, because RGBA8 is a legal blit destination) and a repeat/anisotropic
/// sampler; the alpha array is single-level and clamped, exactly as the GL
/// texture parameters say.</para>
/// sampler; the alpha array is single-level and clamped, matching what the
/// deleted GL path's texture parameters said.
/// </summary>
internal static TerrainAtlas BuildBackendNeutral(IGpuDevice device, IDatReaderWriter dats)
{
@ -826,151 +482,27 @@ public sealed unsafe class TerrainAtlas : IDisposable
return dst;
}
private static TerrainAtlas BuildFallback(
GL gl,
Wb.BindlessSupport? bindless,
GlTextureConstructionTransaction textures)
{
var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
uint tex = TrackedTextureConstruction.Create(
textures,
gl,
"upload fallback terrain texture array",
texture =>
{
gl.BindTexture(TextureTarget.Texture2DArray, texture);
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
fixed (byte* p = white)
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
gl.BindTexture(TextureTarget.Texture2DArray, 0);
});
// Fallback alpha atlas: 1x1 white, no layers tracked
uint alphaTex = TrackedTextureConstruction.Create(
textures,
gl,
"upload fallback alpha texture array",
texture =>
{
gl.BindTexture(TextureTarget.Texture2DArray, texture);
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
fixed (byte* p = white)
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
gl.BindTexture(TextureTarget.Texture2DArray, 0);
});
return new TerrainAtlas(
gl,
bindless,
tex, new Dictionary<uint, uint> { [0] = 0u }, 1,
TerrainTextureTilingTable.Build(Array.Empty<(uint Layer, uint RepeatCount)>()),
alphaTex, 1,
Array.Empty<byte>(), Array.Empty<byte>(), Array.Empty<byte>(),
Array.Empty<uint>(), Array.Empty<uint>(), Array.Empty<uint>());
}
/// <summary>
/// A.5 T22.5: update GL_TEXTURE_MAX_ANISOTROPY on the terrain atlas at
/// runtime (called by
/// Update terrain-array anisotropy at runtime (called by
/// <see cref="AcDream.App.Settings.RuntimeSettingsController.ReapplyQualityPreset"/> when
/// the user changes Quality preset mid-session). Idempotent — calling with
/// the same level as the current setting is safe and produces no visual
/// change. The texture must not be resident-bindless when its parameters
/// are mutated; we temporarily make it non-resident if needed.
/// change.
/// </summary>
public void SetAnisotropic(int level)
{
if (_rhi is not null)
{
ApplyAnisotropic(level);
Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x");
return;
}
GL gl = RequireGl();
void Mutate()
{
_anisotropyBindingMutation.Execute(
() => unchecked((uint)GlResourceCommand.Execute(
gl,
"read terrain-array binding before anisotropy mutation",
() => gl.GetInteger(GetPName.TextureBinding2DArray))),
binding => GlResourceCommand.Execute(
gl,
"set terrain-array binding for anisotropy mutation",
() => gl.BindTexture(TextureTarget.Texture2DArray, binding)),
GlTexture,
() => GlResourceCommand.Execute(
gl,
"set terrain atlas anisotropy",
() =>
{
// GL_TEXTURE_MAX_ANISOTROPY = 0x84FE
gl.TexParameter(
TextureTarget.Texture2DArray,
(TextureParameterName)0x84FE,
(float)level);
}));
}
if (_bindlessMutation is not null)
_bindlessMutation.Execute(Mutate);
else
Mutate();
ApplyAnisotropic(level);
Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x");
}
/// <summary>
/// Slice V6i-2: the GL arm's context. The two construction paths are
/// mutually exclusive, so a null here means a backend-neutral atlas reached
/// GL-only code — a programming error, not a runtime condition.
/// </summary>
private GL RequireGl() =>
_gl ?? throw new InvalidOperationException(
"This TerrainAtlas owns IGpuTexture arrays and has no GL context.");
public void Dispose()
{
if (_rhi is not null)
{
// Slice V4t's teardown rule holds on both arms: the device dies with
// its callers, so the table entries are not released here —
// deferring through a possibly-disposed retirement queue would turn
// a clean shutdown into a throw. The images themselves route through
// the device's retirement queue, which is what IGpuTexture.Dispose
// does.
_rhi.Alpha.Dispose();
_rhi.Terrain.Dispose();
return;
}
_shutdown ??= new ResourceShutdownTransaction(
new ResourceShutdownStage(
"terrain atlas bindless residency",
[
new ResourceShutdownOperation(
"release terrain and alpha handles",
() => _bindlessHandles?.Release()),
]),
new ResourceShutdownStage(
"terrain atlas textures",
[
new ResourceShutdownOperation(
"delete terrain texture",
() => GlResourceCommand.DeleteTexture(
RequireGl(),
GlTexture,
$"delete terrain atlas texture {GlTexture}")),
new ResourceShutdownOperation(
"delete alpha texture",
() => GlResourceCommand.DeleteTexture(
RequireGl(),
GlAlphaTexture,
$"delete terrain alpha texture {GlAlphaTexture}")),
]));
_shutdown.CompleteOrThrow();
// Slice V4t's teardown rule: the device dies with its callers, so the
// table entries are not released here — deferring through a
// possibly-disposed retirement queue would turn a clean shutdown into a
// throw. The images themselves route through the device's retirement
// queue, which is what IGpuTexture.Dispose does.
_rhi.Alpha.Dispose();
_rhi.Terrain.Dispose();
}
}