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:
parent
b70b9832ff
commit
8a7a0837e1
121 changed files with 1243 additions and 19840 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue