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,132 +0,0 @@
using Silk.NET.OpenGL;
using Silk.NET.OpenGL.Extensions.ARB;
using AcDream.App.Rendering;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Thin wrapper around <see cref="ArbBindlessTexture"/> + capability detection
/// for the modern rendering path. Constructed once at startup via
/// <see cref="TryCreate"/>, which returns false if the extension isn't present.
/// </summary>
public sealed class BindlessSupport
{
private readonly GL _gl;
private readonly ArbBindlessTexture _ext;
private BindlessSupport(GL gl, ArbBindlessTexture extension)
{
_gl = gl;
_ext = extension;
}
public static bool TryCreate(GL gl, out BindlessSupport? support)
{
if (gl.TryGetExtension<ArbBindlessTexture>(out var ext))
{
support = new BindlessSupport(gl, ext);
return true;
}
support = null;
return false;
}
/// <summary>Get a 64-bit bindless handle for the texture and make it resident.
/// Idempotent: handle is the same for a given texture name.</summary>
public ulong GetResidentHandle(uint textureName)
{
ulong h = GlResourceCommand.Execute(
_gl,
$"get bindless handle for texture {textureName}",
() => _ext.GetTextureHandle(textureName));
if (h == 0)
throw new InvalidOperationException(
$"OpenGL returned no bindless handle for texture {textureName}.");
bool resident = GlResourceCommand.Execute(
_gl,
$"query bindless handle {h} residency",
() => _ext.IsTextureHandleResident(h));
if (!resident)
{
GlResourceCommand.Execute(
_gl,
$"make bindless handle {h} resident",
() => _ext.MakeTextureHandleResident(h));
}
return h;
}
/// <summary>
/// Get a 64-bit bindless handle combining a texture with an EXPLICIT
/// sampler object (rather than the texture's own baked sampler state) and
/// make it resident. Idempotent per (texture, sampler) pair.
///
/// Added for Campaign V slice V1's <c>GlGpuDevice.RegisterTexture</c>,
/// which registers a (texture, sampler) pair per the RHI contract — "the
/// same texture registered with two samplers occupies two slots." The
/// texture-only <see cref="GetResidentHandle(uint)"/> above cannot express
/// that; <c>ManagedGLTextureArray</c> already calls the equivalent
/// <c>ArbBindlessTexture.GetTextureSamplerHandle</c> directly through
/// <c>OpenGLGraphicsDevice.BindlessExtension</c>, so this simply exposes
/// the same GL entry point through this class for the RHI's use.
/// </summary>
public ulong GetResidentHandle(uint textureName, uint samplerName)
{
ulong h = GlResourceCommand.Execute(
_gl,
$"get bindless handle for texture {textureName} + sampler {samplerName}",
() => _ext.GetTextureSamplerHandle(textureName, samplerName));
if (h == 0)
{
throw new InvalidOperationException(
$"OpenGL returned no bindless handle for texture {textureName} + sampler {samplerName}.");
}
bool resident = GlResourceCommand.Execute(
_gl,
$"query bindless handle {h} residency",
() => _ext.IsTextureHandleResident(h));
if (!resident)
{
GlResourceCommand.Execute(
_gl,
$"make bindless handle {h} resident",
() => _ext.MakeTextureHandleResident(h));
}
return h;
}
/// <summary>Release residency for a handle. Call before deleting the underlying texture.</summary>
public void MakeNonResident(ulong handle)
{
bool resident = GlResourceCommand.Execute(
_gl,
$"query bindless handle {handle} residency before release",
() => _ext.IsTextureHandleResident(handle));
if (!resident)
return;
GlResourceCommand.Execute(
_gl,
$"make bindless handle {handle} non-resident",
() => _ext.MakeTextureHandleNonResident(handle));
}
// Phase N.5b note: a `SetSamplerHandleUniform` wrapper was added in T6
// and removed when terrain rendering surfaced GL_INVALID_OPERATION on
// NVIDIA Windows for the `uniform sampler2DArray` + glProgramUniformHandleARB
// combination. The replacement pattern (uvec2 handle uniform + GLSL
// sampler-from-handle constructor — see terrain_modern.frag) lives at the
// call site via plain `_gl.ProgramUniform2(program, loc, low, high)`. If
// you re-introduce a sampler-handle helper, restrict it to drivers known
// to accept the direct sampler-uniform path.
/// <summary>Detect <c>GL_ARB_shader_draw_parameters</c> in addition to bindless.
/// N.5's vertex shader uses <c>gl_BaseInstanceARB</c> and <c>gl_DrawIDARB</c>
/// from this extension.</summary>
public bool HasShaderDrawParameters(GL gl)
{
return gl.IsExtensionPresent("GL_ARB_shader_draw_parameters");
}
}

View file

@ -25,20 +25,15 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using DatReaderWriter.Enums;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb;
public sealed unsafe partial class EnvCellRenderer :
public sealed partial class EnvCellRenderer :
IDisposable,
IEnvCellLandblockPublisher
{
private readonly object _publicationOwner = new();
// Campaign V slice V6j: null on the RHI arm. Every GL statement below is
// reached only when this is non-null; the encoder arm lives in
// EnvCellRenderer.Rhi.cs and the GL arm is unchanged.
private readonly GL? _gl;
private readonly ObjectMeshManager _meshManager;
private readonly WbFrustum _frustum;
@ -52,14 +47,6 @@ public sealed unsafe partial class EnvCellRenderer :
private readonly object _renderLock = new();
private EnvCellVisibilitySnapshot _activeSnapshot = new();
// Shader (set by caller via Initialize).
// Uses acdream's legacy Shader type (not WB's GLSLShader) to match the
// existing wire-in pattern in GameWindow.cs where _meshShader is loaded
// for mesh_modern.{vert,frag} and shared across multiple consumers.
// API mapping: Bind() -> Use(), SetUniform(s, int) -> SetInt(s, int),
// SetUniform(s, Vector4) -> SetVec4(s, Vector4).
private AcDream.App.Rendering.Shader? _shader;
// Phase U.4 root-cause fix: the view-projection captured in PrepareRenderBatches,
// re-uploaded by Render() so the cell-shell pass is self-contained and does NOT
// inherit WbDrawDispatcher's uViewProjection (which the opaque pass would read one
@ -84,35 +71,22 @@ public sealed unsafe partial class EnvCellRenderer :
// Modern-MDI scratch buffers (single slot — we re-upload every frame).
// WB BaseObjectRenderManager.cs:43-48: _scratchMdiCommandBuffers, _scratchModernBatchBuffers, _modernInstanceBuffers
// We collapse the ring-of-3 to a single slot since we have no persistent/consolidated draws.
private uint _mdiCommandBuffer;
private int _mdiCommandCapacity;
private uint _modernInstanceBuffer;
private int _modernInstanceCapacity;
private uint _modernBatchBuffer;
private int _modernBatchCapacity;
// mesh_modern.vert's SSBO InstanceData is only mat4 transform. The CPU
// InstanceData below also carries CellId/Flags for filtering, so upload a
// packed transform array instead of the 80-byte CPU struct.
private Matrix4x4[] _gpuInstanceTransforms = Array.Empty<Matrix4x4>();
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
// _modernInstanceBuffer. One uint per instance selecting its CellClip slot,
// indexed by the same BaseInstance + gl_InstanceID the shader uses for
// binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots.
private uint _clipSlotBuffer;
private int _clipSlotCapacity;
// Phase U.3: per-instance clip-slot data, parallel to _gpuInstanceTransforms.
// One uint per instance selecting its CellClip slot, indexed by the same
// BaseInstance + gl_InstanceID the shader uses for binding=0. ALL ZEROS ⇒
// slot 0 ⇒ no-clip.
private uint[] _clipSlotData = Array.Empty<uint>();
// A7 Fix D (D-2): this renderer owns its lighting (self-contained GL state,
// like uViewProjection) instead of reading the SSBO 4/5 WbDrawDispatcher last
// left bound. binding=4 = global point-light snapshot (same data/indices as the
// dispatcher, via GlobalLightPacker); binding=5 = 8 int indices per instance.
private uint _globalLightsSsbo; // binding=4
private int _globalLightsCapacity;
// A7 Fix D (D-2): this renderer owns its lighting (self-contained state,
// like uViewProjection) instead of reading whatever WbDrawDispatcher last
// bound. Global point-light snapshot (same data/indices as the dispatcher,
// via GlobalLightPacker) plus 8 int indices per instance.
private float[] _globalLightData = new float[AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight * 16];
private uint _instLightSetSsbo; // binding=5
private int _instLightSetCapacity;
private int[] _lightSetData = new int[1024 * AcDream.Core.Lighting.LightManager.MaxLightsPerObject];
private System.Collections.Generic.IReadOnlyList<AcDream.Core.Lighting.LightSource>? _pointSnapshot;
private sealed class CachedCellLightSet
@ -125,51 +99,17 @@ public sealed unsafe partial class EnvCellRenderer :
private readonly List<uint> _cellLightRemovalScratch = new();
private int _lightFrameGeneration;
private sealed class DynamicBufferSet
{
public uint MdiCommandBuffer;
public uint ModernInstanceBuffer;
public uint ModernBatchBuffer;
public uint ClipSlotBuffer;
public uint GlobalLightsSsbo;
public uint InstanceLightSetSsbo;
public int MdiCommandCapacity;
public int ModernInstanceCapacity;
public int ModernBatchCapacity;
public int ClipSlotCapacity;
public int GlobalLightsCapacity;
public int InstanceLightSetCapacity;
}
private readonly List<DynamicBufferSet>[] _dynamicBufferSetsByFrame =
[[], [], []];
// Per-GPU-fenced-frame-slot draw bookkeeping.
private int _dynamicFrameSlot;
private int _dynamicBufferSetCursor;
private bool _dynamicFrameStarted;
private DynamicBufferSet? _activeDynamicBufferSet;
internal int DynamicBufferSetCount =>
_dynamicBufferSetsByFrame.Sum(frameSets => frameSets.Count);
// Phase U.3: SHARED per-cell clip-region SSBO (binding=2) handed in via
// SetClipRegionSsbo (the GameWindow-level ClipFrame buffer). When 0, we bind
// our own one-slot no-clip fallback so the shader never reads an unbound SSBO.
private uint _sharedClipRegionSsbo;
private uint _fallbackClipRegionSsbo;
// Campaign V slice V4t (2026-07-28): the interim per-renderer
// GlBindlessHandleTable is retired. ObjectRenderBatch already carries the
// device's own GpuTextureSlot, so this renderer shares WbDrawDispatcher's
// table — the device's — and only has to flush and bind it before its own
// raw-GL draws. That also removes the V2 caveat that two renderers could
// legitimately number the same texture differently: there is now one
// numbering, and it is the one the mesh manager assigned at upload.
private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
(_meshManager
?? throw new InvalidOperationException(
"EnvCellRenderer was constructed without a mesh manager: its texture " +
"slots come from that manager's GL device table (Campaign V slice V4t)."))
.WorldTextureTable;
/// <summary>
/// The dynamic per-frame-slot SSBO pool this used to report was raw-GL-only
/// bookkeeping, deleted with that arm at Campaign V slice V11. The RHI arm
/// allocates its storage from the GPU frame's own upload ring instead, so
/// there is no separate pool to count.
/// </summary>
internal int DynamicBufferSetCount => 0;
// Reusable scratch arrays — avoid per-frame allocation.
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
@ -197,11 +137,6 @@ public sealed unsafe partial class EnvCellRenderer :
private readonly Dictionary<ulong, List<InstanceData>> _activeSnapshotGlobalGroups = new();
private readonly List<ulong> _activeSnapshotGlobalGfxObjIds = new();
// Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28.
// Shared across all manager instances on the same GL context.
private static uint _currentVao;
private static CullMode? _currentCullMode;
public bool NeedsPrepare { get; private set; } = true;
// --- Prepare gate (2026-07-24) -------------------------------------------
@ -302,31 +237,19 @@ public sealed unsafe partial class EnvCellRenderer :
}
// ---------------------------------------------------------------------------
// Constructor + Initialize
// Constructor
// Campaign V slice V11: the raw-GL constructor + Initialize(Shader) two-step
// are deleted. EnvCellRenderer.Rhi.cs's constructor is now the class's sole
// constructor — it builds the three shell pipelines itself, so there is no
// second initialization step.
// ---------------------------------------------------------------------------
public EnvCellRenderer(GL gl, ObjectMeshManager meshManager, WbFrustum frustum)
{
_gl = gl;
_meshManager = meshManager;
_frustum = frustum;
}
public void Initialize(AcDream.App.Rendering.Shader shader)
{
_shader = shader;
_initialized = true;
}
/// <summary>Resets the per-frame submission cursor for the GPU-fenced slot.</summary>
public void BeginFrame(int frameSlot)
{
if ((uint)frameSlot >= (uint)_dynamicBufferSetsByFrame.Length)
throw new ArgumentOutOfRangeException(nameof(frameSlot));
ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
_dynamicFrameSlot = frameSlot;
_dynamicBufferSetCursor = 0;
_dynamicFrameStarted = true;
_activeDynamicBufferSet = null;
if (++_lightFrameGeneration == 0)
{
_cellLightSetCache.Clear();
@ -334,15 +257,6 @@ public sealed unsafe partial class EnvCellRenderer :
}
}
/// <summary>
/// Phase U.3: hand the renderer the SHARED per-cell clip-region SSBO
/// (binding=2) created by <see cref="ClipFrame.UploadShared"/>. The renderer
/// re-binds it to binding=2 immediately before its MDI. Pass 0 to fall back to
/// the internal one-slot no-clip region buffer.
/// </summary>
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
// Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
// non-null, RenderModernMDIInternal writes instanceClipSlot[i] =
// _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are
@ -975,18 +889,14 @@ public sealed unsafe partial class EnvCellRenderer :
HashSet<uint>? filter,
IReadOnlyList<uint>? orderedCellIds)
{
// WB EnvCellRenderManager.cs:400:
// WB EnvCellRenderManager.cs:400: the RHI arm's three pipelines are built
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
// answers whether this renderer is ready to draw.
if (!_initialized) return;
// Campaign V slice V6j: the RHI arm has no linked program to check — the
// pipeline it draws with was built at construction and the same readiness
// question is answered by _initialized alone.
if (_gl is not null && (_shader is null || _shader.Program == 0)) return;
lock (_renderLock)
{
var snapshot = _activeSnapshot;
// WB EnvCellRenderManager.cs:403-404:
_shader?.Use();
// FIX 2026-05-28 (pool aliasing root cause): mirror WB
// EnvCellRenderManager.cs:405 — restore the pool cursor to the
// high-water mark Prepare's merge phase reached, so any
@ -997,43 +907,6 @@ public sealed unsafe partial class EnvCellRenderer :
// mid-Render. See docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
_poolIndex = snapshot.PostPreparePoolIndex;
// FIX 2026-05-28: invalidate static GL-state caches at start of Render.
// Mirrors WB EnvCellRenderManager.cs:404-410:
// CurrentVAO = 0; CurrentIBO = 0; CurrentAtlas = 0;
// CurrentInstanceBuffer = 0; CurrentCullMode = null;
//
// These caches let SetCullMode / BindVertexArray skip redundant GL
// calls when the state is already correct. BUT: between two Render()
// invocations, OTHER consumers (WbDrawDispatcher, terrain, the
// RenderInsideOutAcdream stencil pipeline) change the actual GL
// state without updating these caches. The cache then lies, and
// the per-batch SetCullMode in RenderModernMDIInternal skips its
// glCullFace call — leaving stale cull state from the prior
// consumer. For a cottage with mixed CullMode batches, half the
// walls end up culled and the user sees "missing walls".
//
// Forcing the cache to null/0 at entry guarantees each Render call
// re-establishes the GL state it expects.
_currentVao = 0;
_currentCullMode = null;
// WB EnvCellRenderManager.cs:406-409: uniform state setup.
_shader?.SetInt("uRenderPass", (int)renderPass);
_shader?.SetInt("uFilterByCell", 0);
_shader?.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun)
// #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
_shader?.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
// Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when
// moving"): upload uViewProjection HERE rather than inheriting it from
// WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's
// Draw (GameWindow ~7411 vs ~7418, the only other setter), so without
// this the opaque shells used the PREVIOUS frame's matrix — a stale
// gl_Position against this frame's clip planes → pose-dependent clipping,
// worst while moving. Same self-contained-GL-state precedent as the
// 2026-05-28 cull-state cache fix above.
_shader?.SetMatrix4("uViewProjection", _lastViewProjection);
List<InstanceData> allInstances = _renderInstances;
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls =
_renderDrawCalls;
@ -1146,7 +1019,6 @@ public sealed unsafe partial class EnvCellRenderer :
if (_drawCallRanges.Count == 0 && drawCalls.Count > 0)
_drawCallRanges.Add(new DrawCallRange(0, drawCalls.Count));
RenderModernMDIInternal(
_shader,
drawCalls,
allInstances,
_drawCallRanges,
@ -1155,20 +1027,6 @@ public sealed unsafe partial class EnvCellRenderer :
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
// WB EnvCellRenderManager.cs:506-509: cleanup.
_shader?.SetVec4("uHighlightColor", new System.Numerics.Vector4(0, 0, 0, 0));
_shader?.SetInt("uRenderPass", (int)renderPass);
_gl?.BindVertexArray(0);
_currentVao = 0;
// No cull restore at exit, matching WB's manager pattern: the
// last SetCullMode call reflects actual GL state, and the next
// Render call invalidates `_currentCullMode` before issuing its
// own per-batch state. The Landblock->None override below can
// intentionally leave cull disabled for the following IndoorPass,
// preserving the shipped Gate #5 baseline while deeper evidence is
// gathered.
// Update frame stats for probe emission at the call site.
_lastFrameStats.CellsRendered = orderedCellIds?.Count
?? filter?.Count
@ -1285,58 +1143,6 @@ public sealed unsafe partial class EnvCellRenderer :
// issues glMultiDrawElementsIndirect.
// ---------------------------------------------------------------------------
private void ActivateNextDynamicBufferSet()
{
if (!_dynamicFrameStarted)
throw new InvalidOperationException("BeginFrame must be called before drawing EnvCells.");
List<DynamicBufferSet> slotSets = _dynamicBufferSetsByFrame[_dynamicFrameSlot];
if (_dynamicBufferSetCursor == slotSets.Count)
slotSets.Add(CreateDynamicBufferSet());
DynamicBufferSet set = slotSets[_dynamicBufferSetCursor++];
_activeDynamicBufferSet = set;
_mdiCommandBuffer = set.MdiCommandBuffer;
_modernInstanceBuffer = set.ModernInstanceBuffer;
_modernBatchBuffer = set.ModernBatchBuffer;
_clipSlotBuffer = set.ClipSlotBuffer;
_globalLightsSsbo = set.GlobalLightsSsbo;
_instLightSetSsbo = set.InstanceLightSetSsbo;
_mdiCommandCapacity = set.MdiCommandCapacity;
_modernInstanceCapacity = set.ModernInstanceCapacity;
_modernBatchCapacity = set.ModernBatchCapacity;
_clipSlotCapacity = set.ClipSlotCapacity;
_globalLightsCapacity = set.GlobalLightsCapacity;
_instLightSetCapacity = set.InstanceLightSetCapacity;
}
private DynamicBufferSet CreateDynamicBufferSet()
{
var set = new DynamicBufferSet();
try
{
set.MdiCommandBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell MDI buffer");
set.ModernInstanceBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell instance SSBO");
set.ModernBatchBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell batch SSBO");
set.ClipSlotBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell clip-slot SSBO");
set.GlobalLightsSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell global-light SSBO");
set.InstanceLightSetSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell light-set SSBO");
return set;
}
catch (Exception creationFailure)
{
try { DeleteDynamicBufferSet(set); }
catch (Exception cleanupFailure)
{
throw new AggregateException(
"EnvCell dynamic-buffer creation and rollback failed.",
creationFailure,
cleanupFailure);
}
throw;
}
}
private void RebuildUnfilteredGroups(EnvCellVisibilitySnapshot snapshot)
{
foreach (List<InstanceData> instances in _activeSnapshotGlobalGroups.Values)
@ -1359,59 +1165,7 @@ public sealed unsafe partial class EnvCellRenderer :
}
}
private void DeleteDynamicBufferSet(DynamicBufferSet set)
{
List<Exception>? failures = null;
void Attempt(uint buffer, long bytes, string name)
{
try { TrackedGlResource.DeleteBuffer(_gl!, buffer, bytes, $"deleting {name}"); }
catch (Exception ex) { (failures ??= []).Add(ex); }
}
Attempt(
set.MdiCommandBuffer,
(long)set.MdiCommandCapacity * sizeof(DrawElementsIndirectCommand),
"EnvCell MDI buffer");
Attempt(
set.ModernInstanceBuffer,
(long)set.ModernInstanceCapacity * sizeof(Matrix4x4),
"EnvCell instance SSBO");
Attempt(
set.ModernBatchBuffer,
(long)set.ModernBatchCapacity * sizeof(ModernBatchData),
"EnvCell batch SSBO");
Attempt(set.ClipSlotBuffer, (long)set.ClipSlotCapacity * sizeof(uint), "EnvCell clip-slot SSBO");
Attempt(
set.GlobalLightsSsbo,
(long)set.GlobalLightsCapacity
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
* sizeof(float),
"EnvCell global-light SSBO");
Attempt(
set.InstanceLightSetSsbo,
(long)set.InstanceLightSetCapacity
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
* sizeof(int),
"EnvCell light-set SSBO");
if (failures is not null)
throw new AggregateException("One or more EnvCell dynamic buffers failed to delete.", failures);
}
private void PersistActiveDynamicBufferCapacities()
{
DynamicBufferSet set = _activeDynamicBufferSet
?? throw new InvalidOperationException("No dynamic EnvCell buffer set is active.");
set.MdiCommandCapacity = _mdiCommandCapacity;
set.ModernInstanceCapacity = _modernInstanceCapacity;
set.ModernBatchCapacity = _modernBatchCapacity;
set.ClipSlotCapacity = _clipSlotCapacity;
set.GlobalLightsCapacity = _globalLightsCapacity;
set.InstanceLightSetCapacity = _instLightSetCapacity;
}
private void RenderModernMDIInternal(
AcDream.App.Rendering.Shader? shader,
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls,
List<InstanceData> allInstances,
IReadOnlyList<DrawCallRange> drawCallRanges,
@ -1423,26 +1177,11 @@ public sealed unsafe partial class EnvCellRenderer :
int passIdx = (int)renderPass;
if (passIdx < 0 || passIdx > 2) return;
// §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads.
// Without the global VAO nothing can draw, and returning AFTER the pass state
// was established leaked it (same early-out shape as the totalDraws==0 leak —
// see the comment on the state-establish block below).
// Campaign V slice V6j: the RHI arm has no vertex array — the pipeline
// owns one shaped by GpuVertexLayout.WorldMesh — so its readiness test is
// the backend-neutral HasStores the arena publishes (V6i-3).
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
if (_gl is not null)
{
if (globalVao == 0) return;
}
else if (_meshManager.GlobalBuffer is not { HasStores: true })
{
// the backend-neutral HasStores flag the mesh arena publishes (V6i-3).
if (_meshManager.GlobalBuffer is not { HasStores: true })
return;
}
// WB BaseObjectRenderManager.cs:715-716:
shader?.Use();
shader?.SetInt("uFilterByCell", 0);
// WB BaseObjectRenderManager.cs:718-740: count the pass-filtered batches.
// A normal render has one range. The ordered transparent-shell path has
@ -1479,120 +1218,16 @@ public sealed unsafe partial class EnvCellRenderer :
// WB BaseObjectRenderManager.cs:743:
if (totalDraws == 0) return;
int uniqueInstanceCount = allInstances.Count;
// Campaign V slice V6j: the encoder arm owns no buffer pool and no
// imperative state bracket. Every per-frame section is a ring slice, so
// there is nothing to activate or grow, and blend plus depth-write are
// baked into the three shell pipelines rather than set here.
if (_gl is not null)
{
ActivateNextDynamicBufferSet();
// Phase U.4 ROOT-CAUSE FIX (cell-shell "transparent walls / only bluish
// background, flickering when moving"): establish this pass's BLEND + DepthMask
// state OURSELVES rather than inheriting it. Mirror the working WbDrawDispatcher
// passes (Disable(Blend)+DepthMask(true) opaque; Enable(Blend)+DepthMask(false)
// transparent). Restored to opaque defaults at the end of the draw loop so a
// Transparent pass can't leak into later draws.
//
// §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
// totalDraws==0 early-out above. It used to run before the batch grouping, so a
// Transparent pass over a cell whose batches are ALL opaque (a plain cottage
// interior) set Blend-on/DepthMask-off and then returned at the count check
// WITHOUT reaching the restore. The frame ended with dmask=0; the NEXT frame's
// glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world
// fragment failed GL_LESS against its own previous-frame depth ghost, and the
// whole screen dropped to the fog-tinted clear color — onset-locked to the
// building-flood merge (the first frame a flooded building shell draws), holding
// until camera rotation dropped the cell from the flood. From here down every
// path reaches the end-of-pass restore.
if (renderPass == WbRenderPass.Transparent)
{
_gl.Enable(EnableCap.Blend);
_gl.DepthMask(false);
}
else
{
_gl.Disable(EnableCap.Blend);
_gl.DepthMask(true);
}
// WB BaseObjectRenderManager.cs:745-759: resize buffers if needed.
if (totalDraws > _mdiCommandCapacity)
{
int grownMdiCapacity = Math.Max(_mdiCommandCapacity * 2, totalDraws);
TrackedGlResource.AllocateBufferStorage(
_gl,
GLEnum.DrawIndirectBuffer,
_mdiCommandBuffer,
(long)_mdiCommandCapacity * sizeof(DrawElementsIndirectCommand),
(long)grownMdiCapacity * sizeof(DrawElementsIndirectCommand),
GLEnum.DynamicDraw,
$"growing EnvCell MDI buffer to {grownMdiCapacity} commands");
_mdiCommandCapacity = grownMdiCapacity;
int grownBatchCapacity = grownMdiCapacity;
TrackedGlResource.AllocateBufferStorage(
_gl,
GLEnum.ShaderStorageBuffer,
_modernBatchBuffer,
(long)_modernBatchCapacity * sizeof(ModernBatchData),
(long)grownBatchCapacity * sizeof(ModernBatchData),
GLEnum.DynamicDraw,
$"growing EnvCell batch SSBO to {grownBatchCapacity} batches");
_modernBatchCapacity = grownBatchCapacity;
}
if (uniqueInstanceCount > _modernInstanceCapacity)
{
int grownInstanceCapacity = Math.Max(_modernInstanceCapacity * 2, uniqueInstanceCount);
TrackedGlResource.AllocateBufferStorage(
_gl,
GLEnum.ShaderStorageBuffer,
_modernInstanceBuffer,
(long)_modernInstanceCapacity * sizeof(Matrix4x4),
(long)grownInstanceCapacity * sizeof(Matrix4x4),
GLEnum.DynamicDraw,
$"growing EnvCell instance SSBO to {grownInstanceCapacity} instances");
_modernInstanceCapacity = grownInstanceCapacity;
}
// Phase U.3: keep the clip-slot buffer (binding=3) sized to the
// instance prefix so instanceClipSlot[BaseInstance + gl_InstanceID]
// is always in range. It owns an independent committed capacity so a
// failed allocation can never publish the instance buffer's growth as
// if both resources had succeeded.
if (uniqueInstanceCount > _clipSlotCapacity)
{
int grownClipCapacity = Math.Max(_clipSlotCapacity * 2, uniqueInstanceCount);
TrackedGlResource.AllocateBufferStorage(
_gl,
GLEnum.ShaderStorageBuffer,
_clipSlotBuffer,
(long)_clipSlotCapacity * sizeof(uint),
(long)grownClipCapacity * sizeof(uint),
GLEnum.DynamicDraw,
$"growing EnvCell clip-slot SSBO to {grownClipCapacity} instances");
_clipSlotCapacity = grownClipCapacity;
}
if (uniqueInstanceCount > _instLightSetCapacity)
{
int grownLightSetCapacity = Math.Max(_instLightSetCapacity * 2, uniqueInstanceCount);
TrackedGlResource.AllocateBufferStorage(
_gl,
GLEnum.ShaderStorageBuffer,
_instLightSetSsbo,
(long)_instLightSetCapacity
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
* sizeof(int),
(long)grownLightSetCapacity
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
* sizeof(int),
GLEnum.DynamicDraw,
$"growing EnvCell light-set SSBO to {grownLightSetCapacity} instances");
_instLightSetCapacity = grownLightSetCapacity;
}
}
// imperative state bracket. Every per-frame section is a ring slice
// allocated fresh in SubmitRhi, and blend plus depth-write are baked
// into the three shell pipelines rather than set here. The frame-
// started invariant this used to enforce via ActivateNextDynamicBufferSet
// still matters (it gates the light-frame-generation cache), so it is
// checked directly.
if (!_dynamicFrameStarted)
throw new InvalidOperationException("BeginFrame must be called before drawing EnvCells.");
// WB BaseObjectRenderManager.cs:761-762: grow scratch arrays.
if (_commands.Length < totalDraws)
@ -1684,189 +1319,7 @@ public sealed unsafe partial class EnvCellRenderer :
}
}
if (_gl is null)
{
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
return;
}
// WB BaseObjectRenderManager.cs:784-805 upload. Retain capacity and
// update the active prefix so portal frames cannot enqueue an unbounded
// chain of retired driver allocations.
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
fixed (DrawElementsIndirectCommand* ptr = _commands)
{
_gl.BufferSubData(GLEnum.DrawIndirectBuffer, 0,
(nuint)(totalDraws * sizeof(DrawElementsIndirectCommand)), ptr);
}
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
if (_gpuInstanceTransforms.Length < uniqueInstanceCount)
Array.Resize(ref _gpuInstanceTransforms, Math.Max(_gpuInstanceTransforms.Length * 2, uniqueInstanceCount));
for (int i = 0; i < uniqueInstanceCount; i++)
_gpuInstanceTransforms[i] = allInstances[i].Transform;
fixed (Matrix4x4* ptr = _gpuInstanceTransforms)
{
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
(nuint)(uniqueInstanceCount * sizeof(Matrix4x4)), ptr);
}
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernBatchBuffer);
fixed (ModernBatchData* ptr = _modernBatches)
{
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
(nuint)(totalDraws * sizeof(ModernBatchData)), ptr);
}
// Phase U.4: upload the per-instance clip-slot buffer (binding=3). When
// _cellIdToSlot is set (indoor routing), each cell shell instance is gated
// to its cell's CellClip slot via allInstances[i].CellId; cells absent from
// the map (shouldn't happen — the Render filter is the map's keys) and the
// U.3 path both map to slot 0 (no-clip). allInstances is laid out in the
// SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so
// instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance.
if (_clipSlotData.Length < uniqueInstanceCount)
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
// #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell
// instance to slot 0 (no-clip) — retail draws cell shells WHOLE.
if (_cellIdToSlot is null
|| AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim)
{
Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
}
else
{
for (int i = 0; i < uniqueInstanceCount; i++)
_clipSlotData[i] = _cellIdToSlot.TryGetValue(allInstances[i].CellId, out int slot)
? (uint)slot : 0u;
}
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
fixed (uint* ptr = _clipSlotData)
{
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
(nuint)(uniqueInstanceCount * sizeof(uint)), ptr);
}
// A7 Fix D (D-2): per-instance 8-int light set, parallel to the transforms,
// keyed on the cell each shell instance belongs to (mirrors _clipSlotData).
int lightStride = AcDream.Core.Lighting.LightManager.MaxLightsPerObject;
if (_lightSetData.Length < uniqueInstanceCount * lightStride)
_lightSetData = new int[System.Math.Max(_lightSetData.Length * 2, uniqueInstanceCount * lightStride)];
for (int i = 0; i < uniqueInstanceCount; i++)
{
int[] cellSet = GetCellLightSet(allInstances[i].CellId);
System.Array.Copy(cellSet, 0, _lightSetData, i * lightStride, lightStride);
}
// #176 seam-draw probe: emitted HERE (not in Render) so the per-cell light
// sets read through the just-cleared cache against THIS frame's
// _pointSnapshot — the exact data the SSBO upload below carries.
if (renderPass == WbRenderPass.Opaque
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter);
// A7 Fix D (D-2): upload binding=4 (global lights) + binding=5 (per-instance set).
int lightCount = AcDream.Core.Lighting.GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
int glUploadCount = lightCount > 0 ? lightCount : 1;
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _globalLightsSsbo);
if (glUploadCount > _globalLightsCapacity)
{
int grownGlobalLightCapacity = Math.Max(_globalLightsCapacity * 2, glUploadCount);
TrackedGlResource.AllocateBufferStorage(
_gl,
GLEnum.ShaderStorageBuffer,
_globalLightsSsbo,
(long)_globalLightsCapacity
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
* sizeof(float),
(long)grownGlobalLightCapacity
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
* sizeof(float),
GLEnum.DynamicDraw,
$"growing EnvCell global-light SSBO to {grownGlobalLightCapacity} lights");
_globalLightsCapacity = grownGlobalLightCapacity;
}
fixed (float* gp = _globalLightData)
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
(nuint)(glUploadCount * AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight * sizeof(float)), gp);
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _instLightSetSsbo);
fixed (int* lp = _lightSetData)
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
(nuint)(uniqueInstanceCount * lightStride * sizeof(int)), lp);
PersistActiveDynamicBufferCapacities();
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
// (globalVao validated at the top of the method — a return here would leak the
// pass state established above.)
if (_currentVao != globalVao)
{
_gl.BindVertexArray(globalVao);
_currentVao = globalVao;
}
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 0, _modernInstanceBuffer);
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 1, _modernBatchBuffer);
// Phase U.3: per-instance clip slots (binding=3) + shared clip regions
// (binding=2, via the GameWindow ClipFrame or our no-clip fallback).
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 3, _clipSlotBuffer);
BindClipRegionBinding2();
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 4, _globalLightsSsbo); // A7 Fix D (D-2)
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 5, _instLightSetSsbo); // A7 Fix D (D-2)
FlushAndBindTextureTable(); // Campaign V slice V2 (binding=9)
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
_gl.MemoryBarrier(MemoryBarrierMask.ShaderStorageBarrierBit | MemoryBarrierMask.CommandBarrierBit);
// WB BaseObjectRenderManager.cs:821-847: issue per-group multi-draw calls.
// The ranges retain ordered-cell boundaries, so transparent geometry
// stays far-to-near even though all command data was uploaded once.
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
{
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
int groupIndex = drawRange.GroupIndex;
var cullMode = (CullMode)(groupIndex % 4);
// Phase A8 visual-gate evidence: cell meshes use CullMode.Landblock
// uniformly, but the room surfaces need to be visible from inside
// under acdream's current global winding state. Render cell polys
// double-sided while the architectural cause is isolated.
if (cullMode == CullMode.Landblock) cullMode = CullMode.None;
if (_currentCullMode != cullMode)
{
SetCullMode(cullMode);
}
bool isAdditive = groupIndex >= 4;
if (isAdditive)
{
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
shader!.SetInt("uRenderPass", (int)renderPass | 0x100);
}
else
{
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
shader!.SetInt("uRenderPass", (int)renderPass);
}
shader!.SetInt("uDrawIDOffset", drawRange.FirstCommand);
_gl.MultiDrawElementsIndirect(
PrimitiveType.Triangles,
DrawElementsType.UnsignedShort,
(void*)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
(uint)drawRange.CommandCount,
(uint)sizeof(DrawElementsIndirectCommand));
}
// Phase U.4: leave a clean opaque-default render state (mirrors WbDrawDispatcher's
// post-transparent restore) so a Transparent pass's Blend-on / DepthMask-off does
// not leak into particles or the next frame's draws.
_gl.Disable(EnableCap.Blend);
_gl.DepthMask(true);
// WB BaseObjectRenderManager.cs:845-847:
shader!.SetInt("uDrawIDOffset", 0);
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
}
internal static void AppendMdiDrawRange(
@ -1994,116 +1447,6 @@ public sealed unsafe partial class EnvCellRenderer :
System.Console.WriteLine($"[seam-blk] t={now} changed={(changed ? 1 : 0)}{sig}");
}
// ---------------------------------------------------------------------------
// SetCullMode
// Verbatim copy of WB BaseObjectRenderManager.cs:850-866.
// ---------------------------------------------------------------------------
private void SetCullMode(CullMode mode)
{
_currentCullMode = mode;
switch (mode)
{
case CullMode.None:
_gl!.Disable(EnableCap.CullFace);
break;
case CullMode.Clockwise:
_gl!.Enable(EnableCap.CullFace);
_gl.CullFace(TriangleFace.Front);
break;
case CullMode.CounterClockwise:
case CullMode.Landblock:
_gl!.Enable(EnableCap.CullFace);
_gl.CullFace(TriangleFace.Back);
break;
}
}
// ---------------------------------------------------------------------------
// FlushAndBindTextureTable (Campaign V slice V2)
// ---------------------------------------------------------------------------
/// <summary>
/// Campaign V slice V4t: drains the device texture table's dirty runs and
/// (re)binds it at
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
/// A genuinely new slot is rare — new dat surfaces/atlases, not every frame
/// — but the bind is unconditional, because GL's storage-buffer binding
/// points are global and another raw-GL renderer's binding 9 sits there
/// between two of these draws.
/// </summary>
private void FlushAndBindTextureTable()
{
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable;
device.FlushTextureTable();
_gl!.BindBufferBase(
GLEnum.ShaderStorageBuffer,
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
device.TextureTableGlName);
}
// ---------------------------------------------------------------------------
// BindClipRegionBinding2 (Phase U.3)
// ---------------------------------------------------------------------------
/// <summary>
/// Bind the per-cell clip-region SSBO to binding=2. Prefers the shared
/// <see cref="ClipFrame"/> buffer (<see cref="SetClipRegionSsbo"/>); otherwise
/// lazily creates + binds a one-slot no-clip fallback (count 0 = pass-all) so
/// the shader never reads an unbound SSBO.
/// </summary>
private void BindClipRegionBinding2()
{
if (_sharedClipRegionSsbo != 0)
{
_gl!.BindBufferBase(GLEnum.ShaderStorageBuffer,
AcDream.App.Rendering.ClipFrame.MeshClipSsboBinding, _sharedClipRegionSsbo);
return;
}
if (_fallbackClipRegionSsbo == 0)
{
uint fallback = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell fallback clip SSBO");
bool allocated = false;
try
{
TrackedGlResource.AllocateBufferStorage(
_gl!,
GLEnum.ShaderStorageBuffer,
fallback,
0,
AcDream.App.Rendering.ClipFrame.CellClipStrideBytes,
GLEnum.DynamicDraw,
"allocating EnvCell fallback clip SSBO");
allocated = true;
// One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
Span<byte> zero = stackalloc byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes];
zero.Clear();
fixed (byte* p = zero)
{
_gl!.BufferSubData(
GLEnum.ShaderStorageBuffer,
0,
(nuint)zero.Length,
p);
}
GLHelpers.ThrowOnResourceError(_gl, "initializing EnvCell fallback clip SSBO");
_fallbackClipRegionSsbo = fallback;
}
catch
{
TrackedGlResource.DeleteBuffer(
_gl!,
fallback,
allocated ? AcDream.App.Rendering.ClipFrame.CellClipStrideBytes : 0,
"rolling back EnvCell fallback clip SSBO");
throw;
}
}
_gl!.BindBufferBase(GLEnum.ShaderStorageBuffer,
AcDream.App.Rendering.ClipFrame.MeshClipSsboBinding, _fallbackClipRegionSsbo);
}
// ---------------------------------------------------------------------------
// List pool (GetPooledList)
// Copied from WB ObjectRenderManagerBase (pattern).
@ -2154,67 +1497,12 @@ public sealed unsafe partial class EnvCellRenderer :
{
("prepare-scratch", _prepareScratch.Dispose),
};
// Campaign V slice V6j: the encoder arm owns no GL names — its
// pipelines route their physical free through the device's
// retirement queue — so the ledger below holds only the scratch.
if (_gl is null)
DisposeRhiResources();
// Campaign V slice V11: the raw-GL arm's dynamic buffer-set pool is
// deleted along with it. The RHI arm's pipelines route their
// physical free through the device's own retirement queue, so the
// ledger above holds only the scratch.
DisposeRhiResources();
for (int frame = 0; frame < _dynamicBufferSetsByFrame.Length; frame++)
{
List<DynamicBufferSet> frameSets = _dynamicBufferSetsByFrame[frame];
for (int index = 0; index < frameSets.Count; index++)
{
DynamicBufferSet set = frameSets[index];
AddTrackedBufferRelease(
releases,
set.MdiCommandBuffer,
(long)set.MdiCommandCapacity * sizeof(DrawElementsIndirectCommand),
$"dynamic-{frame}-{index}-mdi",
"deleting EnvCell MDI buffer");
AddTrackedBufferRelease(
releases,
set.ModernInstanceBuffer,
(long)set.ModernInstanceCapacity * sizeof(Matrix4x4),
$"dynamic-{frame}-{index}-instances",
"deleting EnvCell instance SSBO");
AddTrackedBufferRelease(
releases,
set.ModernBatchBuffer,
(long)set.ModernBatchCapacity * sizeof(ModernBatchData),
$"dynamic-{frame}-{index}-batches",
"deleting EnvCell batch SSBO");
AddTrackedBufferRelease(
releases,
set.ClipSlotBuffer,
(long)set.ClipSlotCapacity * sizeof(uint),
$"dynamic-{frame}-{index}-clip-slots",
"deleting EnvCell clip-slot SSBO");
AddTrackedBufferRelease(
releases,
set.GlobalLightsSsbo,
(long)set.GlobalLightsCapacity
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
* sizeof(float),
$"dynamic-{frame}-{index}-global-lights",
"deleting EnvCell global-light SSBO");
AddTrackedBufferRelease(
releases,
set.InstanceLightSetSsbo,
(long)set.InstanceLightSetCapacity
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
* sizeof(int),
$"dynamic-{frame}-{index}-light-sets",
"deleting EnvCell light-set SSBO");
}
}
AddTrackedBufferRelease(
releases,
_fallbackClipRegionSsbo,
AcDream.App.Rendering.ClipFrame.CellClipStrideBytes,
"fallback-clip-region",
"deleting EnvCell fallback clip SSBO");
_disposeResources = new RetryableResourceReleaseLedger(releases);
}
@ -2225,17 +1513,7 @@ public sealed unsafe partial class EnvCellRenderer :
"One or more EnvCell renderer resources could not be released.");
}
foreach (List<DynamicBufferSet> frameSets in _dynamicBufferSetsByFrame)
frameSets.Clear();
_activeDynamicBufferSet = null;
_dynamicFrameStarted = false;
_mdiCommandBuffer = 0;
_modernInstanceBuffer = 0;
_modernBatchBuffer = 0;
_clipSlotBuffer = 0;
_globalLightsSsbo = 0;
_instLightSetSsbo = 0;
_fallbackClipRegionSsbo = 0;
_disposeResources = null;
IsDisposed = true;
@ -2250,22 +1528,4 @@ public sealed unsafe partial class EnvCellRenderer :
_disposing = false;
}
}
private void AddTrackedBufferRelease(
List<(string Name, Action Release)> releases,
uint buffer,
long capacityBytes,
string name,
string context)
{
if (buffer == 0)
return;
RetryableGpuResourceRelease release =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl!,
buffer,
capacityBytes,
context);
releases.Add((name, release.Run));
}
}

View file

@ -1,240 +0,0 @@
using Microsoft.Extensions.Logging;
using Silk.NET.Core.Native;
using Silk.NET.OpenGL;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering.Wb {
public static class GLHelpers {
public static OpenGLGraphicsDevice? Device { get; set; }
public static ILogger? Logger { get; set; }
public static void Init(OpenGLGraphicsDevice device, ILogger logger) {
Logger = logger;
Device = device;
}
/// <summary>
/// Always-on error boundary for resource transactions. Most render-path
/// checks remain Debug-only because <c>glGetError</c> is a synchronous
/// driver call; allocation/upload code must not publish CPU state after
/// OpenGL reported OOM, context loss, or a rejected transfer in Release.
/// Call exactly once before committing each transaction.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowOnResourceError(GL gl, string context) {
GLEnum error = gl.GetError();
if (error == GLEnum.NoError)
return;
var errors = new System.Text.StringBuilder();
do {
if (errors.Length != 0)
errors.Append(", ");
errors.Append(error).Append(" (").Append(GetErrorDetails(error)).Append(')');
error = gl.GetError();
} while (error != GLEnum.NoError);
string message = $"OpenGL resource transaction failed: {errors}. Context: {context}";
Logger?.LogError(message);
throw new InvalidOperationException(message);
}
#if DEBUG
private static bool _loggedVersion = false;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckErrors(GL gl, bool logErrors = false, [CallerMemberName] string callerName = "",
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
var error = gl.GetError();
if (error != GLEnum.NoError) {
if (!_loggedVersion) {
_loggedVersion = true;
var version = gl.GetStringS(GLEnum.Version);
var vendor = gl.GetStringS(GLEnum.Vendor);
var renderer = gl.GetStringS(GLEnum.Renderer);
Logger?.LogInformation($"GL Version: {version}, Vendor: {vendor}, Renderer: {renderer}");
}
string errorDetails = GetErrorDetails(error);
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
var program = (uint)gl.GetInteger(GLEnum.CurrentProgram);
var vao = gl.GetInteger(GLEnum.VertexArrayBinding);
var activeTex = gl.GetInteger(GLEnum.ActiveTexture);
var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
string extraInfo = "";
if (program != 0) {
bool isProgram = gl.IsProgram(program);
gl.GetProgram(program, GLEnum.LinkStatus, out int linkStatus);
gl.GetProgram(program, GLEnum.DeleteStatus, out int deleteStatus);
gl.GetProgram(program, GLEnum.ValidateStatus, out int validateStatus);
extraInfo = $", IsProg: {isProgram}, Link: {linkStatus}, Del: {deleteStatus}, Valid: {validateStatus}";
}
string message = $"OpenGL Error: {error} ({errorDetails}) at {location}. Thread: {threadId}, Program: {program}{extraInfo}, VAO: {vao}, ActiveTex: {activeTex}";
Logger?.LogError(message);
throw new Exception(message);
}
}
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckErrors(GL gl, bool logErrors = false, string callerName = "",
string callerFile = "", int callerLine = 0) {
}
#endif
public static string GetErrorDetails(GLEnum error) {
return error switch {
GLEnum.InvalidEnum => "Invalid enum - An unacceptable value is specified for an enumerated argument",
GLEnum.InvalidValue => "Invalid value - A numeric argument is out of range",
GLEnum.InvalidOperation =>
"Invalid operation - The specified operation is not allowed in the current state",
GLEnum.StackOverflow => "Stack overflow - An operation would cause an internal stack to overflow",
GLEnum.StackUnderflow => "Stack underflow - An operation would cause an internal stack to underflow",
GLEnum.OutOfMemory => "Out of memory - There is not enough memory left to execute the command",
GLEnum.InvalidFramebufferOperation =>
"Invalid framebuffer operation - The framebuffer object is not complete",
GLEnum.ContextLost => "Context lost - The OpenGL context has been lost due to a graphics card reset",
_ => "Unknown error"
};
}
#if DEBUG
/// <summary>
/// Checks for OpenGL errors and provides context-specific information
/// </summary>
public static void CheckErrorsWithContext(GL gl, string context, [CallerMemberName] string callerName = "",
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
var error = gl.GetError();
if (error != GLEnum.NoError) {
string errorDetails = GetErrorDetails(error);
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
string message = $"OpenGL Error: {error} ({errorDetails})\nContext: {context}\nLocation: {location}";
Logger?.LogError(message);
throw new Exception(message);
}
}
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CheckErrorsWithContext(GL gl, string context, string callerName = "",
string callerFile = "", int callerLine = 0) {
}
#endif
/// <summary>
/// Gets detailed information about the current texture state for debugging
/// </summary>
public static string GetTextureDebugInfo(GL gl, GLEnum target) {
var info = new System.Text.StringBuilder();
info.AppendLine($"Texture Debug Info for {target}:");
try {
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
GetTextureParameter.TextureWidth, out int width);
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
GetTextureParameter.TextureHeight, out int height);
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
GetTextureParameter.TextureDepthExt, out int depth);
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
GetTextureParameter.TextureInternalFormat, out int format);
info.AppendLine($" Dimensions: {width}x{height}x{depth}");
info.AppendLine($" Internal Format: {(InternalFormat)format}");
gl.GetTexParameter(target, GetTextureParameter.TextureMinFilter, out int minFilter);
gl.GetTexParameter(target, GetTextureParameter.TextureMagFilter, out int magFilter);
info.AppendLine($" Min Filter: {(TextureMinFilter)minFilter}");
info.AppendLine($" Mag Filter: {(TextureMagFilter)magFilter}");
// Get max mipmap level
gl.GetTexParameter(target, GetTextureParameter.TextureMaxLevelSgis, out int maxLevel);
info.AppendLine($" Max Level: {maxLevel}");
// Check completeness
int maxMipLevel = (int)Math.Floor(Math.Log2(Math.Max(width, height)));
info.AppendLine($" Calculated Max Mip Level: {maxMipLevel}");
}
catch (Exception ex) {
info.AppendLine($" Error getting texture info: {ex.Message}");
}
return info.ToString();
}
/// <summary>
/// Logs current OpenGL state for debugging
/// </summary>
public static void LogGLState(GL gl, string context = "") {
var state = new System.Text.StringBuilder();
state.AppendLine($"=== OpenGL State ({context}) ===");
try {
state.AppendLine(
$"Active Texture Unit: GL_TEXTURE{gl.GetInteger(GetPName.ActiveTexture) - (int)GLEnum.Texture0}");
state.AppendLine($"Bound 2D Array Texture: {gl.GetInteger(GetPName.TextureBinding2DArray)}");
state.AppendLine($"Current Program: {gl.GetInteger(GetPName.CurrentProgram)}");
gl.GetInteger(GetPName.MaxTextureSize, out int maxTexSize);
state.AppendLine($"Max Texture Size: {maxTexSize}");
gl.GetInteger(GetPName.Max3DTextureSize, out int max3DSize);
state.AppendLine($"Max 3D Texture Size: {max3DSize}");
gl.GetInteger(GetPName.MaxArrayTextureLayers, out int maxLayers);
state.AppendLine($"Max Array Texture Layers: {maxLayers}");
}
catch (Exception ex) {
state.AppendLine($"Error getting GL state: {ex.Message}");
}
state.AppendLine("======================");
Logger?.LogInformation(state.ToString());
}
/// <summary>
/// Explicit defaults to prevent Avalonia state leakage into our custom rendering pipeline.
/// Call this at the start of complex render cycles immediately inside a GLStateScope.
/// </summary>
public static void SetupDefaultRenderState(GL gl) {
gl.BindSampler(0, 0);
gl.BindSampler(1, 0);
gl.BindSampler(2, 0);
gl.ActiveTexture(TextureUnit.Texture1);
gl.BindTexture(TextureTarget.Texture2D, 0);
gl.ActiveTexture(TextureUnit.Texture2);
gl.BindTexture(TextureTarget.Texture2D, 0);
gl.ActiveTexture(TextureUnit.Texture0); // End on Texture0
gl.BindTexture(TextureTarget.Texture2D, 0);
gl.BindVertexArray(0);
gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
gl.UseProgram(0);
gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
gl.PixelStore(PixelStoreParameter.UnpackRowLength, 0);
gl.PixelStore(PixelStoreParameter.UnpackSkipRows, 0);
gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, 0);
gl.Disable(EnableCap.StencilTest);
gl.BlendColor(0, 0, 0, 0);
gl.PolygonMode(GLEnum.FrontAndBack, PolygonMode.Fill);
// Disable Avalonia/Skia specific states
gl.Disable(EnableCap.SampleAlphaToCoverage);
gl.Disable(EnableCap.SampleAlphaToOne);
gl.Disable(EnableCap.Multisample);
gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
gl.LineWidth(1.0f);
gl.PolygonOffset(0f, 0f);
gl.Disable(EnableCap.PolygonOffsetFill);
gl.Disable((EnableCap)GLEnum.ProgramPointSize);
}
}
}

View file

@ -1,258 +0,0 @@
using Chorizite.Core.Render;
using AcDream.App.Rendering;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace AcDream.App.Rendering.Wb {
public unsafe class GLSLShader : BaseShader, IDisposable {
private OpenGLGraphicsDevice _device;
private Dictionary<string, int> _uniformLocations = [];
private Dictionary<int, object> _uniformValues = [];
private readonly object _lock = new();
private GL GL => _device.GL;
public uint Program { get; protected set; }
public bool HasUniform(string name) {
lock (_lock) {
return GetUniformLocation(Program, name) != -1;
}
}
public GLSLShader(OpenGLGraphicsDevice device, string name, string vertSource, string fragSource, ILogger log) : base(name, vertSource, fragSource, log) {
_device = device;
Load(vertSource, fragSource);
}
public GLSLShader(OpenGLGraphicsDevice device, string name, string shaderDirectory, ILogger log) : base(name, shaderDirectory, log) {
_device = device;
Load();
}
public override void Dispose() {
Unload();
base.Dispose();
}
private int GetUniformLocation(uint program, string name) {
lock (_lock) {
if (!_uniformLocations.ContainsKey(name)) {
_uniformLocations.Add(name, GL.GetUniformLocation(program, name));
}
return _uniformLocations[name];
}
}
public override void SetUniform(string location, Matrix4x4 m) {
lock (_lock) {
int loc = GetUniformLocation(Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is Matrix4x4 mCached && mCached == m) {
return;
}
_uniformValues[loc] = m;
GL.UniformMatrix4(loc, 1, false, (float*)&m);
}
}
public override void SetUniform(string location, int v) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is int vCached && vCached == v) {
return;
}
_uniformValues[loc] = v;
GL.Uniform1(loc, v);
}
}
public override void SetUniform(string location, Vector2 vec) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector2 vCached && vCached == vec) {
return;
}
_uniformValues[loc] = vec;
GL.Uniform2(loc, vec);
}
}
public override void SetUniform(string location, Vector3 vec) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector3 vCached && vCached == vec) {
return;
}
_uniformValues[loc] = vec;
GL.Uniform3(loc, vec);
}
}
public override void SetUniform(string location, Vector3[] vecs) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
fixed (float* v = &vecs[0].X) {
GL.Uniform3(loc, (uint)vecs.Length, v);
}
}
}
public override void SetUniform(string location, Vector4 vec) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector4 vCached && vCached == vec) {
return;
}
_uniformValues[loc] = vec;
GL.Uniform4(loc, vec);
}
}
public override void SetUniform(string location, float v) {
lock (_lock) {
int loc = GetUniformLocation((uint)Program, location);
if (loc == -1) return;
if (_uniformValues.TryGetValue(loc, out var val) && val is float vCached && vCached == v) {
return;
}
_uniformValues[loc] = v;
GL.Uniform1(loc, v);
}
}
public override void SetUniform(string location, float[] vs) {
lock (_lock) {
fixed (float* v = &vs[0]) {
GL.Uniform1(GetUniformLocation((uint)Program, location), (uint)vs.Length, v);
}
}
}
public override void Load(string vertShaderSource, string fragShaderSource) {
if (string.IsNullOrWhiteSpace(vertShaderSource) || string.IsNullOrWhiteSpace(fragShaderSource)) {
_log.LogError($"Shader {Name} has no source code!");
throw new InvalidOperationException($"Shader {Name} has no source code.");
}
if (_device.HasOpenGL43 && _device.HasBindless) {
string replacement = "#version 430 core\n#extension GL_ARB_bindless_texture : require";
vertShaderSource = vertShaderSource.Replace("#version 330 core", replacement);
fragShaderSource = fragShaderSource.Replace("#version 330 core", replacement);
}
var resources = new ResourceCleanupGroup();
uint prog = 0;
bool accountingPublished = false;
try {
prog = ShaderProgramConstruction.Build(
new GlShaderProgramBuildApi(GL),
vertShaderSource,
fragShaderSource);
uint ownedProgram = prog;
var unpublishedProgramRelease = new RetryableGpuResourceRelease(
() => GlResourceCommand.DeleteProgram(
GL,
ownedProgram,
$"delete unpublished WB shader program {ownedProgram}"),
() => {
if (accountingPublished)
{
GpuMemoryTracker.TrackResourceDeallocation(
GpuResourceType.Shader);
}
});
resources.Add("WB shader program", unpublishedProgramRelease.Run);
// Bind SceneData uniform block to point 0 if it exists.
GlResourceCommand.Execute(GL, $"configure shader {Name} SceneData binding", () => {
uint sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData");
if (sceneDataIndex != uint.MaxValue)
GL.UniformBlockBinding(prog, sceneDataIndex, 0);
});
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Shader);
accountingPublished = true;
resources.TransferAll();
} catch (Exception constructionFailure) {
_log.LogError(constructionFailure, "Failed to construct shader {ShaderName}", Name);
resources.RollbackConstructionAndThrow(
$"Shader {Name} construction failed and its GL program did not cleanly roll back.",
constructionFailure);
}
_log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
if (Program != 0) {
Unload();
}
_uniformLocations.Clear();
_uniformValues.Clear();
Program = prog;
ProgramId = prog;
NeedsLoad = false;
GLHelpers.CheckErrors(GL);
}
public override void Bind() {
lock (_lock) {
SetActive();
if (Program != 0) {
GL.UseProgram((uint)Program);
}
}
}
public override void Unbind() {
lock (_lock) {
GL.UseProgram(0);
GLHelpers.CheckErrors(GL);
}
}
protected override void Unload() {
lock (_lock) {
if (Program != 0) {
var prog = Program;
Program = 0;
ProgramId = 0;
_device.QueueGLAction(gl => {
gl.DeleteProgram(prog);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Shader);
});
}
}
}
}
}

View file

@ -1,230 +0,0 @@
using Silk.NET.OpenGL;
using System;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// A RAII scope for saving and restoring OpenGL state.
/// </summary>
public unsafe struct GLStateScope : IDisposable {
private readonly GL _gl;
private fixed int _viewport[4];
private bool _scissorTest;
private fixed int _scissorBox[4];
private bool _depthTest;
private int _depthFunc;
private bool _depthMask;
private bool _cullFace;
private int _cullFaceMode;
private int _frontFace;
private bool _blend;
private int _blendSrc;
private int _blendDst;
private int _blendEquation;
// Extended state
private int _blendSrcAlpha;
private int _blendDstAlpha;
private int _blendEquationAlpha;
private fixed byte _colorMask[4];
private fixed float _clearColor[4];
private float _clearDepth;
private int _currentProgram;
private int _vertexArrayBinding;
private int _arrayBufferBinding;
private int _elementArrayBufferBinding;
private int _activeTexture;
private int _textureBinding2D;
private bool _stencilTest;
private int _stencilFunc;
private int _stencilRef;
private int _stencilValueMask;
private int _stencilFail;
private int _stencilPassDepthFail;
private int _stencilPassDepthPass;
private int _stencilWritemask;
private int _unpackAlignment;
private int _packAlignment;
private int _drawFramebufferBinding;
// Skia / Avalonia extra state protections
private fixed float _blendColor[4];
private int _polygonMode;
private bool _sampleAlphaToCoverage;
private bool _multisample;
private bool _primitiveRestart;
private int _readFramebufferBinding;
private int _uniformBufferBinding0;
private float _lineWidth;
private bool _programPointSize;
private int _samplerBinding0;
private int _samplerBinding1;
private int _samplerBinding2;
private int _unpackRowLength;
private int _unpackSkipRows;
private int _unpackSkipPixels;
private bool _sampleAlphaToOne;
private bool _isDisposed;
/// <summary>
/// Captures the current OpenGL state.
/// </summary>
/// <param name="gl"></param>
public GLStateScope(GL gl) {
_gl = gl;
_isDisposed = false;
fixed (int* v = _viewport) _gl.GetInteger(GetPName.Viewport, v);
_scissorTest = _gl.IsEnabled(EnableCap.ScissorTest);
fixed (int* s = _scissorBox) _gl.GetInteger(GetPName.ScissorBox, s);
_depthTest = _gl.IsEnabled(EnableCap.DepthTest);
_gl.GetInteger(GetPName.DepthFunc, out _depthFunc);
byte depthMask = 0;
_gl.GetBoolean((GetPName)GLEnum.DepthWritemask, (bool*)&depthMask);
_depthMask = depthMask != 0;
_cullFace = _gl.IsEnabled(EnableCap.CullFace);
_gl.GetInteger(GetPName.CullFaceMode, out _cullFaceMode);
_gl.GetInteger(GetPName.FrontFace, out _frontFace);
_blend = _gl.IsEnabled(EnableCap.Blend);
_gl.GetInteger(GetPName.BlendSrcRgb, out _blendSrc);
_gl.GetInteger(GetPName.BlendDstRgb, out _blendDst);
_gl.GetInteger(GetPName.BlendSrcAlpha, out _blendSrcAlpha);
_gl.GetInteger(GetPName.BlendDstAlpha, out _blendDstAlpha);
_gl.GetInteger(GetPName.BlendEquationRgb, out _blendEquation);
_gl.GetInteger(GetPName.BlendEquationAlpha, out _blendEquationAlpha);
fixed (byte* c = _colorMask) _gl.GetBoolean((GetPName)GLEnum.ColorWritemask, (bool*)c);
fixed (float* cc = _clearColor) _gl.GetFloat(GetPName.ColorClearValue, cc);
_gl.GetFloat(GetPName.DepthClearValue, out _clearDepth);
_gl.GetInteger(GetPName.CurrentProgram, out _currentProgram);
_gl.GetInteger(GetPName.VertexArrayBinding, out _vertexArrayBinding);
_gl.GetInteger(GetPName.ArrayBufferBinding, out _arrayBufferBinding);
_gl.GetInteger(GetPName.ElementArrayBufferBinding, out _elementArrayBufferBinding);
_gl.GetInteger(GetPName.ActiveTexture, out _activeTexture);
_gl.GetInteger(GetPName.TextureBinding2D, out _textureBinding2D);
_stencilTest = _gl.IsEnabled(EnableCap.StencilTest);
_gl.GetInteger(GetPName.StencilFunc, out _stencilFunc);
_gl.GetInteger(GetPName.StencilRef, out _stencilRef);
_gl.GetInteger(GetPName.StencilValueMask, out _stencilValueMask);
_gl.GetInteger(GetPName.StencilFail, out _stencilFail);
_gl.GetInteger(GetPName.StencilPassDepthFail, out _stencilPassDepthFail);
_gl.GetInteger(GetPName.StencilPassDepthPass, out _stencilPassDepthPass);
_gl.GetInteger(GetPName.StencilWritemask, out _stencilWritemask);
_gl.GetInteger(GetPName.UnpackAlignment, out _unpackAlignment);
_gl.GetInteger(GetPName.PackAlignment, out _packAlignment);
_gl.GetInteger(GetPName.DrawFramebufferBinding, out _drawFramebufferBinding);
fixed (float* bc = _blendColor) _gl.GetFloat(GetPName.BlendColor, bc);
_gl.GetInteger(GetPName.PolygonMode, out _polygonMode);
_sampleAlphaToCoverage = _gl.IsEnabled(EnableCap.SampleAlphaToCoverage);
_multisample = _gl.IsEnabled(EnableCap.Multisample);
_primitiveRestart = _gl.IsEnabled((EnableCap)GLEnum.PrimitiveRestart);
_gl.GetInteger(GetPName.ReadFramebufferBinding, out _readFramebufferBinding);
_gl.GetInteger(GetPName.UniformBufferBinding, out _uniformBufferBinding0);
_gl.GetFloat(GetPName.LineWidth, out _lineWidth);
_programPointSize = _gl.IsEnabled((EnableCap)GLEnum.ProgramPointSize);
_gl.ActiveTexture(TextureUnit.Texture0);
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding0);
_gl.ActiveTexture(TextureUnit.Texture1);
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding1);
_gl.ActiveTexture(TextureUnit.Texture2);
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding2);
_gl.ActiveTexture((TextureUnit)_activeTexture);
_gl.GetInteger((GetPName)GLEnum.UnpackRowLength, out _unpackRowLength);
_gl.GetInteger((GetPName)GLEnum.UnpackSkipRows, out _unpackSkipRows);
_gl.GetInteger((GetPName)GLEnum.UnpackSkipPixels, out _unpackSkipPixels);
_sampleAlphaToOne = _gl.IsEnabled(EnableCap.SampleAlphaToOne);
}
/// <summary>
/// Restores only the scissor state from the scope.
/// </summary>
public void RestoreScissor() {
if (_scissorTest) _gl.Enable(EnableCap.ScissorTest);
else _gl.Disable(EnableCap.ScissorTest);
_gl.Scissor(_scissorBox[0], _scissorBox[1], (uint)_scissorBox[2], (uint)_scissorBox[3]);
}
/// <summary>
/// Restores the captured OpenGL state.
/// </summary>
public void Dispose() {
if (_isDisposed) return;
// Restoring state
if (_currentProgram != 0) _gl.UseProgram((uint)_currentProgram); else _gl.UseProgram(0);
_gl.BindVertexArray((uint)_vertexArrayBinding);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBufferBinding);
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, (uint)_elementArrayBufferBinding);
_gl.BindBuffer(GLEnum.UniformBuffer, (uint)_uniformBufferBinding0);
_gl.ActiveTexture((TextureUnit)_activeTexture);
_gl.BindTexture(TextureTarget.Texture2D, (uint)_textureBinding2D);
if (_stencilTest) _gl.Enable(EnableCap.StencilTest); else _gl.Disable(EnableCap.StencilTest);
_gl.StencilFunc((StencilFunction)_stencilFunc, _stencilRef, (uint)_stencilValueMask);
_gl.StencilOp((StencilOp)_stencilFail, (StencilOp)_stencilPassDepthFail, (StencilOp)_stencilPassDepthPass);
_gl.StencilMask((uint)_stencilWritemask);
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, _unpackAlignment);
_gl.PixelStore(PixelStoreParameter.PackAlignment, _packAlignment);
_gl.PixelStore(PixelStoreParameter.UnpackRowLength, _unpackRowLength);
_gl.PixelStore(PixelStoreParameter.UnpackSkipRows, _unpackSkipRows);
_gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, _unpackSkipPixels);
_gl.ClearColor(_clearColor[0], _clearColor[1], _clearColor[2], _clearColor[3]);
_gl.ClearDepth(_clearDepth);
_gl.Viewport(_viewport[0], _viewport[1], (uint)_viewport[2], (uint)_viewport[3]);
RestoreScissor();
if (_depthTest) _gl.Enable(EnableCap.DepthTest); else _gl.Disable(EnableCap.DepthTest);
_gl.DepthFunc((DepthFunction)_depthFunc);
_gl.DepthMask(_depthMask);
if (_cullFace) _gl.Enable(EnableCap.CullFace); else _gl.Disable(EnableCap.CullFace);
_gl.CullFace((TriangleFace)_cullFaceMode);
_gl.FrontFace((FrontFaceDirection)_frontFace);
if (_blend) _gl.Enable(EnableCap.Blend); else _gl.Disable(EnableCap.Blend);
_gl.BlendFuncSeparate((BlendingFactor)_blendSrc, (BlendingFactor)_blendDst, (BlendingFactor)_blendSrcAlpha, (BlendingFactor)_blendDstAlpha);
_gl.BlendEquationSeparate((BlendEquationModeEXT)_blendEquation, (BlendEquationModeEXT)_blendEquationAlpha);
_gl.BlendColor(_blendColor[0], _blendColor[1], _blendColor[2], _blendColor[3]);
_gl.ColorMask(_colorMask[0] != 0, _colorMask[1] != 0, _colorMask[2] != 0, _colorMask[3] != 0);
_gl.PolygonMode(GLEnum.FrontAndBack, (PolygonMode)_polygonMode);
if (_sampleAlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage); else _gl.Disable(EnableCap.SampleAlphaToCoverage);
if (_sampleAlphaToOne) _gl.Enable(EnableCap.SampleAlphaToOne); else _gl.Disable(EnableCap.SampleAlphaToOne);
if (_multisample) _gl.Enable(EnableCap.Multisample); else _gl.Disable(EnableCap.Multisample);
if (_primitiveRestart) _gl.Enable((EnableCap)GLEnum.PrimitiveRestart); else _gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
if (_programPointSize) _gl.Enable((EnableCap)GLEnum.ProgramPointSize); else _gl.Disable((EnableCap)GLEnum.ProgramPointSize);
_gl.LineWidth(_lineWidth);
_gl.BindSampler(0, (uint)_samplerBinding0);
_gl.BindSampler(1, (uint)_samplerBinding1);
_gl.BindSampler(2, (uint)_samplerBinding2);
_gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, (uint)_drawFramebufferBinding);
_gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, (uint)_readFramebufferBinding);
_isDisposed = true;
}
}
}

View file

@ -1,10 +1,8 @@
using System.Runtime.InteropServices;
using AcDream.Content;
using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Gl;
namespace AcDream.App.Rendering.Wb;
@ -53,15 +51,6 @@ internal sealed class GlobalMeshMigrationAbortTicket
public void Advance() => _release.Run();
}
internal static class GlobalMeshVaoAccounting
{
public static void TrackAllocation() =>
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO);
public static void TrackDeallocation() =>
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
}
internal enum GlobalMeshCapacityResult
{
Ready,
@ -82,19 +71,15 @@ internal enum GlobalMeshCapacityResult
/// backend implements with <c>vkCmdCopyBuffer</c>. The reclaimable-range
/// allocator, growth quanta, budgeted incremental migration, retirement-ledger
/// gating and the dual-generation physical ceiling are unchanged; only the
/// resource handle type moved. The vertex array object stays raw GL because a
/// VAO has no RHI equivalent (Vulkan bakes vertex input into the pipeline) and
/// <c>WbDrawDispatcher</c>, <c>EnvCellRenderer</c> and <c>ParticleRenderer</c>
/// still bind <see cref="VAO"/>/<see cref="VBO"/>/<see cref="IBO"/> directly
/// on the GL arm.
/// resource handle type moved.
///
/// <para>Campaign V slice V6i-3 made the GL context optional. A backend that has
/// none builds no vertex array and publishes no raw names — <see cref="VAO"/>,
/// <see cref="VBO"/> and <see cref="IBO"/> are 0 there — and its consumers bind
/// <para>Campaign V slice V6i-3 made the GL context (and its vertex array
/// object, which has no RHI equivalent — Vulkan bakes vertex input into the
/// pipeline) optional; Campaign V slice V11 deleted the GL arm entirely, so
/// this arena now only ever builds the two backing stores. Its consumers bind
/// <see cref="VertexStore"/> and <see cref="IndexStore"/> through the pass
/// encoder instead, which is the same 32-byte position/normal/texcoord layout
/// expressed as pipeline vertex input. Everything above the handle — the
/// allocator, the migration, the ledger — is one body on both arms.</para>
/// encoder, which is the same 32-byte position/normal/texcoord layout
/// expressed as pipeline vertex input.</para>
/// </summary>
public sealed class GlobalMeshBuffer : IDisposable
{
@ -113,10 +98,6 @@ public sealed class GlobalMeshBuffer : IDisposable
internal const int MaximumIndexCapacity =
(int)(MaximumIndexBufferBytes / sizeof(ushort));
// Retained only for the vertex array object and its attribute layout, which
// the RHI has no verb for, and null on a backend with no such object. It is
// retired with the raw-GL dispatcher.
private readonly GL? _gl;
private readonly IGpuDevice _device;
private readonly GpuRetirementLedger _retirementLedger;
private readonly GpuRetiredRangeAllocator _vertices;
@ -134,20 +115,6 @@ public sealed class GlobalMeshBuffer : IDisposable
store ?? throw new InvalidOperationException(
"The global mesh arena has no live backing store.");
/// <summary>
/// Campaign V slice V4b transitional bridge. The arena owns its stores as
/// <see cref="IGpuBuffer"/>, but its consumers — the vertex array object here,
/// and <c>WbDrawDispatcher</c>/<c>EnvCellRenderer</c>/<c>ParticleRenderer</c>
/// through <see cref="VBO"/>/<see cref="IBO"/> — are still raw GL until slice
/// V4c. This is the only place that reaches through the interface, and it
/// disappears with those consumers.
/// </summary>
private static GlGpuBuffer RequireGlBuffer(IGpuBuffer buffer) =>
buffer as GlGpuBuffer
?? throw new NotSupportedException(
"The global mesh arena requires a GL-backed buffer while its draw paths "
+ "still bind raw GL names (Campaign V slice V4c retires that requirement).");
private enum BufferKind
{
Vertices,
@ -167,35 +134,16 @@ public sealed class GlobalMeshBuffer : IDisposable
public long CopiedBytes { get; set; }
}
public uint VAO { get; private set; }
/// <summary>
/// The vertex store's raw GL name, or 0 on a backend with no GL context.
/// Transitional: the GL draw paths still bind the arena themselves, so the
/// arena keeps publishing the backend name of the buffer it now owns as an
/// <see cref="IGpuBuffer"/>.
/// </summary>
public uint VBO =>
_gl is null || _vertexBuffer is null ? 0u : RequireGlBuffer(_vertexBuffer).GlName;
/// <summary>The index store's raw GL name. See <see cref="VBO"/>.</summary>
public uint IBO =>
_gl is null || _indexBuffer is null ? 0u : RequireGlBuffer(_indexBuffer).GlName;
/// <summary>
/// The vertex store as the contract's own handle. This is what a pass
/// encoder binds, and it is live on both arms — <see cref="VAO"/> is the
/// GL-only expression of the same thing.
/// encoder binds.
/// </summary>
internal IGpuBuffer? VertexStore => _vertexBuffer;
/// <summary>The index store as the contract's own handle. See <see cref="VertexStore"/>.</summary>
internal IGpuBuffer? IndexStore => _indexBuffer;
/// <summary>
/// True once both backing stores exist. The backend-neutral form of the
/// <c>VAO != 0</c> readiness test the raw-GL draw paths make.
/// </summary>
/// <summary>True once both backing stores exist.</summary>
internal bool HasStores => _vertexBuffer is not null && _indexBuffer is not null;
internal long UploadCount { get; private set; }
internal long UploadedBytes { get; private set; }
@ -268,9 +216,8 @@ public sealed class GlobalMeshBuffer : IDisposable
newBuffers);
}
internal GlobalMeshBuffer(GL? gl, IGpuDevice device, IGpuResourceRetirementQueue retirement)
internal GlobalMeshBuffer(IGpuDevice device, IGpuResourceRetirementQueue retirement)
{
_gl = gl;
_device = device ?? throw new ArgumentNullException(nameof(device));
ArgumentNullException.ThrowIfNull(retirement);
_retirementLedger = new GpuRetirementLedger(retirement);
@ -295,47 +242,20 @@ public sealed class GlobalMeshBuffer : IDisposable
| GpuBufferUsage.TransferDestination,
GpuMemoryResidency.DeviceLocal);
private unsafe void InitBuffers()
private void InitBuffers()
{
uint vao = 0;
IGpuBuffer? vbo = null;
IGpuBuffer? ibo = null;
long vertexBytes = (long)_vertices.Capacity * VertexPositionNormalTexture.Size;
long indexBytes = (long)_indices.Capacity * sizeof(ushort);
bool vaoTracked = false;
bool vertexTracked = false;
bool indexTracked = false;
try
{
// The vertex array is the one object here with no RHI equivalent —
// Vulkan bakes vertex input into the pipeline — so a backend with no
// GL context builds the two stores and nothing else.
if (_gl is { } gl)
{
gl.GenVertexArrays(1, out vao);
if (vao == 0)
throw new InvalidOperationException("OpenGL did not create the global mesh-buffer objects.");
}
vbo = _device.CreateBuffer(DescribeStore(BufferKind.Vertices, vertexBytes, _storeGeneration));
ibo = _device.CreateBuffer(DescribeStore(BufferKind.Indices, indexBytes, _storeGeneration));
if (_gl is { } glBind)
{
glBind.BindVertexArray(vao);
glBind.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(vbo).GlName);
ConfigureVertexAttributes(glBind);
glBind.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(ibo).GlName);
GLHelpers.ThrowOnResourceError(
glBind,
$"creating global mesh buffers ({vertexBytes} vertex bytes, {indexBytes} index bytes)");
GlobalMeshVaoAccounting.TrackAllocation();
vaoTracked = true;
}
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
GpuMemoryTracker.TrackAllocation(vertexBytes, GpuResourceType.Buffer);
vertexTracked = true;
@ -343,25 +263,15 @@ public sealed class GlobalMeshBuffer : IDisposable
GpuMemoryTracker.TrackAllocation(indexBytes, GpuResourceType.Buffer);
indexTracked = true;
VAO = vao;
_vertexBuffer = vbo;
_indexBuffer = ibo;
}
catch
{
// Construction rollback: nothing was ever submitted, so the physical
// stores are released on the spot rather than deferred. Pattern-matched
// rather than RequireGlBuffer'd so a non-GL store could never raise a
// cast failure that masks the original construction exception.
if (ibo is GlGpuBuffer stagedIndexStore)
stagedIndexStore.DeleteRetired("rolling back the global index arena buffer");
else
ibo?.Dispose();
if (vbo is GlGpuBuffer stagedVertexStore)
stagedVertexStore.DeleteRetired("rolling back the global vertex arena buffer");
else
vbo?.Dispose();
if (vao != 0) _gl!.DeleteVertexArray(vao);
// stores are released on the spot rather than deferred.
ibo?.Dispose();
vbo?.Dispose();
if (indexTracked)
{
GpuMemoryTracker.TrackDeallocation(indexBytes, GpuResourceType.Buffer);
@ -372,25 +282,8 @@ public sealed class GlobalMeshBuffer : IDisposable
GpuMemoryTracker.TrackDeallocation(vertexBytes, GpuResourceType.Buffer);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
}
if (vaoTracked)
GlobalMeshVaoAccounting.TrackDeallocation();
throw;
}
finally
{
_gl?.BindVertexArray(0);
}
}
private static unsafe void ConfigureVertexAttributes(GL gl)
{
int stride = VertexPositionNormalTexture.Size;
gl.EnableVertexAttribArray(0);
gl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint)stride, (void*)0);
gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(1, 3, GLEnum.Float, false, (uint)stride, (void*)(3 * sizeof(float)));
gl.EnableVertexAttribArray(2);
gl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
}
internal GlobalMeshAllocation UploadMesh(
@ -673,9 +566,9 @@ public sealed class GlobalMeshBuffer : IDisposable
/// <summary>
/// Copies at most <paramref name="maximumCopyBytes"/> of the immutable
/// live prefix into the staged backing store. The active VAO continues to
/// reference the old store until the final chunk succeeds, then one atomic
/// VAO rebind publishes the destination.
/// live prefix into the staged backing store. Draws continue to reference
/// the old store until the final chunk succeeds, then one atomic field swap
/// (<see cref="CommitMigration"/>) publishes the destination.
/// </summary>
internal GlobalMeshMaintenanceStep AdvanceMigration(long maximumCopyBytes)
{
@ -819,48 +712,9 @@ public sealed class GlobalMeshBuffer : IDisposable
private void CommitMigration(BufferMigration migration)
{
// The atomic publication step is a VAO rebind on GL and nothing at all
// on a backend whose vertex source is a per-draw encoder bind: the field
// swap below IS the publication there, and the next pass reads the new
// store. The rollback arm exists for the same reason it did — a failed
// rebind must leave the vertex array pointing at the live store.
if (_gl is { } gl)
{
try
{
gl.BindVertexArray(VAO);
if (migration.Kind == BufferKind.Vertices)
{
gl.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(migration.NewBuffer).GlName);
ConfigureVertexAttributes(gl);
}
else
{
gl.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(migration.NewBuffer).GlName);
}
GLHelpers.ThrowOnResourceError(gl, $"publishing staged {migration.Kind} arena buffer");
}
catch
{
gl.BindVertexArray(VAO);
if (migration.Kind == BufferKind.Vertices)
{
gl.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(migration.OldBuffer).GlName);
ConfigureVertexAttributes(gl);
}
else
{
gl.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(migration.OldBuffer).GlName);
}
gl.BindVertexArray(0);
throw;
}
finally
{
gl.BindVertexArray(0);
}
}
// The atomic publication step is nothing at all here: the vertex source
// is a per-draw pass-encoder bind, so the field swap below IS the
// publication, and the next pass reads the new store.
if (migration.Kind == BufferKind.Vertices)
{
_vertexBuffer = migration.NewBuffer;
@ -908,11 +762,10 @@ public sealed class GlobalMeshBuffer : IDisposable
/// <summary>
/// The arena's own flight gate — <see cref="_retirementLedger"/> and the abort
/// ticket — already proves no submitted frame can reference the store, so the
/// physical delete runs here rather than being deferred a second time by
/// <see cref="IGpuBuffer.Dispose"/>. Stages match
/// <c>TrackedGlResource.CreateRetryableBufferDeletion</c> exactly: precondition,
/// mutation-with-validation, byte accounting, then resource-count accounting,
/// so a driver failure re-issues only the delete and never double-counts.
/// physical delete runs here rather than being deferred a second time.
/// Stages: precondition (no-op), mutation (dispose), byte accounting, then
/// resource-count accounting, so a failure re-issues only the delete and
/// never double-counts.
/// </summary>
private RetryableGpuResourceRelease CreateRetryableStoreDeletion(
IGpuBuffer buffer,
@ -921,24 +774,9 @@ public sealed class GlobalMeshBuffer : IDisposable
{
ArgumentNullException.ThrowIfNull(buffer);
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
GL? gl = _gl;
return new RetryableGpuResourceRelease(
() =>
{
if (gl is not null)
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
},
// On GL the delete runs here rather than through IGpuBuffer.Dispose
// because the arena's own flight gate has already proven no submitted
// frame can reference the store. A backend with no GL context has no
// second deferral to skip: Dispose IS its retirement-queued release.
() =>
{
if (gl is not null)
RequireGlBuffer(buffer).DeleteRetired(context);
else
buffer.Dispose();
},
() => { },
() => buffer.Dispose(),
() =>
{
if (capacityBytes != 0)
@ -1020,16 +858,6 @@ public sealed class GlobalMeshBuffer : IDisposable
releases.Add(("staged-migration-buffer", release.Run));
}
if (VAO != 0 && _gl is { } vaoGl)
{
RetryableGpuResourceRelease release =
TrackedGlResource.CreateRetryableVertexArrayDeletion(
vaoGl,
VAO,
$"deleting global mesh vertex array {VAO}",
GlobalMeshVaoAccounting.TrackDeallocation);
releases.Add(("global-vao", release.Run));
}
if (_vertexBuffer is { } vertexStore)
{
RetryableGpuResourceRelease release =
@ -1058,7 +886,6 @@ public sealed class GlobalMeshBuffer : IDisposable
_migration = null;
_migrationAbort = null;
VAO = 0;
_vertexBuffer = null;
_indexBuffer = null;
_disposeResources = null;

View file

@ -14,11 +14,11 @@ namespace AcDream.App.Rendering.Wb;
/// retirement queue, the shared instance VBO, and two capability flags. Seven
/// members out of a 760-line class.</para>
///
/// <para>So the coupling is expressed as an interface at exactly that surface
/// and <see cref="OpenGLGraphicsDevice"/> declares it — every member already
/// existed, so the GL arm executes not one changed statement. What this buys is
/// that <c>ObjectMeshManager</c> and <c>WbMeshAdapter</c> no longer NAME a
/// backend, which is the prerequisite for the slice that gives them a second
/// <para>So the coupling is expressed as an interface at exactly that surface,
/// and <c>OpenGLGraphicsDevice</c> declared it — every member already existed,
/// so the GL arm executed not one changed statement. What this buys is that
/// <c>ObjectMeshManager</c> and <c>WbMeshAdapter</c> no longer NAME a backend,
/// which is the prerequisite for the slice that gives them a second
/// implementation.</para>
///
/// <para><b>What slice V6i-3 then moved.</b> V6i-2 left the upload bodies raw —
@ -27,10 +27,16 @@ namespace AcDream.App.Rendering.Wb;
/// GL but not RUN. The arena now builds its stores through
/// <c>IGpuDevice.CreateBuffer</c> and publishes them as
/// <c>GlobalMeshBuffer.VertexStore</c>/<c>IndexStore</c>, which a pass encoder
/// binds; the vertex array is built only where one exists. What still reads
/// <see cref="Gl"/> is the LEGACY per-mesh upload the N.5 ship amendment made
/// unreachable, and <c>AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice</c>
/// is the second implementation this interface was cut for.</para>
/// binds; the vertex array is built only where one exists.
/// <see cref="AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice"/> is the
/// second implementation this interface was cut for.</para>
///
/// <para><b>Campaign V slice V11</b> deleted <c>OpenGLGraphicsDevice</c> along
/// with the rest of the raw-GL arm it fronted, so
/// <see cref="AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice"/> is now
/// the interface's only implementation. <see cref="Gl"/> always answers null
/// there; removing it (and the legacy per-mesh upload bodies it alone still
/// gated) is Campaign V's package/shader cleanup slice, not this one.</para>
/// </summary>
internal interface IMeshPipelineDevice : IDisposable
{

View file

@ -1,104 +0,0 @@
using Chorizite.Core.Render;
using Silk.NET.OpenGL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET.
/// </summary>
public class ManagedGLFramebuffer : IFramebuffer {
private readonly OpenGLGraphicsDevice _device;
private GL _gl => _device.GL;
private readonly uint _fboId;
private readonly uint _depthStencilRenderbuffer; // 0 if not used
private readonly ITexture _texture;
private readonly int _width;
private readonly int _height;
public ITexture Texture => _texture;
public IntPtr NativeHandle => new IntPtr(_fboId);
public ManagedGLFramebuffer(OpenGLGraphicsDevice device, ITexture texture, int width, int height, bool hasDepthStencil) {
_device = device;
_texture = texture;
_width = width;
_height = height;
// Generate and bind the framebuffer
_fboId = _gl.GenFramebuffer();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.FBO);
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fboId);
// Attach the texture as the color attachment
_gl.FramebufferTexture2D(
FramebufferTarget.Framebuffer,
FramebufferAttachment.ColorAttachment0,
TextureTarget.Texture2D,
(uint)texture.NativePtr.ToInt32(),
0
);
// Create and attach a depth-stencil renderbuffer if requested
if (true || hasDepthStencil) {
_depthStencilRenderbuffer = _gl.GenRenderbuffer();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.RBO);
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthStencilRenderbuffer);
_gl.RenderbufferStorage(
RenderbufferTarget.Renderbuffer,
InternalFormat.Depth24Stencil8,
(uint)width,
(uint)height
);
_gl.FramebufferRenderbuffer(
FramebufferTarget.Framebuffer,
FramebufferAttachment.DepthStencilAttachment,
RenderbufferTarget.Renderbuffer,
_depthStencilRenderbuffer
);
GpuMemoryTracker.TrackAllocation(_width * _height * 4, GpuResourceType.RBO); // Depth24Stencil8 is 4 bytes per pixel
}
// Check framebuffer completeness
var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
if (status != GLEnum.FramebufferComplete) {
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
_gl.DeleteFramebuffer(_fboId);
if (_depthStencilRenderbuffer != 0) {
_gl.DeleteRenderbuffer(_depthStencilRenderbuffer);
}
throw new InvalidOperationException($"Framebuffer creation failed: {status}");
}
var error = _gl.GetError();
if (error != GLEnum.NoError) {
throw new InvalidOperationException($"OpenGL error during framebuffer setup: {error}");
}
// Unbind the framebuffer
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
}
public void Dispose() {
var fboId = _fboId;
var depthStencilRenderbuffer = _depthStencilRenderbuffer;
var width = _width;
var height = _height;
_device.QueueGLAction(gl => {
if (fboId != 0) {
gl.DeleteFramebuffer(fboId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.FBO);
}
if (depthStencilRenderbuffer != 0) {
gl.DeleteRenderbuffer(depthStencilRenderbuffer);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.RBO);
GpuMemoryTracker.TrackDeallocation(width * height * 4, GpuResourceType.RBO);
}
});
}
}
}

View file

@ -1,184 +0,0 @@
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Silk.NET.OpenGL;
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// OpenGL index buffer
/// </summary>
public unsafe class ManagedGLIndexBuffer : IIndexBuffer {
private uint bufferId;
private readonly OpenGLGraphicsDevice _device;
private void* _mappedPtr;
private GL GL => _device.GL;
/// <inheritdoc />
public int Size { get; private set; }
/// <inheritdoc />
public BufferUsage Usage { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="ManagedGLIndexBuffer"/> class.
/// </summary>
/// <param name="usage">Buffer usage</param>
/// <param name="size">The size of the buffer, in bytes</param>
public unsafe ManagedGLIndexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
_device = device;
Size = size;
Usage = usage;
// Generate the buffer
bufferId = GL.GenBuffer();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
// Allocate the buffer with the specified size but no initial data
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
if (_device.HasBufferStorage) {
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
GL.BufferStorage(GLEnum.ElementArrayBuffer, (uint)Size, (void*)0, flags);
_mappedPtr = GL.MapBufferRange(GLEnum.ElementArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
} else {
GL.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)Size, (void*)0, Usage.ToGL());
}
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
}
/// <inheritdoc />
public void SetData(uint[] data) {
SetData(data.AsSpan());
}
/// <inheritdoc />
public unsafe void SetData(Span<uint> data) {
uint dataSize = (uint)data.Length * sizeof(uint);
// Ensure the buffer size is sufficient
if (dataSize > Size) {
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
}
if (_mappedPtr != null) {
Span<uint> mappedSpan = new Span<uint>(_mappedPtr, data.Length);
data.CopyTo(mappedSpan);
} else {
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
fixed (uint* dataPtr = &data[0]) {
GL.BufferData(GLEnum.ElementArrayBuffer, dataSize, (void*)dataPtr, Usage.ToGL());
}
GLHelpers.CheckErrors(GL);
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
GLHelpers.CheckErrors(GL);
}
}
/// <inheritdoc />
public unsafe void SetSubData(Span<uint> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
if (Usage != BufferUsage.Dynamic) {
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
}
if (lengthElements <= 0) {
lengthElements = data.Length - sourceOffsetElements;
}
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
if (dataSizeBytes == 0) {
return;
}
// Make sure we're not trying to write past the end of the buffer
if (destinationOffsetBytes + dataSizeBytes > Size) {
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
}
if (_mappedPtr != null) {
Span<uint> mappedSpan = new Span<uint>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
} else {
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
GL.BufferSubData(
GLEnum.ElementArrayBuffer,
destinationOffsetBytes,
dataSizeBytes,
(void*)dataPtr);
GLHelpers.CheckErrors(GL);
}
}
}
/// <inheritdoc />
public unsafe void SetSubData(uint[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
if (Usage != BufferUsage.Dynamic) {
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
}
if (lengthElements <= 0) {
lengthElements = data.Length - sourceOffsetElements;
}
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
if (dataSizeBytes == 0) {
return;
}
// Make sure we're not trying to write past the end of the buffer
if (destinationOffsetBytes + dataSizeBytes > Size) {
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
}
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
GL.BufferSubData(
GLEnum.ElementArrayBuffer,
destinationOffsetBytes,
dataSizeBytes,
(void*)dataPtr);
GLHelpers.CheckErrors(GL);
}
}
/// <inheritdoc />
public void Bind() {
RenderStateCache.CurrentIBO = 0;
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public void Unbind() {
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
GLHelpers.CheckErrors(GL);
}
public unsafe void Dispose() {
_device.QueueGLAction(GL => {
if (bufferId != 0) {
GL.DeleteBuffer(bufferId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
bufferId = 0;
_mappedPtr = null;
}
});
}
}
}

View file

@ -1,165 +0,0 @@
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb {
public unsafe class ManagedGLTexture : ITexture {
private uint _texture;
private readonly OpenGLGraphicsDevice _device;
private GL GL => (_device as OpenGLGraphicsDevice).GL;
/// <inheritdoc/>
public IntPtr NativePtr => (IntPtr)_texture;
/// <inheritdoc/>
public int Width { get; private set; }
/// <inheritdoc/>
public int Height { get; private set; }
public TextureFormat Format => TextureFormat.RGBA8;
/// <inheritdoc/>
public ManagedGLTexture(OpenGLGraphicsDevice device, byte[]? source, int width, int height, TextureParameters? texParams = null) {
var p = texParams ?? TextureParameters.Default;
_device = device;
_texture = GL.GenTexture();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
Width = width;
Height = height;
GL.BindTexture(GLEnum.Texture2D, _texture);
GLHelpers.CheckErrors(GL);
int maxDimension = Math.Max(width, height);
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
if (_device.HasTextureStorage) {
GL.TexStorage2D(GLEnum.Texture2D, (uint)mipLevels, GLEnum.Rgba8, (uint)width, (uint)height);
GLHelpers.CheckErrors(GL);
}
else {
GL.TexImage2D(GLEnum.Texture2D, 0, (int)InternalFormat.Rgba8, (uint)width, (uint)height, 0, PixelFormat.Rgba, (PixelType)0x1401, (void*)0);
GLHelpers.CheckErrors(GL);
}
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapS, (int)p.WrapS);
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapT, (int)p.WrapT);
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMinFilter, (int)p.MinFilter);
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
GLHelpers.CheckErrors(GL);
if (p.EnableAnisotropicFiltering && _device.RenderSettings.EnableAnisotropicFiltering)
{
if (_device.MaxSupportedAnisotropy > 0)
{
GL.TexParameter(GLEnum.Texture2D, GLEnum.TextureMaxAnisotropy,
_device.MaxSupportedAnisotropy);
}
}
if (p.EnableMipmaps) {
GL.GenerateMipmap(GLEnum.Texture2D);
}
GLHelpers.CheckErrors(GL);
GL.BindTexture(GLEnum.Texture2D, 0);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackAllocation(CalculateSize(), GpuResourceType.Texture);
}
private long CalculateSize() {
int maxDimension = Math.Max(Width, Height);
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
long totalSize = 0;
for (int i = 0; i < mipLevels; i++) {
int w = Math.Max(1, Width >> i);
int h = Math.Max(1, Height >> i);
totalSize += (long)w * h * 4;
}
return totalSize;
}
/// <inheritdoc/>
public ManagedGLTexture(OpenGLGraphicsDevice device, string file) {
throw new NotImplementedException();
}
public void SetData(Rectangle rectangle, byte[] data) {
if (_texture == 0) return;
GLHelpers.CheckErrors(GL);
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
RenderStateCache.CurrentAtlas = 0;
GL.GetInteger(GLEnum.TextureBinding2D, out int oldBinding);
GL.BindTexture(GLEnum.Texture2D, _texture);
fixed (byte* ptr = data) {
GL.TexSubImage2D(
GLEnum.Texture2D,
0, // level
rectangle.X,
rectangle.Y,
(uint)rectangle.Width,
(uint)rectangle.Height,
PixelFormat.Rgba,
PixelType.UnsignedByte,
ptr
);
}
// Generate mipmaps if needed
GL.GenerateMipmap(GLEnum.Texture2D);
GL.BindTexture(GLEnum.Texture2D, (uint)oldBinding);
GL.ActiveTexture((GLEnum)oldActiveTexture);
GLHelpers.CheckErrors(GL);
}
public void Bind(int slot = 0) {
if (slot == 0) {
RenderStateCache.CurrentAtlas = 0;
}
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.Texture2D, (uint)NativePtr);
if (changedUnit) {
GL.ActiveTexture((GLEnum)oldActiveTexture);
}
GLHelpers.CheckErrors(GL);
}
public void Unbind() {
GL.BindTexture(GLEnum.Texture2D, 0);
GLHelpers.CheckErrors(GL);
}
protected void ReleaseTexture() {
_device.QueueGLAction(GL => {
if (_texture != 0) {
GL.DeleteTexture(_texture);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
GpuMemoryTracker.TrackDeallocation(CalculateSize(), GpuResourceType.Texture);
}
GLHelpers.CheckErrors(GL);
_texture = 0;
});
}
public void Dispose() {
ReleaseTexture();
}
}
}

View file

@ -1,705 +0,0 @@
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, IWorldTextureArray {
private readonly bool[] _usedLayers;
private readonly GL GL;
private readonly OpenGLGraphicsDevice _device;
private readonly ILogger _logger;
/// <summary>
/// Campaign V slice V6i-2: the device whose one texture table this
/// array's two resident handles are interned into. Before this slice
/// <c>ObjectMeshManager</c> read the handles off this object and did the
/// interning itself; a 64-bit bindless handle cannot cross to Vulkan, so
/// the array now answers <see cref="ResolveSlot"/> instead. Null only
/// for the legacy <c>OpenGLGraphicsDevice.CreateTextureArrayInternal</c>
/// entry points, which no shared atlas uses.
/// </summary>
private readonly AcDream.App.Rendering.Gpu.Gl.GlGpuDevice? _worldTextureTable;
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)
: this(graphicsDevice, format, width, height, size, logger, worldTextureTable: null, texParams) {
}
internal ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
int size, ILogger logger,
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice? worldTextureTable,
TextureParameters? texParams = null) {
_worldTextureTable = worldTextureTable;
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;
bool IWorldTextureArray.HasDurableDisposeOwnership => HasDurableDisposeOwnership;
bool IWorldTextureArray.IsPhysicalRetirementComplete => IsPhysicalRetirementComplete;
/// <summary>
/// Campaign V slice V6i-2: this array's device-table slot for the
/// requested address mode.
///
/// <para>The interning call is the one <c>ObjectMeshManager</c> made
/// itself before this slice, moved one level down so the caller can be
/// written against <see cref="IWorldTextureArray"/> instead of against a
/// 64-bit <c>ARB_bindless_texture</c> handle that has no Vulkan
/// spelling. It is idempotent by handle, which is why it stays a per-batch
/// call rather than becoming cached state — exactly as before.</para>
/// </summary>
AcDream.App.Rendering.Gpu.GpuTextureSlot IWorldTextureArray.ResolveSlot(bool wrapping) {
if (_worldTextureTable is null)
return AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
ulong handle = wrapping ? _retiredWrapHandle : _retiredClampHandle;
if (handle == 0)
handle = wrapping ? BindlessWrapHandle : BindlessClampHandle;
return _worldTextureTable.RegisterWorldTextureHandle(handle);
}
/// <summary>
/// Retires both table entries. <see cref="Dispose"/> zeroes the public
/// handle properties, so the values are captured there and read from the
/// captures here — this is called after physical retirement completes,
/// which is necessarily after Dispose.
/// </summary>
public void ReleaseTextureSlots() {
if (_worldTextureTable is null)
return;
_worldTextureTable.ReleaseWorldTextureHandle(_retiredWrapHandle);
_worldTextureTable.ReleaseWorldTextureHandle(_retiredClampHandle);
_retiredWrapHandle = 0;
_retiredClampHandle = 0;
}
private ulong _retiredWrapHandle;
private ulong _retiredClampHandle;
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();
// Slice V6i-2: the handles the two table entries are keyed by. The
// properties are zeroed below, so ReleaseTextureSlots — which runs
// only once physical retirement completes — reads these captures.
_retiredWrapHandle = bindlessWrapHandle;
_retiredClampHandle = bindlessClampHandle;
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;
}
}
}
}

View file

@ -1,177 +0,0 @@
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
using System.Runtime.InteropServices;
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// OpenGL uniform buffer
/// </summary>
public unsafe class ManagedGLUniformBuffer : IUniformBuffer {
private uint bufferId;
private readonly OpenGLGraphicsDevice _device;
private GL GL => _device.GL;
/// <inheritdoc />
public int Size { get; private set; }
/// <inheritdoc />
public BufferUsage Usage { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="ManagedGLUniformBuffer"/> class.
/// </summary>
/// <param name="device">Graphics device</param>
/// <param name="usage">Buffer usage</param>
/// <param name="size">The size of the buffer, in bytes</param>
public unsafe ManagedGLUniformBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
_device = device ?? throw new ArgumentNullException(nameof(device));
ArgumentOutOfRangeException.ThrowIfLessThan(size, 1);
Size = size;
Usage = usage;
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
uint buffer = 0;
bool allocated = false;
try {
buffer = TrackedGlResource.CreateBuffer(
GL,
"creating managed uniform buffer");
uint ownedBuffer = buffer;
RetryableGpuResourceRelease unpublishedBufferRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
GL,
ownedBuffer,
() => allocated ? Size : 0,
"rolling back managed uniform buffer");
resources.Add(
"managed uniform buffer",
unpublishedBufferRelease.Run);
TrackedGlResource.AllocateBufferStorage(
GL,
GLEnum.UniformBuffer,
buffer,
0,
Size,
GLEnum.DynamicDraw,
"allocating managed uniform buffer");
allocated = true;
resources.TransferAll();
} catch (Exception constructionFailure) {
resources.RollbackConstructionAndThrow(
"ManagedGLUniformBuffer construction failed and its GL buffer did not cleanly roll back.",
constructionFailure);
}
bufferId = buffer;
}
/// <inheritdoc />
public unsafe void SetData<T>(T[] data) where T : unmanaged {
SetData(data.AsSpan());
}
/// <inheritdoc />
public unsafe void SetData<T>(Span<T> data) where T : unmanaged {
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
// Ensure the buffer size is sufficient
if (dataSize > Size) {
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
}
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
fixed (T* ptr = data) {
GL.BufferSubData(GLEnum.UniformBuffer, 0, (nuint)dataSize, ptr);
}
}
/// <inheritdoc />
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
}
/// <inheritdoc />
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
if (lengthElements <= 0) {
lengthElements = data.Length - sourceOffsetElements;
}
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
// Validate buffer bounds
if (destinationOffsetBytes + dataSizeBytes > Size) {
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
}
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
fixed (T* ptr = data.Slice(sourceOffsetElements, lengthElements)) {
GL.BufferSubData(GLEnum.UniformBuffer, (nint)destinationOffsetBytes, (nuint)dataSizeBytes, ptr);
}
}
/// <summary>
/// Sets a single piece of data in the buffer.
/// </summary>
public unsafe void SetData<T>(ref T data) where T : unmanaged {
fixed (T* pData = &data) {
SetData(new Span<T>(pData, 1));
}
}
/// <summary>
/// Binds the buffer to the specified binding point.
/// </summary>
/// <param name="bindingPoint">The binding point to bind to</param>
public void Bind(uint bindingPoint) {
GL.BindBufferBase(GLEnum.UniformBuffer, bindingPoint, bufferId);
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public void Bind() {
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public void Unbind() {
GL.BindBuffer(GLEnum.UniformBuffer, 0);
GLHelpers.CheckErrors(GL);
}
public void Dispose() {
_device.QueueGLAction(GL => {
if (bufferId != 0) {
GL.DeleteBuffer(bufferId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
bufferId = 0;
}
});
}
/// <summary>
/// Releases an unpublished constructor-owned buffer synchronously on
/// the GL thread. This is deliberately separate from ordinary queued
/// disposal so an enclosing constructor can prove rollback before it
/// propagates its failure.
/// </summary>
internal void DisposeImmediately() {
if (bufferId == 0)
return;
RetryableGpuResourceRelease release =
TrackedGlResource.CreateRetryableBufferDeletion(
GL,
bufferId,
Size,
"rolling back unpublished managed uniform buffer");
release.Run();
bufferId = 0;
}
}
}

View file

@ -1,77 +0,0 @@
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Silk.NET.OpenGL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VertexAttribType = Silk.NET.OpenGL.VertexAttribType;
namespace AcDream.App.Rendering.Wb {
public unsafe class ManagedGLVertexArray : IVertexArray {
private readonly OpenGLGraphicsDevice _device;
private GL GL => _device.GL;
private uint _vaoId = 0;
public ManagedGLVertexArray(OpenGLGraphicsDevice device, IVertexBuffer buffer, VertexFormat format) {
_device = device;
// Generate the vertex array
_vaoId = GL.GenVertexArray();
GLHelpers.CheckErrors(GL);
if (_vaoId == 0) {
throw new Exception("Failed to generate vertex array.");
}
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO);
SetVertexBuffer(buffer, format);
}
public void SetVertexBuffer(IVertexBuffer buffer, VertexFormat format) {
GL.BindVertexArray(_vaoId);
GLHelpers.CheckErrors(GL);
buffer.Bind();
for (int i = 0; i < format.Attributes.Length; i++) {
var attr = format.Attributes[i];
GL.EnableVertexAttribArray((uint)i);
GLHelpers.CheckErrors(GL);
GL.VertexAttribPointer((uint)i, attr.Size, Convert(attr.Type), attr.Normalized, (uint)format.Stride, attr.Offset);
GLHelpers.CheckErrors(GL);
}
GL.BindVertexArray(0);
GLHelpers.CheckErrors(GL);
}
private GLEnum Convert(Chorizite.Core.Render.Enums.VertexAttribType type) => type switch {
Chorizite.Core.Render.Enums.VertexAttribType.Float => GLEnum.Float,
Chorizite.Core.Render.Enums.VertexAttribType.Int => GLEnum.Int,
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedInt => GLEnum.UnsignedInt,
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedByte => GLEnum.UnsignedByte,
Chorizite.Core.Render.Enums.VertexAttribType.Byte => GLEnum.Byte,
_ => throw new NotSupportedException()
};
public void Bind() {
GL.BindVertexArray(_vaoId);
GLHelpers.CheckErrors(GL);
}
public void Unbind() {
GL.BindVertexArray(0);
GLHelpers.CheckErrors(GL);
}
public void Dispose() {
_device.QueueGLAction(GL => {
if (_vaoId != 0) {
GL.DeleteVertexArray(_vaoId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
_vaoId = 0;
}
GLHelpers.CheckErrors(GL);
});
}
}
}

View file

@ -1,185 +0,0 @@
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
using System.Buffers;
using System.Runtime.InteropServices;
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// OpenGL vertex buffer
/// </summary>
public unsafe class ManagedGLVertexBuffer : IVertexBuffer {
private uint bufferId;
private readonly OpenGLGraphicsDevice _device;
private void* _mappedPtr;
private GL GL => _device.GL;
/// <inheritdoc />
public int Size { get; private set; }
/// <inheritdoc />
public BufferUsage Usage { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="ManagedGLVertexBuffer"/> class.
/// </summary>
/// <param name="usage">Buffer usage</param>
/// <param name="size">The size of the buffer, in bytes</param>
public unsafe ManagedGLVertexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
_device = device;
Size = size;
Usage = usage;
// Generate the buffer
bufferId = GL.GenBuffer();
if (bufferId == 0) {
throw new Exception("Failed to generate vertex buffer.");
}
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
// Allocate the buffer with the specified size but no initial data
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
if (_device.HasBufferStorage) {
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
GL.BufferStorage(GLEnum.ArrayBuffer, (uint)Size, (void*)0, flags);
_mappedPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
} else {
GL.BufferData(
GLEnum.ArrayBuffer,
(uint)Size,
(void*)0, // No initial data
Usage.ToGL());
}
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
}
/// <inheritdoc />
public unsafe void SetData<T>(T[] data) where T : IVertex {
SetData(data.AsSpan());
}
/// <inheritdoc />
public unsafe void SetData<T>(Span<T> data) where T : IVertex {
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
// Ensure the buffer size is sufficient
if (dataSize > Size) {
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
}
if (_mappedPtr != null) {
Span<T> mappedSpan = new Span<T>(_mappedPtr, data.Length);
data.CopyTo(mappedSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
// Map the buffer for writing
void* mappedPtr = GL.MapBufferRange(
GLEnum.ArrayBuffer,
0, // offset
dataSize,
MapBufferAccessMask.WriteBit | MapBufferAccessMask.InvalidateBufferBit // Overwrite entire buffer
);
if (mappedPtr == null) {
throw new Exception("Failed to map buffer for writing.");
}
try {
// Copy data directly to mapped memory
Span<T> mappedSpan = new Span<T>(mappedPtr, data.Length);
data.CopyTo(mappedSpan);
}
finally {
// Unmap the buffer
GL.UnmapBuffer(GLEnum.ArrayBuffer);
GLHelpers.CheckErrors(GL);
}
}
}
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
}
/// <inheritdoc />
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
if (Usage != BufferUsage.Dynamic) {
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
}
if (lengthElements <= 0) {
lengthElements = data.Length - sourceOffsetElements;
}
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
// Validate buffer bounds
if (destinationOffsetBytes + dataSizeBytes > Size) {
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
}
if (_mappedPtr != null) {
Span<T> mappedSpan = new Span<T>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
// Map the specific range of the buffer
void* mappedPtr = GL.MapBufferRange(
GLEnum.ArrayBuffer,
destinationOffsetBytes,
dataSizeBytes,
MapBufferAccessMask.WriteBit // Write access for partial update
);
if (mappedPtr == null) {
throw new Exception("Failed to map buffer for writing.");
}
try {
// Copy the specified range of data to the mapped memory
Span<T> mappedSpan = new Span<T>(mappedPtr, lengthElements);
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
}
finally {
// Unmap the buffer
GL.UnmapBuffer(GLEnum.ArrayBuffer);
GLHelpers.CheckErrors(GL);
}
}
}
public void Bind() {
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
GLHelpers.CheckErrors(GL);
}
public void Unbind() {
GL.BindBuffer(GLEnum.ArrayBuffer, 0);
GLHelpers.CheckErrors(GL);
}
public unsafe void Dispose() {
_device.QueueGLAction(GL => {
if (bufferId != 0) {
GL.DeleteBuffer(bufferId);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
bufferId = 0;
_mappedPtr = null;
}
});
}
}
}

View file

@ -1,6 +1,4 @@
using System.Runtime.InteropServices;
using DatReaderWriter.Enums;
using Chorizite.Core.Render;
namespace AcDream.App.Rendering.Wb {
/// <summary>
@ -19,17 +17,4 @@ namespace AcDream.App.Rendering.Wb {
public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags
}
public struct LandblockMdiCommand {
public ulong SortKey;
public ulong ObjectId;
public DrawElementsIndirectCommand Command;
public ModernBatchData BatchData;
public uint TextureIndex;
public ManagedGLTextureArray Atlas;
public uint VAO;
public uint IBO;
public bool IsTransparent;
public bool IsAdditive;
public bool HasWrappingUVs;
}
}

View file

@ -28,7 +28,17 @@ namespace AcDream.App.Rendering.Wb
/// </summary>
public class ObjectRenderData
{
/// <summary>
/// Campaign V slice V11 deleted the per-mesh raw-GL vertex array/buffer
/// this used to carry — Vulkan bakes vertex input into the pipeline and
/// the shared <see cref="GlobalMeshBuffer"/> arena has no VAO/VBO
/// concept at all (see <see cref="GlobalMeshBuffer.VertexStore"/>). This
/// is always 0 now; it survives only because
/// <c>WbDrawDispatcher.cs</c>'s legacy (non-RHI) dispatcher still reads
/// it into its own dead <c>anyVao</c> bookkeeping.
/// </summary>
public uint VAO { get; set; }
/// <summary>See <see cref="VAO"/> — always 0 for the same reason.</summary>
public uint VBO { get; set; }
public int VertexCount { get; set; }
public List<ObjectRenderBatch> Batches { get; set; } = new();
@ -76,6 +86,11 @@ namespace AcDream.App.Rendering.Wb
/// </summary>
public class ObjectRenderBatch
{
/// <summary>See <see cref="ObjectRenderData.VAO"/> — Campaign V slice
/// V11 deleted the legacy per-batch raw-GL index buffer this used to
/// carry. Always 0 now; every batch's actual index range lives in the
/// shared arena via <see cref="FirstIndex"/>/<see cref="BaseVertex"/>.
/// </summary>
public uint IBO { get; set; }
public int IndexCount { get; set; }
public TextureAtlasManager Atlas { get; set; } = null!;
@ -124,26 +139,6 @@ namespace AcDream.App.Rendering.Wb
/// </summary>
private readonly IMeshPipelineDevice _graphicsDevice;
/// <summary>
/// The GL context the LEGACY (pre-modern-path) upload bodies write
/// through.
///
/// <para>Campaign V slice V6i-3 narrowed what still needs it. The modern
/// path's arena upload is <see cref="GlobalMeshBuffer"/>'s, and that is
/// now <see cref="AcDream.App.Rendering.Gpu.IGpuBuffer"/> work on both
/// arms; what remains raw is the per-mesh VAO/VBO/IBO construction the
/// N.5 ship amendment made unreachable — missing bindless or
/// draw-parameters throws at startup, so <c>_useModernRendering</c> is
/// true in every shipping configuration. The accessor therefore survives
/// as the guard on genuinely dead code rather than as a blocker, and it
/// is deleted with that code.</para>
/// </summary>
private GL RequireGl() =>
_graphicsDevice.Gl
?? throw new InvalidOperationException(
"The mesh pipeline's legacy per-mesh vertex-array upload is raw GL and this "
+ "device has no context. The modern path is mandatory (N.5 ship amendment), "
+ "so reaching this is a composition error rather than a backend gap.");
private readonly IPreparedAssetSource _preparedAssets;
private readonly ILogger _logger;
@ -158,20 +153,6 @@ namespace AcDream.App.Rendering.Wb
/// </summary>
private readonly AcDream.App.Rendering.Gpu.IGpuDevice _gpuDevice;
/// <summary>
/// Campaign V slice V6i-2: the downcast moved here from the constructor.
/// Only the raw-GL world renderers reach this — the Vulkan backend binds
/// set 2 and never touches the handle table — so a Vulkan-composed mesh
/// pipeline can now be CONSTRUCTED, and only a caller that genuinely
/// needs a GL handle table fails, naming why.
/// </summary>
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
_gpuDevice as AcDream.App.Rendering.Gpu.Gl.GlGpuDevice
?? throw new InvalidOperationException(
"The GL bindless handle table was requested from a mesh pipeline composed against "
+ $"the {_gpuDevice.Backend} backend. It is GL-only emulation of the Vulkan texture "
+ "table and is deleted with the raw-GL world path.");
/// <summary>
/// Campaign V slice V6i-2: how a shared atlas's physical array is made.
/// Composed once; see <see cref="IWorldTextureArrayFactory"/>.
@ -339,7 +320,6 @@ namespace AcDream.App.Rendering.Wb
}
public GlobalMeshBuffer? GlobalBuffer { get; }
private readonly bool _useModernRendering;
internal (int RenderData, int AtlasArrays, int UnusedLru, long EstimatedBytes) Diagnostics
{
get
@ -527,11 +507,16 @@ namespace AcDream.App.Rendering.Wb
_stagedMeshData = new MeshUploadStagingQueue(
budgets.MeshStagingEntries,
budgets.MeshStagingBytes);
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
if (_useModernRendering)
// The modern path is mandatory (N.5 ship amendment) and Campaign V
// slice V11 deleted the only other backend, so a production
// IMeshPipelineDevice always reports both flags true. The gate
// survives because AcDream.App.Tests.Rendering.Wb.
// MeshPipelineDeviceSeamTests exercises a device that reports
// neither, to prove the arena is genuinely optional rather than
// dereferenced unconditionally.
if (_graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless)
{
GlobalBuffer = new GlobalMeshBuffer(
_graphicsDevice.Gl,
gpuDevice,
_graphicsDevice.ResourceRetirement);
}
@ -680,7 +665,7 @@ namespace AcDream.App.Rendering.Wb
/// <summary>
/// #105 diagnostic: counts staged-but-unflushed texture layer updates across all
/// shared atlases (see <see cref="ManagedGLTextureArray.PendingUpdateCount"/>).
/// shared atlases (see <see cref="IWorldTextureArray.PendingUpdateCount"/>).
/// Render thread only — <c>_globalAtlases</c> is render-thread-owned.
/// </summary>
public (int PendingUpdates, int ArraysWithPending, int TotalArrays) GetPendingTextureUpdateStats()
@ -901,7 +886,7 @@ namespace AcDream.App.Rendering.Wb
private long GetReclaimableBytes(ObjectRenderData data)
{
if (_useModernRendering && data.GlobalAllocation is { } allocation)
if (data.GlobalAllocation is { } allocation)
{
return checked(
(long)allocation.Vertices.Length * VertexPositionNormalTexture.Size
@ -1989,16 +1974,10 @@ namespace AcDream.App.Rendering.Wb
#region Private: GPU Upload
private unsafe ObjectRenderData? UploadGfxObjMeshData(ObjectMeshData meshData)
private ObjectRenderData? UploadGfxObjMeshData(ObjectMeshData meshData)
{
if (meshData.Vertices.Length == 0) return null;
// Resolved lazily since Campaign V slice V6i-3: every reader below
// is inside a !_useModernRendering branch, and the modern path is
// mandatory, so a backend with no GL context uploads meshes here
// without ever asking for one.
GL? gl = _graphicsDevice.Gl;
uint vao = 0, vbo = 0;
var modernIndexBatches = meshData.TextureBatches.Values
.SelectMany(batches => batches)
.Where(batch => batch.Indices.Count != 0)
@ -2007,62 +1986,23 @@ namespace AcDream.App.Rendering.Wb
GlobalMeshAllocation? globalAllocation = null;
var renderBatches = new List<ObjectRenderBatch>();
var acquiredTextures = new List<(TextureAtlasManager Atlas, TextureKey Key)>();
var legacyIndexBuffers = new List<(uint Name, int Bytes)>();
try
{
if (_useModernRendering)
{
// One mesh owns one vertex range and one contiguous index
// range. The former append path duplicated the full vertex
// array per material and never reclaimed evicted ranges.
vao = GlobalBuffer!.VAO;
vbo = GlobalBuffer!.VBO;
}
else
{
GL legacyGl = RequireGl();
legacyGl.GenVertexArrays(1, out vao);
legacyGl.BindVertexArray(vao);
legacyGl.GenBuffers(1, out vbo);
legacyGl.BindBuffer(GLEnum.ArrayBuffer, vbo);
fixed (VertexPositionNormalTexture* ptr = meshData.Vertices)
{
legacyGl.BufferData(GLEnum.ArrayBuffer, (nuint)(meshData.Vertices.Length * VertexPositionNormalTexture.Size), ptr, GLEnum.StaticDraw);
}
GpuMemoryTracker.TrackAllocation(meshData.Vertices.Length * VertexPositionNormalTexture.Size, GpuResourceType.Buffer);
int stride = VertexPositionNormalTexture.Size;
// Position (location 0)
legacyGl.EnableVertexAttribArray(0);
legacyGl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint)stride, (void*)0);
// Normal (location 1)
legacyGl.EnableVertexAttribArray(1);
legacyGl.VertexAttribPointer(1, 3, GLEnum.Float, false, (uint)stride, (void*)(3 * sizeof(float)));
// TexCoord (location 2)
legacyGl.EnableVertexAttribArray(2);
legacyGl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
// Instance data (shared VBO)
legacyGl.BindBuffer(GLEnum.ArrayBuffer, _graphicsDevice.InstanceVBO);
for (uint i = 0; i < 4; i++)
{
var loc = 3 + i;
legacyGl.EnableVertexAttribArray(loc);
legacyGl.VertexAttribPointer(loc, 4, GLEnum.Float, false, (uint)sizeof(InstanceData), (void*)(i * 16));
legacyGl.VertexAttribDivisor(loc, 1);
}
legacyGl.EnableVertexAttribArray(8);
legacyGl.VertexAttribIPointer(8, 1, GLEnum.UnsignedInt, (uint)sizeof(InstanceData), (void*)64);
legacyGl.VertexAttribDivisor(8, 1);
}
// Allocate the shared vertex/index range before acquiring texture
// references. A buffer-growth failure therefore leaves every atlas
// untouched; later failures still roll this allocation back below.
if (_useModernRendering && modernIndexBatches.Length != 0)
globalAllocation = GlobalBuffer!.UploadMesh(meshData.Vertices, modernIndexBatches);
//
// GlobalBuffer is null only for a test double that reports no
// modern-path capability (MeshPipelineDeviceSeamTests); every
// production IMeshPipelineDevice is Vulkan-backed and reports both
// flags true (N.5 ship amendment; Campaign V slice V11 deleted the
// only other backend). There is no longer a per-mesh vertex array
// or vertex/index buffer to build here — Vulkan bakes vertex input
// into the pipeline, and the shared arena's stores are bound once
// per pass (see WbDrawDispatcher.Rhi.cs's BindPipelineWithMesh).
if (GlobalBuffer is not null && modernIndexBatches.Length != 0)
globalAllocation = GlobalBuffer.UploadMesh(meshData.Vertices, modernIndexBatches);
foreach (var (format, batches) in meshData.TextureBatches)
{
@ -2070,7 +2010,6 @@ namespace AcDream.App.Rendering.Wb
{
if (batch.Indices.Count == 0) continue;
uint ibo = 0;
TextureAtlasManager? atlasManager = null;
int textureIndex = 0;
uint firstIndex = 0;
@ -2128,24 +2067,6 @@ namespace AcDream.App.Rendering.Wb
if (uploadsNewLayer)
_dirtyAtlases.Add(atlasManager);
if (_useModernRendering)
{
ibo = GlobalBuffer!.IBO;
}
else
{
GL legacyGl = RequireGl();
legacyGl.GenBuffers(1, out ibo);
legacyGl.BindBuffer(GLEnum.ElementArrayBuffer, ibo);
var indexArray = batch.Indices.ToArray();
fixed (ushort* iptr = indexArray)
{
legacyGl.BufferData(GLEnum.ElementArrayBuffer, (nuint)(indexArray.Length * sizeof(ushort)), iptr, GLEnum.StaticDraw);
}
GpuMemoryTracker.TrackAllocation(indexArray.Length * sizeof(ushort), GpuResourceType.Buffer);
legacyIndexBuffers.Add((ibo, indexArray.Length * sizeof(ushort)));
}
// Campaign V slice V4t interned the atlas's resident
// handle into the device's one texture table here and
// carried the slot. Slice V6i-2 asks the array for the
@ -2159,7 +2080,6 @@ namespace AcDream.App.Rendering.Wb
renderBatches.Add(new ObjectRenderBatch
{
IBO = ibo,
IndexCount = batch.Indices.Count,
Atlas = atlasManager!,
TextureIndex = textureIndex,
@ -2178,7 +2098,7 @@ namespace AcDream.App.Rendering.Wb
}
}
if (_useModernRendering && globalAllocation is not null)
if (globalAllocation is not null)
{
if (renderBatches.Count != globalAllocation.BatchFirstIndices.Count)
{
@ -2197,8 +2117,6 @@ namespace AcDream.App.Rendering.Wb
+ renderBatches.Sum(b => (long)b.IndexCount * sizeof(ushort)));
var renderData = new ObjectRenderData
{
VAO = vao,
VBO = vbo,
VertexCount = meshData.Vertices.Length,
Batches = renderBatches,
GlobalAllocation = globalAllocation,
@ -2209,26 +2127,17 @@ namespace AcDream.App.Rendering.Wb
CPUEdgeLines = meshData.EdgeLines,
MemorySize = geometryBytes,
NonArenaGpuBytes = CalculateNonArenaGeometryBytes(
_useModernRendering,
GlobalBuffer is not null,
geometryBytes),
};
if (!_useModernRendering)
{
RequireGl().BindVertexArray(0);
}
return renderData;
}
catch (Exception uploadFailure)
{
RetryableResourceReleaseLedger rollback = CreateUploadRollback(
meshData,
gl,
vao,
vbo,
globalAllocation,
acquiredTextures,
legacyIndexBuffers);
acquiredTextures);
ResourceReleaseAttempt attempt = rollback.Advance();
if (!rollback.IsComplete)
{
@ -2248,13 +2157,8 @@ namespace AcDream.App.Rendering.Wb
}
private RetryableResourceReleaseLedger CreateUploadRollback(
ObjectMeshData meshData,
GL? gl,
uint vao,
uint vbo,
GlobalMeshAllocation? globalAllocation,
IReadOnlyList<(TextureAtlasManager Atlas, TextureKey Key)> acquiredTextures,
IReadOnlyList<(uint Name, int Bytes)> legacyIndexBuffers)
IReadOnlyList<(TextureAtlasManager Atlas, TextureKey Key)> acquiredTextures)
{
var releases = new List<(string Name, Action Release)>();
@ -2282,35 +2186,6 @@ namespace AcDream.App.Rendering.Wb
acquiredTextures[releaseIndex].Key)));
}
if (!_useModernRendering)
{
GL legacyGl = gl ?? RequireGl();
for (int i = 0; i < legacyIndexBuffers.Count; i++)
{
int bufferIndex = i;
releases.Add((
$"legacy-index-buffer-{bufferIndex}-delete",
() => legacyGl.DeleteBuffer(legacyIndexBuffers[bufferIndex].Name)));
releases.Add((
$"legacy-index-buffer-{bufferIndex}-accounting",
() => GpuMemoryTracker.TrackDeallocation(
legacyIndexBuffers[bufferIndex].Bytes,
GpuResourceType.Buffer)));
}
if (vbo != 0)
{
releases.Add(("legacy-vertex-buffer-delete", () => legacyGl.DeleteBuffer(vbo)));
releases.Add((
"legacy-vertex-buffer-accounting",
() => GpuMemoryTracker.TrackDeallocation(
meshData.Vertices.Length * VertexPositionNormalTexture.Size,
GpuResourceType.Buffer)));
}
if (vao != 0)
releases.Add(("legacy-vertex-array-delete", () => legacyGl.DeleteVertexArray(vao)));
}
return new RetryableResourceReleaseLedger(releases);
}
@ -2447,48 +2322,14 @@ namespace AcDream.App.Rendering.Wb
return null;
var releases = new List<(string Name, Action Release)>();
if (_useModernRendering)
if (data.GlobalAllocation is { } allocation)
{
if (data.GlobalAllocation is { } allocation)
{
releases.Add((
"global-index-range",
() => GlobalBuffer!.ReleaseIndexRange(allocation)));
releases.Add((
"global-vertex-range",
() => GlobalBuffer!.ReleaseVertexRange(allocation)));
}
}
else
{
GL gl = RequireGl();
if (data.VAO != 0)
releases.Add(("legacy-vertex-array-delete", () => gl.DeleteVertexArray(data.VAO)));
if (data.VBO != 0)
{
releases.Add(("legacy-vertex-buffer-delete", () => gl.DeleteBuffer(data.VBO)));
releases.Add((
"legacy-vertex-buffer-accounting",
() => GpuMemoryTracker.TrackDeallocation(
data.VertexCount * VertexPositionNormalTexture.Size,
GpuResourceType.Buffer)));
}
for (int i = 0; i < data.Batches.Count; i++)
{
int batchIndex = i;
ObjectRenderBatch batch = data.Batches[batchIndex];
if (batch.IBO == 0)
continue;
releases.Add((
$"legacy-index-buffer-{batchIndex}-delete",
() => gl.DeleteBuffer(data.Batches[batchIndex].IBO)));
releases.Add((
$"legacy-index-buffer-{batchIndex}-accounting",
() => GpuMemoryTracker.TrackDeallocation(
data.Batches[batchIndex].IndexCount * sizeof(ushort),
GpuResourceType.Buffer)));
}
releases.Add((
"global-index-range",
() => GlobalBuffer!.ReleaseIndexRange(allocation)));
releases.Add((
"global-vertex-range",
() => GlobalBuffer!.ReleaseVertexRange(allocation)));
}
for (int i = 0; i < data.Batches.Count; i++)
@ -2841,7 +2682,7 @@ namespace AcDream.App.Rendering.Wb
"One or more texture atlases could not be disposed.",
failures!);
if (_useModernRendering && GlobalBuffer is not null)
if (GlobalBuffer is not null)
Capture(ref failures, GlobalBuffer.Dispose);
if (failures is not null)

View file

@ -1,776 +0,0 @@
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using AcDream.App.Rendering;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
using Silk.NET.OpenGL.Extensions.ARB;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using PolygonMode = Silk.NET.OpenGL.PolygonMode;
using PrimitiveType = Silk.NET.OpenGL.PrimitiveType;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// OpenGL graphics device
/// </summary>
public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice, IMeshPipelineDevice {
private readonly ILogger _log;
private readonly DebugRenderSettings _renderSettings;
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue _resourceRetirement;
public GL GL { get; }
public DebugRenderSettings RenderSettings => _renderSettings;
private readonly ConcurrentQueue<Action<GL>> _glThreadQueue = new();
private readonly ConcurrentQueue<Action<GL>> _nextGlThreadQueue = new();
internal bool HasPendingGLWork =>
!_glThreadQueue.IsEmpty || !_nextGlThreadQueue.IsEmpty;
// Campaign V slice V6i-2: IMeshPipelineDevice. Every member below already
// existed under a GL-specific name; these are aliases, not behaviour, so
// the shipping backend executes exactly the statements it executed
// before. See the interface for what the mesh pipeline actually needs
// and what still has to move before it has a second implementation.
GL? IMeshPipelineDevice.Gl => GL;
AcDream.App.Rendering.IGpuResourceRetirementQueue IMeshPipelineDevice.ResourceRetirement =>
_resourceRetirement;
bool IMeshPipelineDevice.HasPendingWork => HasPendingGLWork;
void IMeshPipelineDevice.ProcessQueue() => ProcessGLQueue();
public void QueueGLAction(Action<GL> action) {
_glThreadQueue.Enqueue(action);
}
internal void QueueGLActionForNextPass(Action<GL> action) {
ArgumentNullException.ThrowIfNull(action);
_nextGlThreadQueue.Enqueue(action);
}
public void ProcessGLQueue() {
// Retry the prior pass before ordinary work (notably sampler
// deletion), but process only the captured generation so a
// persistent driver failure cannot spin this frame forever.
int retryCount = _nextGlThreadQueue.Count;
for (int i = 0; i < retryCount && _nextGlThreadQueue.TryDequeue(out Action<GL>? retry); i++) {
try {
retry(GL);
} catch (Exception ex) {
_log.LogError(ex, "Error processing retryable GL queue action");
}
}
// Normal actions retain drain-to-empty semantics because teardown
// actions intentionally enqueue dependent atlas releases here.
// A persistent retryable error must not starve unrelated uploads
// and releases forever: the retry generation remains bounded to
// one attempt per pass, while ordinary work still makes progress.
while (_glThreadQueue.TryDequeue(out var action)) {
try {
action(GL);
} catch (Exception ex) {
_log.LogError(ex, "Error processing GL queue action");
}
}
}
public bool HasBindless { get; private set; }
public bool HasOpenGL43 { get; private set; }
public bool HasBufferStorage { get; private set; }
public bool HasTextureStorage { get; private set; }
public ArbBindlessTexture? BindlessExtension { get; private set; }
public uint InstanceVBO { get; private set; }
public void* InstanceVBOPtr { get; private set; }
public uint SharedQuadVBO { get; private set; }
public uint SharedDebugVAO { get; private set; }
public uint SharedDebugInstanceVBO { get; private set; }
/// <summary>OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs).</summary>
public uint WrapSampler { get; private set; }
/// <summary>OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs).</summary>
public uint ClampSampler { get; private set; }
internal float MaxSupportedAnisotropy { get; private set; }
private ManagedGLUniformBuffer? _sceneDataBuffer;
/// <summary>Shared SceneData UBO.</summary>
public ManagedGLUniformBuffer SceneDataBuffer => _sceneDataBuffer!;
private SceneData _currentSceneData;
public SceneData CurrentSceneData => _currentSceneData;
public void SetSceneData(ref SceneData data) {
_currentSceneData = data;
SceneDataBuffer.SetData(ref data);
}
private int _instanceBufferCapacity = 0;
private int _instanceBufferStride = 0;
/// <inheritdoc />
public override IntPtr NativeDevice { get; }
protected OpenGLGraphicsDevice() : base() {
_log = null!;
_renderSettings = null!;
_resourceRetirement = null!;
GL = null!;
}
public OpenGLGraphicsDevice(GL gl, ILogger log, DebugRenderSettings renderSettings, bool allowBindless = true)
: this(gl, log, renderSettings, AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance, allowBindless) {
}
internal OpenGLGraphicsDevice(
GL gl,
ILogger log,
DebugRenderSettings renderSettings,
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
bool allowBindless = true) : base() {
_log = log;
_renderSettings = renderSettings;
_resourceRetirement = resourceRetirement ?? throw new ArgumentNullException(nameof(resourceRetirement));
GL = gl;
GLHelpers.Init(this, log);
try {
GL.GetInteger(GLEnum.MajorVersion, out int major);
GL.GetInteger(GLEnum.MinorVersion, out int minor);
HasOpenGL43 = major > 4 || (major == 4 && minor >= 3);
HasTextureStorage = major > 4 || (major == 4 && minor >= 2) || GL.IsExtensionPresent("GL_ARB_texture_storage");
HasBufferStorage = major > 4 || (major == 4 && minor >= 4) || GL.IsExtensionPresent("GL_ARB_buffer_storage");
if (allowBindless && GL.TryGetExtension(out ArbBindlessTexture ext)) {
BindlessExtension = ext;
HasBindless = true;
} else {
HasBindless = false;
}
} catch {
HasOpenGL43 = false;
HasBindless = false;
}
var resources = new ResourceCleanupGroup();
try {
InstanceVBO = CreateConstructionBuffer(resources, "WB instance buffer");
// Query this immutable device limit once. Atlas construction can
// happen hundreds of times during portal streaming; repeating a
// driver GetFloat for every texture serialized the upload burst.
if (renderSettings.EnableAnisotropicFiltering) {
MaxSupportedAnisotropy = GlResourceCommand.Execute(
GL,
"query maximum texture anisotropy",
() => {
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
return Math.Max(0f, maxAniso);
});
}
WrapSampler = CreateConstructionSampler(
resources,
TextureWrapMode.Repeat,
"WB repeat sampler");
ClampSampler = CreateConstructionSampler(
resources,
TextureWrapMode.ClampToEdge,
"WB clamp sampler");
_sceneDataBuffer = new ManagedGLUniformBuffer(
this,
BufferUsage.Dynamic,
Marshal.SizeOf<SceneData>());
ManagedGLUniformBuffer ownedSceneDataBuffer = _sceneDataBuffer;
resources.Add(
"WB scene-data uniform buffer",
ownedSceneDataBuffer.DisposeImmediately);
InitializeSharedDebugResources(resources);
resources.TransferAll();
} catch (Exception constructionFailure) {
resources.RollbackConstructionAndThrow(
"OpenGLGraphicsDevice construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
}
}
/// <summary>
/// Retires a GL resource only after every submitted draw that could
/// reference it has completed on the GPU.
/// </summary>
internal void RetireGpuResource(Action release) => _resourceRetirement.Retire(release);
internal AcDream.App.Rendering.IGpuResourceRetirementQueue ResourceRetirement =>
_resourceRetirement;
private uint CreateConstructionBuffer(ResourceCleanupGroup resources, string name) {
uint buffer = GlResourceCommand.CreateName(GL, name, GL.GenBuffer, GL.DeleteBuffer);
resources.Add(
name,
() => GlResourceCommand.DeleteBuffer(
GL,
buffer,
$"delete {name} {buffer}"));
return buffer;
}
private uint CreateConstructionSampler(
ResourceCleanupGroup resources,
TextureWrapMode wrapMode,
string name) {
uint sampler = GlResourceCommand.CreateName(GL, name, GL.GenSampler, GL.DeleteSampler);
resources.Add(
name,
() => GlResourceCommand.Execute(
GL,
$"delete {name} {sampler}",
() => GL.DeleteSampler(sampler)));
GlResourceCommand.Execute(GL, $"configure {name}", () => {
GL.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrapMode);
GL.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrapMode);
GL.SamplerParameter(
sampler,
SamplerParameterI.MinFilter,
(int)TextureMinFilter.LinearMipmapLinear);
GL.SamplerParameter(
sampler,
SamplerParameterI.MagFilter,
(int)TextureMagFilter.Linear);
if (MaxSupportedAnisotropy > 0)
GL.SamplerParameter(
sampler,
GLEnum.TextureMaxAnisotropy,
MaxSupportedAnisotropy);
});
return sampler;
}
private void InitializeSharedDebugResources(ResourceCleanupGroup resources) {
// Unit quad vertices for two triangles (0 to 1 for length, -0.5 to 0.5 for thickness)
float[] quadVertices = {
0.0f, -0.5f,
1.0f, -0.5f,
1.0f, 0.5f,
0.0f, -0.5f,
1.0f, 0.5f,
0.0f, 0.5f
};
SharedQuadVBO = CreateConstructionBuffer(resources, "WB shared debug quad buffer");
SharedDebugInstanceVBO = CreateConstructionBuffer(
resources,
"WB shared debug instance buffer");
SharedDebugVAO = GlResourceCommand.CreateName(
GL,
"WB shared debug vertex array",
GL.GenVertexArray,
GL.DeleteVertexArray);
uint ownedDebugVao = SharedDebugVAO;
resources.Add(
"WB shared debug vertex array",
() => GlResourceCommand.DeleteVertexArray(
GL,
ownedDebugVao,
$"delete WB shared debug vertex array {ownedDebugVao}"));
GlResourceCommand.Execute(GL, "configure WB shared debug resources", () => {
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
fixed (float* pQuad = quadVertices) {
GL.BufferData(
GLEnum.ArrayBuffer,
(nuint)(quadVertices.Length * sizeof(float)),
pQuad,
GLEnum.StaticDraw);
}
// Initial capacity for debug instances.
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
GL.BufferData(
GLEnum.ArrayBuffer,
(nuint)(1024 * 44),
(void*)0,
GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance)
GL.BindVertexArray(SharedDebugVAO);
// Quad Pos attribute (location 0)
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
GL.EnableVertexAttribArray(0);
GL.VertexAttribPointer(0, 2, GLEnum.Float, false, 2 * sizeof(float), (void*)0);
// Instance attributes
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
uint lineInstanceSize = 44;
// aStart (location 1)
GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(1, 3, GLEnum.Float, false, lineInstanceSize, (void*)0);
GL.VertexAttribDivisor(1, 1);
// aEnd (location 2)
GL.EnableVertexAttribArray(2);
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12);
GL.VertexAttribDivisor(2, 1);
// aColor (location 3)
GL.EnableVertexAttribArray(3);
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24);
GL.VertexAttribDivisor(3, 1);
// aThickness (location 4)
GL.EnableVertexAttribArray(4);
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40);
GL.VertexAttribDivisor(4, 1);
GL.BindVertexArray(0);
});
}
public void EnsureInstanceBufferCapacity(int count, int stride, bool forceOrphan = false) {
if (count <= _instanceBufferCapacity && !forceOrphan) return;
if (_instanceBufferCapacity > 0) {
GpuMemoryTracker.TrackDeallocation(_instanceBufferCapacity * _instanceBufferStride);
}
_instanceBufferCapacity = Math.Max(count, 256);
_instanceBufferStride = stride;
if (HasBufferStorage) {
if (InstanceVBO != 0) {
GL.DeleteBuffer(InstanceVBO);
}
GL.GenBuffers(1, out uint instanceVbo);
InstanceVBO = instanceVbo;
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
GL.BufferStorage(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride), (void*)0, flags);
InstanceVBOPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)(_instanceBufferCapacity * _instanceBufferStride), MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride),
(void*)null, GLEnum.DynamicDraw);
InstanceVBOPtr = null;
}
GpuMemoryTracker.TrackAllocation(_instanceBufferCapacity * _instanceBufferStride);
}
public void UpdateInstanceBuffer<T>(List<T> data) where T : unmanaged {
EnsureInstanceBufferCapacity(data.Count, Marshal.SizeOf<T>(), true);
var span = CollectionsMarshal.AsSpan(data);
if (InstanceVBOPtr != null) {
var destSpan = new Span<T>(InstanceVBOPtr, data.Count);
span.CopyTo(destSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
fixed (T* ptr = span) {
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Count * Marshal.SizeOf<T>()), ptr);
}
}
}
public void UpdateInstanceBuffer<T>(Span<T> data) where T : unmanaged {
EnsureInstanceBufferCapacity(data.Length, Marshal.SizeOf<T>(), true);
if (InstanceVBOPtr != null) {
var destSpan = new Span<T>(InstanceVBOPtr, data.Length);
data.CopyTo(destSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
fixed (T* ptr = data) {
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Length * Marshal.SizeOf<T>()), ptr);
}
}
}
/// <inheritdoc />
public override void Clear(ColorVec color, ClearFlags flags, float depth, int stencil) {
GL.ClearColor(color.R, color.G, color.B, color.A);
GLHelpers.CheckErrors(GL);
GL.Clear((uint)Convert(flags));
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public override IIndexBuffer CreateIndexBuffer(int size,
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
return new ManagedGLIndexBuffer(this, usage, size);
}
/// <inheritdoc />
public override IVertexBuffer CreateVertexBuffer(int size,
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
return new ManagedGLVertexBuffer(this, usage, size);
}
/// <inheritdoc />
public override IVertexArray CreateArrayBuffer(IVertexBuffer vertexBuffer, VertexFormat format) {
return new ManagedGLVertexArray(this, vertexBuffer, format);
}
/// <inheritdoc />
public override void DrawElements(Chorizite.Core.Render.Enums.PrimitiveType type, int numElements, int indiceOffset = 0) {
GL.DrawElements(Convert(type), (uint)numElements, GLEnum.UnsignedInt, (void*)(indiceOffset * sizeof(uint)));
GLHelpers.CheckErrors(GL);
}
public override IShader CreateShader(string name, string vertexCode, string fragmentCode) {
var key = $"{GL.GetHashCode()}_{name}_{vertexCode.GetHashCode()}_{fragmentCode.GetHashCode()}";
while (true) {
if (_shaderCache.TryGetValue(key, out var existing)) {
if (existing is SharedShader shared && shared.TryIncrement()) {
return existing;
}
}
var inner = new GLSLShader(this, name, vertexCode, fragmentCode, _log);
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
if (_shaderCache.TryAdd(key, newShader)) {
return newShader;
}
// Someone else added it first, dispose ours and try again
newShader.DisposeInternal();
}
}
/// <inheritdoc />
public override IShader CreateShader(string name, string shaderDirectory) {
var key = $"{GL.GetHashCode()}_{name}";
while (true) {
if (_shaderCache.TryGetValue(key, out var existing)) {
if (existing is SharedShader shared && shared.TryIncrement()) {
return existing;
}
}
var inner = new GLSLShader(this, name, shaderDirectory, _log);
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
if (_shaderCache.TryAdd(key, newShader)) {
return newShader;
}
// Someone else added it first, dispose ours and try again
newShader.DisposeInternal();
}
}
private static readonly ConcurrentDictionary<string, IShader> _shaderCache = new();
private class SharedShader : IShader, IDisposable {
private readonly IShader _shader;
private readonly Action _onDispose;
private int _refCount = 1;
public string Name => _shader.Name;
public uint ProgramId => _shader.ProgramId;
public SharedShader(IShader shader, Action onDispose) {
_shader = shader;
_onDispose = onDispose;
}
public bool TryIncrement() {
while (true) {
int current = _refCount;
if (current <= 0) return false;
if (Interlocked.CompareExchange(ref _refCount, current + 1, current) == current) {
return true;
}
}
}
public void Bind() => _shader.Bind();
public void Unbind() => _shader.Unbind();
public void Load(string vertexSource, string fragmentSource) => _shader.Load(vertexSource, fragmentSource);
public void SetUniform(string name, int value) => _shader.SetUniform(name, value);
public void SetUniform(string name, float value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Vector2 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Vector3 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Vector4 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Matrix4x4 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, float[] values) => _shader.SetUniform(name, values);
public void DisposeInternal() {
_refCount = 0;
(_shader as IDisposable)?.Dispose();
}
public void Dispose() {
if (Interlocked.Decrement(ref _refCount) == 0) {
(_shader as IDisposable)?.Dispose();
_onDispose();
}
}
}
/// <inheritdoc />
public override ITexture
CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data = null) {
if (format != TextureFormat.RGBA8) {
throw new NotImplementedException($"Texture format {format} is not supported.");
}
return new ManagedGLTexture(this, data, width, height);
}
/// <summary>
/// Creates a texture with custom texture parameters.
/// </summary>
public ITexture CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data, TextureParameters texParams) {
if (format != TextureFormat.RGBA8) {
throw new NotImplementedException($"Texture format {format} is not supported.");
}
return new ManagedGLTexture(this, data, width, height, texParams);
}
/// <inheritdoc />
public override ITexture? CreateTextureInternal(TextureFormat format, string filename) {
if (format != TextureFormat.RGBA8) {
throw new NotImplementedException($"Texture format {format} is not supported.");
}
return new ManagedGLTexture(this, filename);
}
/// <inheritdoc />
public override ITextureArray
CreateTextureArrayInternal(TextureFormat format, int width, int height, int size) {
return new ManagedGLTextureArray(this, format, width, height, size, _log);
}
/// <summary>
/// Creates a texture array with custom texture parameters.
/// </summary>
public ITextureArray CreateTextureArrayInternal(TextureFormat format, int width, int height, int size, TextureParameters texParams) {
return new ManagedGLTextureArray(this, format, width, height, size, _log, texParams);
}
/// <inheritdoc />
public override void BeginFrame() {
GL.Viewport(Viewport.X, Viewport.Y, (uint)Viewport.Width, (uint)Viewport.Height);
GLHelpers.CheckErrors(GL);
GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public override void EndFrame() {
}
/// <inheritdoc />
protected override void SetRenderStateInternal(RenderState state, bool enabled) {
switch (state) {
case RenderState.AlphaBlend:
if (enabled) GL.Enable(EnableCap.Blend);
else GL.Disable(EnableCap.Blend);
GLHelpers.CheckErrors(GL);
break;
case RenderState.DepthTest:
if (enabled) GL.Enable(EnableCap.DepthTest);
else GL.Disable(EnableCap.DepthTest);
GLHelpers.CheckErrors(GL);
break;
case RenderState.ScissorTest:
if (enabled) GL.Enable(EnableCap.ScissorTest);
else GL.Disable(EnableCap.ScissorTest);
GLHelpers.CheckErrors(GL);
break;
case RenderState.DepthWrite:
if (enabled) GL.DepthMask(true);
else GL.DepthMask(false);
GLHelpers.CheckErrors(GL);
break;
case RenderState.Fog:
break;
case RenderState.Lighting:
break;
}
}
/// <inheritdoc />
protected override void SetBlendFactorInternal(BlendFactor srcBlendFactor, BlendFactor dstBlendFactor) {
GL.BlendFunc(Convert(srcBlendFactor), Convert(dstBlendFactor));
GLHelpers.CheckErrors(GL);
}
protected override void SetScissorRectInternal(Rectangle scissor) {
var gtop = (int)Viewport.Height - scissor.Y - scissor.Height;
GL.Scissor(scissor.X, gtop, (uint)scissor.Width, (uint)scissor.Height);
GLHelpers.CheckErrors(GL);
}
protected override void SetViewportInternal(Rectangle viewport) {
GL.Viewport(viewport.X, viewport.Y, (uint)viewport.Width, (uint)viewport.Height);
GLHelpers.CheckErrors(GL);
}
protected override void SetPolygonModeInternal(Chorizite.Core.Render.Enums.PolygonMode polygonMode) {
GL.PolygonMode(GLEnum.FrontAndBack, Convert(polygonMode));
GLHelpers.CheckErrors(GL);
}
protected override void SetCullModeInternal(CullMode cullMode) {
switch (cullMode) {
case CullMode.None:
GL.Disable(EnableCap.CullFace);
break;
case CullMode.Front:
GL.Enable(EnableCap.CullFace);
GL.CullFace(GLEnum.Front);
break;
case CullMode.Back:
GL.Enable(EnableCap.CullFace);
GL.CullFace(GLEnum.Back);
break;
}
}
private GLEnum Convert(Chorizite.Core.Render.Enums.PolygonMode mode) {
switch (mode) {
case Chorizite.Core.Render.Enums.PolygonMode.Fill:
return GLEnum.Fill;
case Chorizite.Core.Render.Enums.PolygonMode.Line:
return GLEnum.Line;
case Chorizite.Core.Render.Enums.PolygonMode.Point:
return GLEnum.Point;
default:
return GLEnum.Fill;
}
}
private GLEnum Convert(ClearFlags flags) {
GLEnum mask = 0;
if ((flags & ClearFlags.Color) == ClearFlags.Color) mask |= GLEnum.ColorBufferBit;
if ((flags & ClearFlags.Depth) == ClearFlags.Depth) mask |= GLEnum.DepthBufferBit;
if ((flags & ClearFlags.Stencil) == ClearFlags.Stencil) mask |= GLEnum.StencilBufferBit;
return mask;
}
private GLEnum Convert(BlendFactor factor) {
switch (factor) {
case BlendFactor.One:
return GLEnum.One;
case BlendFactor.SrcAlpha:
return GLEnum.SrcAlpha;
case BlendFactor.OneMinusSrcAlpha:
return GLEnum.OneMinusSrcAlpha;
case BlendFactor.DstAlpha:
return GLEnum.DstAlpha;
case BlendFactor.OneMinusDstAlpha:
return GLEnum.OneMinusDstAlpha;
default:
return GLEnum.One;
}
}
private PrimitiveType Convert(Chorizite.Core.Render.Enums.PrimitiveType type) {
switch (type) {
case Chorizite.Core.Render.Enums.PrimitiveType.PointList:
return PrimitiveType.Points;
case Chorizite.Core.Render.Enums.PrimitiveType.LineList:
return PrimitiveType.Lines;
case Chorizite.Core.Render.Enums.PrimitiveType.LineStrip:
return PrimitiveType.LineStrip;
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleList:
return PrimitiveType.Triangles;
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleStrip:
return PrimitiveType.TriangleStrip;
default:
throw new NotImplementedException($"Primitive type {type} is not supported.");
}
}
/// <inheritdoc />
public override IFramebuffer CreateFramebuffer(ITexture texture, int width, int height,
bool hasDepthStencil = true) {
if (texture == null) {
throw new ArgumentNullException(nameof(texture));
}
if (width <= 0 || height <= 0) {
throw new ArgumentException("Width and height must be positive.");
}
return new ManagedGLFramebuffer(this, texture, width, height, hasDepthStencil);
}
/// <inheritdoc />
public override void BindFramebuffer(IFramebuffer? framebuffer) {
uint fboId = framebuffer != null ? (uint)framebuffer.NativeHandle.ToInt32() : 0;
GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId);
}
/// <inheritdoc />
public override void Dispose() {
var instanceVBO = InstanceVBO;
var instanceBufferCapacity = _instanceBufferCapacity;
var instanceBufferStride = _instanceBufferStride;
var wrapSampler = WrapSampler;
var clampSampler = ClampSampler;
var sharedQuadVbo = SharedQuadVBO;
var sharedDebugInstanceVbo = SharedDebugInstanceVBO;
var sharedDebugVao = SharedDebugVAO;
QueueGLAction(gl => {
if (sharedQuadVbo != 0) gl.DeleteBuffer(sharedQuadVbo);
if (sharedDebugInstanceVbo != 0) gl.DeleteBuffer(sharedDebugInstanceVbo);
if (sharedDebugVao != 0) gl.DeleteVertexArray(sharedDebugVao);
if (instanceVBO != 0) {
gl.DeleteBuffer(instanceVBO);
if (instanceBufferCapacity > 0) {
GpuMemoryTracker.TrackDeallocation(instanceBufferCapacity * instanceBufferStride);
}
}
});
// Bindless texture-array retirements embed these samplers in their
// resident handles. Ordinary GL work must keep flowing when one
// retry is sick, but sampler deletion itself is dependency-ordered
// behind the retry queue. Requeue into the next generation (never
// the drain-to-empty ordinary queue) to remain one attempt/frame.
Action<GL>? deleteSamplersWhenSafe = null;
deleteSamplersWhenSafe = gl => {
if (!_nextGlThreadQueue.IsEmpty) {
QueueGLActionForNextPass(deleteSamplersWhenSafe!);
return;
}
if (wrapSampler != 0)
gl.DeleteSampler(wrapSampler);
if (clampSampler != 0)
gl.DeleteSampler(clampSampler);
};
QueueGLActionForNextPass(deleteSamplersWhenSafe);
InstanceVBO = 0;
InstanceVBOPtr = null;
WrapSampler = 0;
ClampSampler = 0;
_sceneDataBuffer?.Dispose();
_sceneDataBuffer = null;
}
public override IUniformBuffer CreateUniformBuffer(BufferUsage usage, int size) {
return (IUniformBuffer)new ManagedGLUniformBuffer(this, usage, size);
}
}
}

View file

@ -1,26 +0,0 @@
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Tracks currently-bound GL state to skip redundant rebinds across the
/// WB-derived render path. Previously these were static fields on
/// <c>BaseObjectRenderManager</c> in the WorldBuilder.Shared project; inlined
/// here in Phase O-T7 to eliminate the WorldBuilder project reference.
///
/// Semantics are identical to the WB originals:
/// <c>CurrentAtlas</c> — slot index of the currently bound texture atlas.
/// <c>CurrentVAO</c> — OpenGL name of the currently bound vertex array object.
/// <c>CurrentIBO</c> — OpenGL name of the currently bound index buffer object.
/// Sentinel value 0 means "no valid binding cached."
/// </summary>
/// <remarks>
/// All accesses must occur on the render thread. GL state binding is
/// not thread-safe; these sentinels are written immediately after the
/// corresponding glBind* call and read by the next dispatch on the
/// same thread.
/// </remarks>
public static class RenderStateCache
{
public static uint CurrentAtlas = 0;
public static uint CurrentVAO = 0;
public static uint CurrentIBO = 0;
}

View file

@ -1,283 +0,0 @@
using Silk.NET.OpenGL;
using AcDream.App.Rendering;
using System.Runtime.ExceptionServices;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Always-on transaction boundary and accounting for raw dynamic GL objects.
/// Growth keeps the published CPU capacity unchanged until BufferData succeeds;
/// OpenGL leaves the previous data store intact when allocation reports an
/// error, so the caller can continue using the old capacity or unwind.
/// </summary>
internal static unsafe class TrackedGlResource
{
public static RetryableGpuResourceRelease CreateRetryableBufferDeletion(
GL gl,
uint buffer,
long capacityBytes,
string context) =>
CreateRetryableBufferDeletion(
gl,
buffer,
() => capacityBytes,
context);
public static RetryableGpuResourceRelease CreateRetryableBufferDeletion(
GL gl,
uint buffer,
Func<long> capacityBytes,
string context)
{
if (buffer == 0)
throw new ArgumentOutOfRangeException(nameof(buffer));
ArgumentNullException.ThrowIfNull(capacityBytes);
return new RetryableGpuResourceRelease(
() => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"),
() =>
{
gl.DeleteBuffer(buffer);
// Per the GL error contract, a command which generates an
// error does not change object state. Keep mutation and its
// validation in one retryable stage so that failed deletion
// is issued again, while later accounting remains untouched.
GLHelpers.ThrowOnResourceError(gl, context);
},
() =>
{
long bytes = capacityBytes();
ArgumentOutOfRangeException.ThrowIfNegative(bytes);
if (bytes != 0)
GpuMemoryTracker.TrackDeallocation(bytes, GpuResourceType.Buffer);
},
() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer));
}
public static RetryableGpuResourceRelease CreateRetryableVertexArrayDeletion(
GL gl,
uint vertexArray,
string context,
Action? trackDeallocation = null)
{
if (vertexArray == 0)
throw new ArgumentOutOfRangeException(nameof(vertexArray));
return new RetryableGpuResourceRelease(
() => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"),
() =>
{
gl.DeleteVertexArray(vertexArray);
GLHelpers.ThrowOnResourceError(gl, context);
},
trackDeallocation
?? (() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO)));
}
public static void AllocateBufferStorage(
GL gl,
BufferTargetARB target,
uint buffer,
long previousBytes,
long newBytes,
BufferUsageARB usage,
string context)
=> AllocateBufferStorage(
gl,
(GLEnum)target,
buffer,
previousBytes,
newBytes,
(GLEnum)usage,
context);
public static void AllocateBufferStorage(
GL gl,
BufferTargetARB target,
uint buffer,
long previousBytes,
long newBytes,
BufferUsageARB usage,
void* data,
string context)
=> AllocateBufferStorage(
gl,
(GLEnum)target,
buffer,
previousBytes,
newBytes,
(GLEnum)usage,
data,
context);
public static uint CreateBuffer(GL gl, string context)
{
return CreateTrackedName(
gl,
"buffer",
context,
gl.GenBuffer,
name => GlResourceCommand.DeleteBuffer(
gl,
name,
$"rollback buffer {name} after failed {context}"),
() => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer));
}
public static uint CreateVertexArray(GL gl, string context)
{
return CreateTrackedName(
gl,
"vertex-array",
context,
gl.GenVertexArray,
name => GlResourceCommand.DeleteVertexArray(
gl,
name,
$"rollback vertex array {name} after failed {context}"),
() => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO));
}
private static uint CreateTrackedName(
GL gl,
string resourceName,
string context,
Func<uint> create,
Action<uint> rollback,
Action publishAccounting)
{
uint name = GlResourceCommand.CreateName(
gl,
$"{resourceName} for {context}",
create,
rollback);
var cleanup = new ResourceCleanupGroup();
cleanup.Add($"{resourceName} name {name}", () => rollback(name));
try
{
publishAccounting();
return name;
}
catch (Exception publicationFailure)
{
try
{
cleanup.RetryCleanup();
}
catch (Exception cleanupFailure)
{
throw new GlResourceConstructionException(
$"Publishing {resourceName} accounting failed and GL name {name} could not be released.",
cleanup,
[publicationFailure, cleanupFailure]);
}
ExceptionDispatchInfo.Capture(publicationFailure).Throw();
throw new InvalidOperationException("Unreachable resource-publication path.");
}
}
public static void AllocateBufferStorage(
GL gl,
GLEnum target,
uint buffer,
long previousBytes,
long newBytes,
GLEnum usage,
string context)
=> AllocateBufferStorage(
gl,
target,
buffer,
previousBytes,
newBytes,
usage,
null,
context);
public static void AllocateBufferStorage(
GL gl,
GLEnum target,
uint buffer,
long previousBytes,
long newBytes,
GLEnum usage,
void* data,
string context)
{
ArgumentOutOfRangeException.ThrowIfNegative(previousBytes);
ArgumentOutOfRangeException.ThrowIfLessThan(newBytes, 1);
if (buffer == 0)
throw new ArgumentOutOfRangeException(nameof(buffer));
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
gl.BindBuffer(target, buffer);
gl.BufferData(target, checked((nuint)newBytes), data, usage);
GLHelpers.ThrowOnResourceError(gl, context);
long delta = checked(newBytes - previousBytes);
if (delta > 0)
GpuMemoryTracker.TrackAllocation(delta, GpuResourceType.Buffer);
else if (delta < 0)
GpuMemoryTracker.TrackDeallocation(-delta, GpuResourceType.Buffer);
}
public static void UpdateBufferSubData(
GL gl,
BufferTargetARB target,
uint buffer,
nint byteOffset,
long byteCount,
void* data,
string context)
=> UpdateBufferSubData(
gl,
(GLEnum)target,
buffer,
byteOffset,
byteCount,
data,
context);
public static void UpdateBufferSubData(
GL gl,
GLEnum target,
uint buffer,
nint byteOffset,
long byteCount,
void* data,
string context)
{
ArgumentOutOfRangeException.ThrowIfNegative(byteOffset);
ArgumentOutOfRangeException.ThrowIfLessThan(byteCount, 1);
if (buffer == 0)
throw new ArgumentOutOfRangeException(nameof(buffer));
ArgumentNullException.ThrowIfNull(data);
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
gl.BindBuffer(target, buffer);
gl.BufferSubData(target, byteOffset, checked((nuint)byteCount), data);
GLHelpers.ThrowOnResourceError(gl, context);
}
public static void DeleteBuffer(GL gl, uint buffer, long capacityBytes, string context)
{
if (buffer == 0)
return;
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
gl.DeleteBuffer(buffer);
GLHelpers.ThrowOnResourceError(gl, context);
if (capacityBytes != 0)
GpuMemoryTracker.TrackDeallocation(capacityBytes, GpuResourceType.Buffer);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
}
public static void DeleteVertexArray(GL gl, uint vertexArray, string context)
{
if (vertexArray == 0)
return;
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
gl.DeleteVertexArray(vertexArray);
GLHelpers.ThrowOnResourceError(gl, context);
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
}
}

File diff suppressed because it is too large Load diff

View file

@ -200,39 +200,15 @@ public sealed class WbMeshAdapter
{
// Campaign V slice V6i-3: WHICH mesh-pipeline device is decided
// here, once, and it is the only place in the mesh pipeline that
// names a backend. The GL arm is unchanged — same construction,
// same queue-drain guarantee on rollback. A backend with no context
// gets the RHI arm, whose queue is empty by construction because
// Vulkan resource work is recorded or retirement-queued rather than
// deferred onto a context-owning thread.
if (gl is { } context)
{
var openGl = new OpenGLGraphicsDevice(
context,
logger,
new DebugRenderSettings(),
resourceRetirement);
graphicsDevice = openGl;
var graphicsDeviceRelease = new RetryableGpuResourceRelease(
openGl.Dispose,
() =>
{
openGl.ProcessGLQueue();
if (openGl.HasPendingGLWork)
{
throw new InvalidOperationException(
"WB graphics-device construction cleanup still has queued GL work.");
}
});
resources.Add("WB graphics device", graphicsDeviceRelease.Run);
}
else
{
var rhiDevice = new AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice(
resourceRetirement);
graphicsDevice = rhiDevice;
resources.Add("WB graphics device", rhiDevice.Dispose);
}
// names a backend. The raw-GL arm (OpenGLGraphicsDevice) was
// deleted at Campaign V slice V11; the RHI arm's queue is empty by
// construction because Vulkan resource work is recorded or
// retirement-queued rather than deferred onto a context-owning
// thread.
var rhiDevice = new AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice(
resourceRetirement);
graphicsDevice = rhiDevice;
resources.Add("WB graphics device", rhiDevice.Dispose);
if (resolvedPreparedAssets is null)
{
resolvedPreparedAssets = new DatPreparedAssetSource(
@ -267,19 +243,6 @@ public sealed class WbMeshAdapter
: null;
}
/// <summary>
/// Campaign V slice V4t: the GL device whose texture table every mesh
/// batch's <c>GpuTextureSlot</c> indexes. The world renderers this adapter
/// feeds flush and bind that table before their raw-GL draws, and taking it
/// from here rather than from a second composition wire is what makes "the
/// slot and the table came from the same device" true by construction.
/// </summary>
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
(_meshManager
?? throw new InvalidOperationException(
"An initialized mesh adapter is required for the world texture table."))
.WorldTextureTable;
internal void RegisterResidencySources(ResidencyManager manager)
{
ArgumentNullException.ThrowIfNull(manager);

View file

@ -1,6 +1,6 @@
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Gl;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.Core.Rendering.Wb;
using Chorizite.Core.Render.Enums;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
@ -15,21 +15,21 @@ namespace AcDream.App.Rendering.Wb;
/// deliberately left CREATION with the caches — plan §5.5.11 records why, and
/// §5.5.12 item 1 hands the remainder forward: "the missing piece is an
/// <c>ITextureArray</c> implementation over <see cref="IGpuTexture"/>, not a
/// codec." This is that interface. <see cref="ManagedGLTextureArray"/> and
/// <see cref="RhiWorldTextureArray"/> implement it, and which one exists is
/// decided once at composition by <see cref="IWorldTextureArrayFactory"/> —
/// never per call, so the GL path executes exactly the statements it executed
/// before.</para>
/// codec." This is that interface. <c>ManagedGLTextureArray</c> used to be its
/// GL implementation, alongside <see cref="RhiWorldTextureArray"/>; which one
/// existed was decided once at composition by
/// <see cref="IWorldTextureArrayFactory"/>, never per call. Campaign V slice
/// V11 deleted <c>ManagedGLTextureArray</c> along with the rest of the raw-GL
/// arm, so <see cref="RhiWorldTextureArray"/> is now the sole implementation.</para>
///
/// <para><b>The slot, not the handle, is the seam.</b> Before this slice
/// <para><b>The slot, not the handle, is the seam.</b> Before V6i-2
/// <c>ObjectMeshManager</c> read <c>BindlessWrapHandle</c>/
/// <c>BindlessClampHandle</c> off the concrete GL array and interned them into
/// the device table itself. A 64-bit <c>ARB_bindless_texture</c> handle is
/// unspellable on Vulkan, so the array now answers the question the caller was
/// really asking — <see cref="ResolveSlot"/> — and each implementation gets
/// there its own way: the GL array interns its resident handle (the same
/// idempotent call, one level down), while the RHI array registered its two
/// (texture, sampler) pairs at construction and returns a field.</para>
/// unspellable on Vulkan, so the array answers the question the caller was
/// really asking — <see cref="ResolveSlot"/> — instead: the RHI array
/// registered its two (texture, sampler) pairs at construction and returns a
/// field.</para>
/// </summary>
internal interface IWorldTextureArray : IDisposable
{
@ -116,9 +116,9 @@ internal interface IWorldTextureArrayFactory
ArgumentNullException.ThrowIfNull(graphicsDevice);
ArgumentNullException.ThrowIfNull(gpuDevice);
ArgumentNullException.ThrowIfNull(logger);
return graphicsDevice is OpenGLGraphicsDevice gl && gpuDevice is GlGpuDevice table
? new GlWorldTextureArrayFactory(gl, table, logger)
: new RhiWorldTextureArrayFactory(gpuDevice);
// The GL arm this used to select between was deleted at Campaign V
// slice V11; the RHI arm is the only one left.
return new RhiWorldTextureArrayFactory(gpuDevice);
}
/// <summary>The retirement queue array layers and images are released through.</summary>
@ -132,36 +132,6 @@ internal interface IWorldTextureArrayFactory
IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers);
}
/// <summary>
/// The GL arm. Delegates to the same <c>OpenGLGraphicsDevice</c> entry point
/// <see cref="TextureAtlasManager"/> called directly before this slice, so the
/// shipping backend's construction is textually unchanged.
/// </summary>
internal sealed class GlWorldTextureArrayFactory(
OpenGLGraphicsDevice graphicsDevice,
GlGpuDevice worldTextureTable,
ILogger logger) : IWorldTextureArrayFactory
{
private readonly OpenGLGraphicsDevice _graphicsDevice = graphicsDevice
?? throw new ArgumentNullException(nameof(graphicsDevice));
private readonly GlGpuDevice _worldTextureTable = worldTextureTable
?? throw new ArgumentNullException(nameof(worldTextureTable));
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
public IGpuResourceRetirementQueue Retirement => _graphicsDevice.ResourceRetirement;
public IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers) =>
new ManagedGLTextureArray(
_graphicsDevice,
format,
width,
height,
layers,
_logger,
_worldTextureTable,
TextureParameters.ClampToEdge);
}
/// <summary>
/// The backend-neutral arm. Creates through <see cref="IGpuDevice.CreateTexture"/>
/// and registers both address modes into the device's one texture table, so an
@ -353,11 +323,7 @@ internal sealed class RhiWorldTextureArray : IWorldTextureArray
ArgumentNullException.ThrowIfNull(data);
ArgumentOutOfRangeException.ThrowIfNegative(layer);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(layer, Size);
// The GL array validates the payload against the format's expected byte
// count and rejects transfer overrides that contradict it. Reusing that
// validator rather than writing a second one keeps the two arms agreeing
// on what a well-formed layer is.
ManagedGLTextureArray.ValidateUploadPayload(
ValidateUploadPayload(
SourceFormat,
_width,
_height,
@ -504,4 +470,65 @@ internal sealed class RhiWorldTextureArray : IWorldTextureArray
+ "and is not part of GpuTextureDescription. Campaign V's world-draw slice owns "
+ "extending the contract or proving no such atlas exists."),
};
private static bool IsCompressedFormat(TextureFormat format) =>
format is TextureFormat.DXT1 or TextureFormat.DXT3 or TextureFormat.DXT5;
/// <summary>
/// The expected byte count for one uploaded layer of <paramref name="format"/>
/// at <paramref name="width"/>x<paramref name="height"/>.
/// </summary>
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}"),
};
}
/// <summary>
/// Validates an upload payload against the format's expected byte count and
/// rejects transfer overrides that contradict it.
/// </summary>
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}.");
}
}
}