diff --git a/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs b/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs
index 9265c1f2..ab545721 100644
--- a/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs
+++ b/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs
@@ -16,7 +16,6 @@ using AcDream.Runtime;
using AcDream.Runtime.Gameplay;
using DatReaderWriter;
using Silk.NET.Input;
-using Silk.NET.OpenGL;
namespace AcDream.App.Composition;
diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs
index 0e56982c..7248f712 100644
--- a/src/AcDream.App/Composition/FrameRootComposition.cs
+++ b/src/AcDream.App/Composition/FrameRootComposition.cs
@@ -15,7 +15,6 @@ using AcDream.Core.Physics;
using AcDream.Core.Selection;
using AcDream.Core.World;
using Silk.NET.Input;
-using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace AcDream.App.Composition;
@@ -236,20 +235,15 @@ internal sealed class FrameRootCompositionPhase
{
FrameRootDependencies d = _dependencies;
WorldRenderFoundation foundation = world.Foundation;
- // Campaign V slice V6h: the frame root is the one seam that genuinely
- // forks. On GL every world renderer exists and the render graph is
- // unchanged. On Vulkan none of them does, so the graph is the clear pass,
- // the private-presentation phase, and the retained UI inside it — the
- // client's own frame, drawn entirely through the RHI.
- GL? gl = d.Graphics.Gl;
+ // Campaign V slice V6h: the frame root's raw-GL render graph fork was
+ // deleted at slice V11. The graph is the clear pass, the private-
+ // presentation phase, and the retained UI inside it — the client's
+ // own frame, drawn entirely through the RHI.
var teleportRenderState =
new LocalPlayerTeleportRenderStateSource(session.LocalTeleport);
var renderLoginState = new RenderLoginStateSource(
d.Options.LiveMode,
d.PlayerMode);
- RenderFrameGlStateController? renderFrameGlState = gl is null
- ? null
- : new RenderFrameGlStateController(new SilkRenderFrameGlStateApi(gl));
// Campaign V slice V6i-3: on Vulkan the frame's clear is a load op of the
// world pass rather than a pass of its own, so the two phases share this
// one value. See VulkanWorldScenePhase for why the merge is required
@@ -268,18 +262,8 @@ internal sealed class FrameRootCompositionPhase
live.ParticleRenderer,
d.FrameProfiler,
d.FrameDiagnosticsEnabled);
- IRenderFrameClearPhase clearPhase = gl is not null
- ? new RuntimeRenderFrameClearPhase(
- gl,
- d.WorldTime,
- d.Weather,
- teleportRenderState,
- d.ParticleVisibility,
- host.WorldRenderDiagnostics
- ?? throw new InvalidOperationException(
- "The GL frame root requires the GL state tripwire."),
- renderFrameGlState!)
- : new AcDream.App.Rendering.Gpu.Vk.VulkanRenderFrameClearPhase(
+ IRenderFrameClearPhase clearPhase =
+ new AcDream.App.Rendering.Gpu.Vk.VulkanRenderFrameClearPhase(
d.WorldTime,
d.Weather,
teleportRenderState,
@@ -335,27 +319,17 @@ internal sealed class FrameRootCompositionPhase
?? new WorldRenderDiagnostics(
NullRenderGlStateReader.Instance,
d.RenderDiagnosticLog);
- IRenderFrameGlState worldFrameGlState =
- (IRenderFrameGlState?)renderFrameGlState
- ?? NullRenderFrameGlState.Instance;
+ IRenderFrameGlState worldFrameGlState = NullRenderFrameGlState.Instance;
IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope;
var worldFramebufferSource =
new SilkRetailPViewFramebufferSource(d.Window);
- IWorldPassSurface worldPassSurface = gl is not null
- ? new GlWorldPassSurface(
- gl,
- live.ClipFrame,
- worldFramebufferSource,
- live.DrawDispatcher!,
- live.EnvCellRenderer!,
- foundation.Terrain)
- : new RhiWorldPassSurface(
- worldPassScope
- ?? throw new InvalidOperationException(
- "A backend without a GL context must publish a world pass scope."),
- host.GpuFrameLifetime,
- live.ClipFrame,
- worldFramebufferSource);
+ IWorldPassSurface worldPassSurface = new RhiWorldPassSurface(
+ worldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope."),
+ host.GpuFrameLifetime,
+ live.ClipFrame,
+ worldFramebufferSource);
var worldFrameEnvironment =
new RuntimeWorldFrameEnvironmentPreparation(
d.Options,
@@ -438,7 +412,7 @@ internal sealed class FrameRootCompositionPhase
// V11, so nothing is lost — composing it would throw on the
// first wireframe frame rather than silently misdraw (plan
// §5.5.14 item 7).
- gl is not null ? foundation.DebugLines : null,
+ null,
d.PhysicsEngine,
d.PlayerMode,
d.PlayerController,
@@ -488,21 +462,17 @@ internal sealed class FrameRootCompositionPhase
d.RenderRange,
worldSceneDiagnostics,
live.WorldAvailability);
- if (gl is null)
- {
- // On Vulkan the world renderer runs INSIDE the frame's one
- // backbuffer pass, which this phase opens, publishes on the
- // scope, and closes.
- worldSceneRenderer =
- new AcDream.App.Rendering.Gpu.Vk.VulkanWorldScenePhase(
- host.GpuFrameLifetime,
- vulkanClear,
- () => d.Graphics.Vulkan?.SampleCount ?? 1,
- (d.Graphics as VulkanGameWindowGraphics)?.WorldPassScopeCore
- ?? throw new InvalidOperationException(
- "The Vulkan world phase requires the Vulkan graphics handle."),
- worldSceneRenderer);
- }
+ // The world renderer runs INSIDE the frame's one backbuffer pass,
+ // which this phase opens, publishes on the scope, and closes.
+ worldSceneRenderer =
+ new AcDream.App.Rendering.Gpu.Vk.VulkanWorldScenePhase(
+ host.GpuFrameLifetime,
+ vulkanClear,
+ () => d.Graphics.Vulkan?.SampleCount ?? 1,
+ (d.Graphics as VulkanGameWindowGraphics)?.WorldPassScopeCore
+ ?? throw new InvalidOperationException(
+ "The Vulkan world phase requires the Vulkan graphics handle."),
+ worldSceneRenderer);
}
Fault(FrameRootCompositionPoint.WorldRendererCreated);
bindings = new FrameRootRuntimeBindings();
@@ -591,17 +561,15 @@ internal sealed class FrameRootCompositionPhase
?? NullRenderFramePostDiagnosticsPhase.Instance;
var renderFrame = new RenderFrameOrchestrator(
host.GpuFrameLifetime,
- gl is not null
- ? new FrameProfilerGpuMeasurement(d.FrameProfiler, gl)
- // Campaign V slice V8: the Vulkan arm measures the same bracket
- // through its own timestamp scope. It stayed on the null adapter
- // from V6h until V8, which meant no [frame-prof] line existed on
- // Vulkan at all — see VulkanFrameGpuMeasurement.
- : d.Graphics.Vulkan is { } vulkanGraphics
- ? new AcDream.App.Rendering.Gpu.Vk.VulkanFrameGpuMeasurement(
- d.FrameProfiler,
- vulkanGraphics.Device)
- : AcDream.App.Rendering.Gpu.Vk.NullRenderFrameGpuMeasurement.Instance,
+ // Campaign V slice V8: the Vulkan arm measures the frame bracket
+ // through its own timestamp scope — see VulkanFrameGpuMeasurement.
+ // The raw-GL adapter (FrameProfilerGpuMeasurement) was deleted at
+ // slice V11.
+ d.Graphics.Vulkan is { } vulkanGraphics
+ ? new AcDream.App.Rendering.Gpu.Vk.VulkanFrameGpuMeasurement(
+ d.FrameProfiler,
+ vulkanGraphics.Device)
+ : AcDream.App.Rendering.Gpu.Vk.NullRenderFrameGpuMeasurement.Instance,
framePreparation,
worldSceneRenderer,
privatePresentation,
diff --git a/src/AcDream.App/Composition/GameWindowGraphics.cs b/src/AcDream.App/Composition/GameWindowGraphics.cs
index 6070adb2..5c174416 100644
--- a/src/AcDream.App/Composition/GameWindowGraphics.cs
+++ b/src/AcDream.App/Composition/GameWindowGraphics.cs
@@ -1,79 +1,32 @@
using AcDream.App.Rendering.Gpu.Vk;
-using Silk.NET.OpenGL;
namespace AcDream.App.Composition;
///
-/// Campaign V slice V6h: the graphics ownership that platform acquisition
-/// publishes, once per backend.
+/// The graphics ownership that platform acquisition publishes.
///
-/// was already generic over its
-/// graphics type; only the call sites pinned GL. This class is what they
-/// pin instead, so one composition pipeline drives both backends and the fork
-/// lives at the handful of construction sites that genuinely differ rather than
-/// in a second copy of the startup topology.
-///
-/// It is deliberately NOT a capability abstraction. Nothing dispatches on
-/// it per frame; phases that still speak raw GL ask for the context by name and
-/// take their Vulkan arm when it is absent. The GL arm is deleted at slice V11
-/// and this class with it.
+/// Vulkan is the only backend as of Campaign V slice V11: the raw-GL
+/// implementation (OpenGlGameWindowGraphics) and the members that only
+/// existed to branch on it (Backend, Gl, RequireGl) were
+/// deleted along with it. This class stays as the seam
+/// and the composition phases are
+/// already written against, so a future second backend would still add one
+/// class of this shape rather than reopening every call site.
///
internal abstract class GameWindowGraphics : IDisposable
{
- /// Which backend this handle owns.
- public abstract RenderBackendKind Backend { get; }
-
- ///
- /// The live GL context, or null on any other backend. Phases whose owners
- /// are still raw GL branch on this; each such branch is a slice of Campaign
- /// V that has not landed yet, and the null arm names which one.
- ///
- public virtual GL? Gl => null;
-
- /// The live Vulkan context, or null on any other backend.
+ /// The live Vulkan context.
public virtual VulkanGraphicsContext? Vulkan => null;
///
- /// Campaign V slice V6j: the backend's world-pass seam, or null where the
- /// world renderers open their own passes.
- ///
- /// It lives here because the three composition phases that need it —
- /// world render, live presentation and the frame root — already borrow this
- /// handle, and because whether a backend HAS such a seam is exactly the kind
- /// of thing this type exists to answer. GL returns null: its renderers
- /// submit raw, and the frame spine still owns framebuffer management until
- /// V4h.
+ /// The backend's world-pass seam. Every remaining renderer records into the
+ /// pass this publishes rather than opening its own.
///
public virtual AcDream.App.Rendering.IWorldPassScope? WorldPassScope => null;
- ///
- /// The GL context, or a failure naming the caller. Used where the call site
- /// has already established that the GL arm is running, so a null would be a
- /// composition bug rather than a backend difference.
- ///
- public GL RequireGl(string owner) =>
- Gl ?? throw new InvalidOperationException(
- $"'{owner}' requires the OpenGL context, but the {Backend} backend is active.");
-
public abstract void Dispose();
}
-/// OpenGL ownership: the Silk.NET context itself.
-internal sealed class OpenGlGameWindowGraphics : GameWindowGraphics
-{
- public OpenGlGameWindowGraphics(GL gl) =>
- Context = gl ?? throw new ArgumentNullException(nameof(gl));
-
- /// The owned context. is the borrowed view.
- public GL Context { get; }
-
- public override RenderBackendKind Backend => RenderBackendKind.Gl;
-
- public override GL? Gl => Context;
-
- public override void Dispose() => Context.Dispose();
-}
-
///
/// Vulkan ownership: the instance, surface, device, swapchain and RHI device
/// that acquired and gated.
@@ -91,8 +44,6 @@ internal sealed class VulkanGameWindowGraphics : GameWindowGraphics
public VulkanGraphicsContext Context { get; }
- public override RenderBackendKind Backend => RenderBackendKind.Vulkan;
-
public override VulkanGraphicsContext? Vulkan => Context;
/// The concrete scope, for the phase that publishes the encoder on it.
diff --git a/src/AcDream.App/Composition/HostInputCameraComposition.cs b/src/AcDream.App/Composition/HostInputCameraComposition.cs
index af96be07..7be703e7 100644
--- a/src/AcDream.App/Composition/HostInputCameraComposition.cs
+++ b/src/AcDream.App/Composition/HostInputCameraComposition.cs
@@ -4,7 +4,6 @@ using AcDream.App.Rendering.Gpu;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
using Silk.NET.Maths;
-using Silk.NET.OpenGL;
namespace AcDream.App.Composition;
@@ -115,91 +114,6 @@ internal interface IHostInputCameraCompositionFactory
PointerPositionState pointer);
}
-internal sealed class RetailHostInputCameraCompositionFactory
- : IHostInputCameraCompositionFactory
-{
- public IFramebufferViewportTarget CreateViewportTarget(GameWindowGraphics graphics) =>
- new SilkFramebufferViewportTarget(graphics.RequireGl("viewport target"));
-
- public GpuFrameFlightController? CreateGpuFrameFlights(GameWindowGraphics graphics) =>
- new(graphics.RequireGl("GPU frame flights"));
-
- public IGpuDevice CreateGpuDevice(
- GameWindowGraphics graphics,
- GpuFrameFlightController? frameFlights) =>
- new AcDream.App.Rendering.Gpu.Gl.GlGpuDevice(
- graphics.RequireGl("GPU device (RHI)"),
- frameFlights ?? throw new ArgumentNullException(nameof(frameFlights)),
- Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders"));
-
- public IGpuResourceRetirementQueue CreateRetirement(
- GameWindowGraphics graphics,
- GpuFrameFlightController? frameFlights,
- IGpuDevice device) =>
- frameFlights ?? throw new ArgumentNullException(nameof(frameFlights));
-
- public IRenderFrameSlotSource CreateFrameSlots(
- GameWindowGraphics graphics,
- GpuFrameFlightController? frameFlights,
- IGpuDevice device) =>
- frameFlights ?? throw new ArgumentNullException(nameof(frameFlights));
-
- public WorldRenderDiagnostics? CreateWorldRenderDiagnostics(
- GameWindowGraphics graphics,
- IRenderFrameDiagnosticLog log) =>
- new(
- new SilkRenderGlStateReader(
- graphics.RequireGl("world render diagnostics")),
- log);
-
- public SilkKeyboardSource CreateKeyboardSource(
- IKeyboard keyboard,
- HostQuiescenceGate quiescence) =>
- SilkKeyboardSource.CreateDetached(keyboard, quiescence);
-
- public SilkMouseSource CreateMouseSource(
- IMouse mouse,
- IInputCaptureSource capture,
- IKeyboardSource? keyboard,
- HostQuiescenceGate quiescence) =>
- SilkMouseSource.CreateDetached(mouse, capture, keyboard, quiescence);
-
- public IMouseLookCursor CreateMouseLookCursor(IMouse mouse) =>
- new SilkMouseLookCursor(mouse);
-
- public InputDispatcher CreateInputDispatcher(
- IKeyboardSource keyboard,
- IMouseSource mouse,
- KeyBindings bindings) =>
- InputDispatcher.CreateDetached(keyboard, mouse, bindings);
-
- public CameraController CreateCameraController() =>
- new(new OrbitCamera(), new FlyCamera());
-
- public IFramebufferCameraTarget CreateCameraTarget(CameraController camera) =>
- new CameraFramebufferTarget(camera);
-
- public CameraPointerInputController CreateCameraPointerInput(
- IReadOnlyList mice,
- HostQuiescenceGate quiescence,
- IInputCaptureSource capture,
- LocalPlayerModeState playerMode,
- CameraController camera,
- ChaseCameraInputState chase,
- IMouseSource mouse,
- PointerPositionState pointer) =>
- CameraPointerInputController.Create(
- mice,
- quiescence,
- capture,
- playerMode,
- camera,
- chase,
- mouse,
- pointer,
- new EnvironmentInputMonotonicClock());
-}
-
internal enum HostInputCameraCompositionPoint
{
ViewportBound,
@@ -236,7 +150,7 @@ internal sealed class HostInputCameraCompositionPhase :
private readonly IHostInputCameraCompositionFactory? _injectedFactory;
private readonly Action? _faultInjection;
private IHostInputCameraCompositionFactory _factory =
- new RetailHostInputCameraCompositionFactory();
+ new VulkanHostInputCameraCompositionFactory();
public HostInputCameraCompositionPhase(
HostInputCameraDependencies dependencies,
@@ -253,17 +167,13 @@ internal sealed class HostInputCameraCompositionPhase :
}
///
- /// Campaign V slice V6h: the default factory is chosen from the platform
- /// result, not from the host. The backend is a property of the graphics
- /// ownership that acquisition published, so the phase reads it rather than
- /// making every call site branch — and an injected factory (composition
+ /// The Vulkan factory is the only one left since the raw-GL arm was
+ /// deleted at Campaign V slice V11; an injected factory (composition
/// tests) still wins.
///
private static IHostInputCameraCompositionFactory DefaultFactoryFor(
GameWindowGraphics graphics) =>
- graphics.Backend == RenderBackendKind.Vulkan
- ? new VulkanHostInputCameraCompositionFactory()
- : new RetailHostInputCameraCompositionFactory();
+ new VulkanHostInputCameraCompositionFactory();
public HostInputCameraResult Compose(
GameWindowPlatformResult platform)
diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs
index 792ad2e0..535121ef 100644
--- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs
+++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs
@@ -25,7 +25,6 @@ using AcDream.UI.Abstractions.Panels.Chat;
using AcDream.UI.Abstractions.Panels.Vitals;
using DatReaderWriter;
using Silk.NET.Input;
-using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace AcDream.App.Composition;
diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs
index c00325b0..b1cfe102 100644
--- a/src/AcDream.App/Composition/LivePresentationComposition.cs
+++ b/src/AcDream.App/Composition/LivePresentationComposition.cs
@@ -30,7 +30,6 @@ using AcDream.Runtime.Entities;
using AcDream.Runtime.World;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
-using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace AcDream.App.Composition;
@@ -678,46 +677,28 @@ internal sealed class LivePresentationCompositionPhase
AlphaScratchBudgetProfile.Create(
d.Options.ResidencyBudgets.AlphaScratchBytes);
- // Campaign V slice V6h: every renderer below this line is still raw GL.
- // On a backend without a GL context none of them is constructed, and the
- // CPU owners this phase also produces — entity lifetime, motion,
- // selection, effects, lights, the landblock pipeline — run unchanged.
- GL? gl = d.Graphics.Gl;
var selectionScene = new RetailSelectionScene(
new RetailSelectionGeometryCache(content.Dats, d.DatLock));
- // Campaign V slice V6j: the world dispatcher exists on BOTH arms. The GL
- // arm is unchanged; the RHI arm records into the pass the world scene
- // phase publishes on this scope.
+ // Campaign V slice V6j: the world dispatcher records into the pass the
+ // world scene phase publishes on this scope. The raw-GL arm was
+ // deleted at slice V11.
IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope;
var dispatcherLease = scope.Acquire(
"WB draw dispatcher",
- () => gl is not null
- ? new WbDrawDispatcher(
- gl,
- foundation.MeshShader!,
- foundation.TextureCache,
- foundation.MeshAdapter!,
- entitySpawnAdapter,
- foundation.Bindless!,
- d.ClassificationCache,
- d.TranslucencyFades,
- selectionScene,
- d.RetailAlphaQueue,
- alphaScratchBudgets.DispatcherBytes)
- : new WbDrawDispatcher(
- host.GpuDevice,
- host.GpuFrameLifetime,
- worldPassScope
- ?? throw new InvalidOperationException(
- "A backend without a GL context must publish a world pass scope."),
- foundation.TextureCache,
- foundation.MeshAdapter!,
- entitySpawnAdapter,
- d.ClassificationCache,
- d.TranslucencyFades,
- selectionScene,
- d.RetailAlphaQueue,
- alphaScratchBudgets.DispatcherBytes),
+ () => new WbDrawDispatcher(
+ host.GpuDevice,
+ host.GpuFrameLifetime,
+ worldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope."),
+ foundation.TextureCache,
+ foundation.MeshAdapter!,
+ entitySpawnAdapter,
+ d.ClassificationCache,
+ d.TranslucencyFades,
+ selectionScene,
+ d.RetailAlphaQueue,
+ alphaScratchBudgets.DispatcherBytes),
static value => value.Dispose());
var selectionQuery = new WorldSelectionQuery(
liveEntities,
@@ -815,8 +796,9 @@ internal sealed class LivePresentationCompositionPhase
paperdollLease = scope.Acquire(
"paperdoll viewport",
() => new PaperdollViewportRenderer(
- gl,
- worldPassScope,
+ worldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope."),
host.GpuDevice,
host.GpuFrameLifetime,
paperdollDispatcher,
@@ -861,8 +843,9 @@ internal sealed class LivePresentationCompositionPhase
creatureAppraisalLease = scope.Acquire(
"creature appraisal viewport",
() => new CreatureAppraisalViewportRenderer(
- gl,
- worldPassScope,
+ worldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope."),
host.GpuDevice,
host.GpuFrameLifetime,
appraisalDispatcher,
@@ -897,24 +880,17 @@ internal sealed class LivePresentationCompositionPhase
var envCellFrustum = new WbFrustum();
var envCellLease = scope.Acquire(
"environment-cell renderer",
- () => gl is not null
- ? new EnvCellRenderer(
- gl,
- foundation.MeshAdapter!.MeshManager!,
- envCellFrustum)
- : new EnvCellRenderer(
- host.GpuDevice,
- host.GpuFrameLifetime,
- worldPassScope
- ?? throw new InvalidOperationException(
- "A backend without a GL context must publish a world pass scope."),
- foundation.MeshAdapter!.MeshManager!,
- envCellFrustum),
+ () => new EnvCellRenderer(
+ host.GpuDevice,
+ host.GpuFrameLifetime,
+ worldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope."),
+ foundation.MeshAdapter!.MeshManager!,
+ envCellFrustum),
static value => value.Dispose());
- // The RHI arm's three pipelines ARE its program, built at construction,
- // so only the GL arm has a second initialisation step.
- if (foundation.MeshShader is { } envCellShader)
- envCellLease.Resource.Initialize(envCellShader);
+ // The three pipelines ARE its program, built at construction — the
+ // raw-GL arm's separate Initialize(Shader) step was deleted at V11.
Fault(LivePresentationCompositionPoint.EnvironmentCellsCreated);
// The streaming pipeline itself is backend-neutral and runs on both
@@ -972,20 +948,18 @@ internal sealed class LivePresentationCompositionPhase
"portal clip frame",
ClipFrame.NoClip,
static value => value.Dispose());
- // Campaign V slice V6l: the portal depth mask exists on BOTH arms. The
- // GL arm keeps its inline program and raw draws; the RHI arm compiles
- // portal_depth from SPIR-V into three pipelines and records into the
- // pass the world scene phase publishes on this scope.
+ // Campaign V slice V6l: the portal depth mask compiles portal_depth
+ // from SPIR-V into three pipelines and records into the pass the
+ // world scene phase publishes on this scope. The raw-GL inline
+ // program and raw draws were deleted at slice V11.
var portalDepthLease = scope.Acquire(
"portal depth mask",
- () => gl is not null
- ? new PortalDepthMaskRenderer(gl)
- : new PortalDepthMaskRenderer(
- host.GpuDevice,
- host.GpuFrameLifetime,
- worldPassScope
- ?? throw new InvalidOperationException(
- "A backend without a GL context must publish a world pass scope.")),
+ () => new PortalDepthMaskRenderer(
+ host.GpuDevice,
+ host.GpuFrameLifetime,
+ worldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope.")),
static value => value.Dispose());
CompositionAcquisitionScope.CompositionAcquisitionLease<
PortalWaitNoticeController>? portalWaitNoticeLease = null;
@@ -1003,11 +977,9 @@ internal sealed class LivePresentationCompositionPhase
: null;
CompositionAcquisitionScope.CompositionAcquisitionLease<
PortalTunnelPresentation>? portalTunnelLease = null;
- // Campaign V slice V6m: portal space exists on BOTH arms. The GL arm is
- // unchanged; the RHI arm opens a backbuffer pass of its own and publishes
- // it on the scope for the span of the draw, the way the two offscreen
- // viewports do. It was the last raw-GL world-adjacent renderer, so the
- // Vulkan arm no longer composes a portal-less teleport presentation.
+ // Campaign V slice V6m: portal space opens a backbuffer pass of its
+ // own and publishes it on the scope for the span of the draw, the way
+ // the two offscreen viewports do. The raw-GL arm was deleted at V11.
if (dispatcherLease.Resource is { } portalDispatcher)
{
PortalTunnelPresentation portalTunnel;
@@ -1015,8 +987,9 @@ internal sealed class LivePresentationCompositionPhase
{
portalTunnel = d.PortalTunnelFallback.AcquirePrepared(
() => PortalTunnelPresentation.CreateRequired(
- gl,
- worldPassScope,
+ worldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope."),
host.GpuFrameLifetime,
content.Dats,
content.AnimationLoader,
@@ -1051,87 +1024,45 @@ internal sealed class LivePresentationCompositionPhase
}
Fault(LivePresentationCompositionPoint.PortalResourcesCreated);
- AcDream.App.Rendering.Shader? skyShader = gl is null
- ? null
- : d.RenderResourceLifetime.AcquireSkyShader(
- () => new AcDream.App.Rendering.Shader(
- gl,
- Path.Combine(foundation.ShadersDirectory, "sky.vert"),
- Path.Combine(foundation.ShadersDirectory, "sky.frag"),
- // Campaign V slice V6e: sky reads the shared texture table and
- // ACDREAM_UBO_SET now, both of which common.glsl declares.
- includeCommonPreamble: true));
- var skyShaderLease = skyShader is null
- ? null
- : scope.Own(
- "sky shader lifetime",
- skyShader,
- _ => d.RenderResourceLifetime.ReleaseSkyShader());
- // Campaign V slice V6k: the sky exists on BOTH arms. The GL arm is
- // unchanged except for where its table slots come from; the RHI arm
- // compiles the sky pair from SPIR-V and records into the pass the world
- // scene phase publishes on this scope.
+ // Campaign V slice V6k: the sky compiles its pair from SPIR-V and
+ // records into the pass the world scene phase publishes on this
+ // scope. The raw-GL arm — its own shader pair (acquired through
+ // GameRenderResourceLifetime.AcquireSkyShader, deleted with it) and
+ // its own bindless/device-table wiring — was deleted at slice V11.
var skyLease = scope.Acquire(
"sky renderer",
- () => gl is not null
- ? new SkyRenderer(
- gl,
- content.Dats,
- skyShader!,
- foundation.TextureCache,
- foundation.Samplers!,
- // Slice V6e: the sky samples through the shared texture table,
- // so it needs the same bindless entry point the world path uses.
- foundation.Bindless!,
- // Slice V6k: and V4t's world-handle seam on the device, which
- // retired the last per-renderer GlBindlessHandleTable.
- (AcDream.App.Rendering.Gpu.Gl.GlGpuDevice)host.GpuDevice)
- {
- // Campaign V slice V7: null unless ACDREAM_SKY_PHASE_SECONDS
- // is set, which is every run but a differential gate's.
- AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds,
- }
- : new SkyRenderer(
- host.GpuDevice,
- host.GpuFrameLifetime,
- worldPassScope
- ?? throw new InvalidOperationException(
- "A backend without a GL context must publish a world pass scope."),
- content.Dats,
- foundation.TextureCache)
- {
- AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds,
- },
+ () => new SkyRenderer(
+ host.GpuDevice,
+ host.GpuFrameLifetime,
+ worldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope."),
+ content.Dats,
+ foundation.TextureCache)
+ {
+ // Campaign V slice V7: null unless ACDREAM_SKY_PHASE_SECONDS
+ // is set, which is every run but a differential gate's.
+ AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds,
+ },
static value => value.Dispose());
- // Campaign V slice V6l: particles exist on BOTH arms. The GL arm is
- // unchanged; the RHI arm compiles the two particle pairs from SPIR-V,
- // draws instances through the vertex binding the V6l contract amendment
- // added, and records into the pass the world scene phase publishes on
- // this scope.
+ // Campaign V slice V6l: the RHI arm compiles the two particle pairs
+ // from SPIR-V, draws instances through the vertex binding the V6l
+ // contract amendment added, and records into the pass the world scene
+ // phase publishes on this scope. The raw-GL arm was deleted at V11.
var particleLease = scope.AcquireOptional(
"particle renderer",
- () => gl is not null
- ? new ParticleRenderer(
- gl,
- foundation.ShadersDirectory,
- content.ParticleSystem,
- foundation.TextureCache,
- content.Dats,
- foundation.MeshAdapter!,
- d.RetailAlphaQueue,
- alphaScratchBudgets.ParticleBytes)
- : new ParticleRenderer(
- host.GpuDevice,
- host.GpuFrameLifetime,
- worldPassScope
- ?? throw new InvalidOperationException(
- "A backend without a GL context must publish a world pass scope."),
- content.ParticleSystem,
- foundation.TextureCache,
- content.Dats,
- foundation.MeshAdapter!,
- d.RetailAlphaQueue,
- alphaScratchBudgets.ParticleBytes),
+ () => new ParticleRenderer(
+ host.GpuDevice,
+ host.GpuFrameLifetime,
+ worldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope."),
+ content.ParticleSystem,
+ foundation.TextureCache,
+ content.Dats,
+ foundation.MeshAdapter!,
+ d.RetailAlphaQueue,
+ alphaScratchBudgets.ParticleBytes),
static value => value.Dispose());
Fault(LivePresentationCompositionPoint.SkyAndParticlesCreated);
@@ -1252,7 +1183,6 @@ internal sealed class LivePresentationCompositionPhase
portalTunnelLease?.Transfer();
portalWaitNoticeLease?.Transfer();
skyLease.Transfer();
- skyShaderLease?.Transfer();
particleLease.Transfer();
bindingsLease.Transfer();
Fault(LivePresentationCompositionPoint.ResultPublished);
diff --git a/src/AcDream.App/Composition/VulkanHostInputCameraCompositionFactory.cs b/src/AcDream.App/Composition/VulkanHostInputCameraCompositionFactory.cs
index 74cffdec..6a19956d 100644
--- a/src/AcDream.App/Composition/VulkanHostInputCameraCompositionFactory.cs
+++ b/src/AcDream.App/Composition/VulkanHostInputCameraCompositionFactory.cs
@@ -8,28 +8,22 @@ using Silk.NET.Input;
namespace AcDream.App.Composition;
///
-/// Campaign V slice V6h: the Vulkan arm of the host phase.
-///
-/// This is the whole of the backend fork at composition Phase 1. Only the
-/// four graphics members differ from
-/// ; input, camera and
-/// pointer construction are platform concerns, not graphics ones, so they
-/// delegate rather than duplicate. Adding a fifth backend would add one more
-/// class of this shape and touch nothing else in the composition pipeline —
-/// which is the property §5.5.9 asked this slice to establish.
+/// Campaign V slice V6h: the Vulkan arm of the host phase — now the only arm,
+/// the raw-GL RetailHostInputCameraCompositionFactory it used to fork
+/// from having been deleted at slice V11. Input, camera and pointer
+/// construction are inlined directly rather than delegated, since there is no
+/// longer a second implementation to share them with.
///
/// What is absent, and why. There is no GL fence ring: the RHI
/// device owns its own frames-in-flight through a timeline semaphore, so
/// retirement and slot indexing come from the device instead. There is no
-/// : it is a raw-GL state tripwire, and
+/// : it was a raw-GL state tripwire, and
/// Vulkan has no global state for it to watch. Both nulls are read by the
/// phases that would otherwise consume them.
///
internal sealed class VulkanHostInputCameraCompositionFactory
: IHostInputCameraCompositionFactory
{
- private readonly RetailHostInputCameraCompositionFactory _platform = new();
-
public IFramebufferViewportTarget CreateViewportTarget(
GameWindowGraphics graphics) =>
// The viewport is a pipeline dynamic state on Vulkan, set per pass by
@@ -64,29 +58,29 @@ internal sealed class VulkanHostInputCameraCompositionFactory
public SilkKeyboardSource CreateKeyboardSource(
IKeyboard keyboard,
HostQuiescenceGate quiescence) =>
- _platform.CreateKeyboardSource(keyboard, quiescence);
+ SilkKeyboardSource.CreateDetached(keyboard, quiescence);
public SilkMouseSource CreateMouseSource(
IMouse mouse,
IInputCaptureSource capture,
IKeyboardSource? keyboard,
HostQuiescenceGate quiescence) =>
- _platform.CreateMouseSource(mouse, capture, keyboard, quiescence);
+ SilkMouseSource.CreateDetached(mouse, capture, keyboard, quiescence);
public IMouseLookCursor CreateMouseLookCursor(IMouse mouse) =>
- _platform.CreateMouseLookCursor(mouse);
+ new SilkMouseLookCursor(mouse);
public InputDispatcher CreateInputDispatcher(
IKeyboardSource keyboard,
IMouseSource mouse,
KeyBindings bindings) =>
- _platform.CreateInputDispatcher(keyboard, mouse, bindings);
+ InputDispatcher.CreateDetached(keyboard, mouse, bindings);
public CameraController CreateCameraController() =>
- _platform.CreateCameraController();
+ new(new OrbitCamera(), new FlyCamera());
public IFramebufferCameraTarget CreateCameraTarget(CameraController camera) =>
- _platform.CreateCameraTarget(camera);
+ new CameraFramebufferTarget(camera);
public CameraPointerInputController CreateCameraPointerInput(
IReadOnlyList mice,
@@ -97,7 +91,7 @@ internal sealed class VulkanHostInputCameraCompositionFactory
ChaseCameraInputState chase,
IMouseSource mouse,
PointerPositionState pointer) =>
- _platform.CreateCameraPointerInput(
+ CameraPointerInputController.Create(
mice,
quiescence,
capture,
@@ -105,14 +99,15 @@ internal sealed class VulkanHostInputCameraCompositionFactory
camera,
chase,
mouse,
- pointer);
+ pointer,
+ new EnvironmentInputMonotonicClock());
private static VulkanGraphicsContext RequireContext(
GameWindowGraphics graphics) =>
graphics.Vulkan
?? throw new InvalidOperationException(
- "The Vulkan host factory was composed against the " +
- $"{graphics.Backend} backend.");
+ "The Vulkan host factory was composed against a backend with no " +
+ "Vulkan context.");
///
/// Vulkan sets the viewport per pass from the pass extent, so there is no
diff --git a/src/AcDream.App/Composition/WorldRenderComposition.cs b/src/AcDream.App/Composition/WorldRenderComposition.cs
index 7b72ce07..47513e25 100644
--- a/src/AcDream.App/Composition/WorldRenderComposition.cs
+++ b/src/AcDream.App/Composition/WorldRenderComposition.cs
@@ -14,7 +14,6 @@ using DatReaderWriter.DBObjs;
using Microsoft.Extensions.Logging.Abstractions;
using Silk.NET.Input;
using Silk.NET.OpenGL;
-using Shader = AcDream.App.Rendering.Shader;
namespace AcDream.App.Composition;
@@ -31,27 +30,21 @@ internal sealed record WorldTerrainBuildContext(
///
/// The render foundation the later phases build on.
///
-/// Campaign V slice V6h made every raw-GL member nullable. They are all
-/// present on GL and all absent on Vulkan, because the world renderers that own
-/// them are still raw GL until slices V4t/V4c/V4d land the Vulkan world arm.
-/// What survives on both backends is exactly the RHI-ported set — the texture
-/// cache's UI path, the debug font, the text renderer and the debug lines — plus
-/// the backend-neutral residency ledger and shader directory.
+/// Every member here is backend-neutral. The raw-GL-only members this
+/// record used to carry (Bindless, TerrainShader, MeshShader,
+/// Samplers) were deleted at Campaign V slice V11 along with the GL arm
+/// that was their only producer.
///
internal sealed record WorldRenderFoundation(
string ShadersDirectory,
- BindlessSupport? Bindless,
TerrainAtlas? TerrainAtlas,
- Shader? TerrainShader,
SceneLightingUboBinding? SceneLighting,
DebugLineRenderer DebugLines,
BitmapFont? DebugFont,
TextRenderer? TextRenderer,
TerrainModernRenderer? Terrain,
- Shader? MeshShader,
WbMeshAdapter? MeshAdapter,
TextureCache TextureCache,
- SamplerCache? Samplers,
ResidencyManager Residency);
internal sealed record WorldRenderResult(
@@ -71,8 +64,6 @@ internal sealed record WorldRenderDependencies(
internal interface IGameWindowWorldRenderPublication
{
- void PublishBindlessSupport(BindlessSupport value);
- void PublishTerrainShader(Shader value);
void PublishSceneLighting(SceneLightingUboBinding value);
void PublishDebugLines(DebugLineRenderer value);
void PublishHudResources(BitmapFont font, TextRenderer text);
@@ -81,27 +72,20 @@ internal interface IGameWindowWorldRenderPublication
float[] heightTable,
TerrainBlendingContext blending,
ConcurrentDictionary surfaceCache);
- void PublishMeshShader(Shader value);
void PublishWbMeshAdapter(WbMeshAdapter value);
void PublishTextureCache(TextureCache value);
- void PublishSamplerCache(SamplerCache value);
}
internal interface IWorldRenderCompositionFactory
{
- void InitializeGlState(GL gl);
WorldRegionData LoadRegion(IDatReaderWriter dats);
void InitializeEnvironment(WorldEnvironmentController environment, Region region);
- BindlessSupport RequireBindless(GL gl, Action log);
- TerrainAtlas AcquireTerrainAtlas(
- IGameRenderResourceLifetime lifetime,
- GL gl,
- IDatReaderWriter dats,
- BindlessSupport bindless);
///
/// Campaign V slice V6i-2: the terrain atlas built through
- /// rather than raw GL.
- /// Same DATs, same decode, same layer ordering — only the upload differs.
+ /// . The raw-GL arm this
+ /// used to fork from (AcquireTerrainAtlas) was deleted at slice V11;
+ /// the name keeps its "BackendNeutral" suffix rather than being renamed, to
+ /// avoid touching every call site for a purely cosmetic change.
///
TerrainAtlas AcquireBackendNeutralTerrainAtlas(
IGameRenderResourceLifetime lifetime,
@@ -120,12 +104,11 @@ internal interface IWorldRenderCompositionFactory
Action log);
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
- Shader CreateTerrainShader(GL gl, string shadersDirectory);
- SceneLightingUboBinding CreateSceneLighting(GL gl);
///
/// Campaign V slice V6j: scene lighting on a backend with no global uniform
/// binding point. It publishes a ring section on the world pass scope and
- /// each renderer binds it inside the pass.
+ /// each renderer binds it inside the pass. The raw-GL global-uniform-binding
+ /// arm (CreateSceneLighting) was deleted at slice V11.
///
SceneLightingUboBinding CreateBackendNeutralSceneLighting(
ICurrentGpuFrameSource frameSource,
@@ -140,17 +123,11 @@ internal interface IWorldRenderCompositionFactory
AcDream.App.Rendering.Gpu.IGpuDevice device,
ICurrentGpuFrameSource frameSource,
string shadersDirectory);
- TerrainModernRenderer CreateTerrain(
- GL gl,
- BindlessSupport bindless,
- Shader shader,
- TerrainAtlas atlas,
- AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
- IGpuResourceRetirementQueue retirement);
///
/// Campaign V slice V6j: terrain's RHI arm. No GL context, no linked
/// program — the pipeline compiles terrain_modern from the committed
- /// SPIR-V and records into the world pass the scope publishes.
+ /// SPIR-V and records into the world pass the scope publishes. The raw-GL
+ /// arm (CreateTerrain) was deleted at slice V11.
///
TerrainModernRenderer CreateBackendNeutralTerrain(
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
@@ -170,7 +147,6 @@ internal interface IWorldRenderCompositionFactory
uint initialCenterLandblockId,
float[] heightTable,
TerrainAtlas? atlas);
- Shader CreateMeshShader(GL gl, string shadersDirectory);
WbMeshAdapter CreateMeshAdapter(
GL? gl,
AcDream.App.Rendering.Gpu.IGpuDevice device,
@@ -179,10 +155,8 @@ internal interface IWorldRenderCompositionFactory
IGpuResourceRetirementQueue retirement,
ResidencyBudgetOptions budgets);
TextureCache CreateTextureCache(
- GL? gl,
AcDream.App.Rendering.Gpu.IGpuDevice device,
IDatReaderWriter dats,
- BindlessSupport? bindless,
IGpuResourceRetirementQueue retirement,
string diagnosticsDirectory,
ResidencyBudgetOptions budgets);
@@ -193,20 +167,12 @@ internal interface IWorldRenderCompositionFactory
IPreparedAssetSource preparedAssets,
IAnimationLoader animations,
DatSoundCache? audio);
- SamplerCache CreateSamplerCache(GL gl);
void Release(IDisposable resource);
}
internal sealed class RetailWorldRenderCompositionFactory
: IWorldRenderCompositionFactory
{
- public void InitializeGlState(GL gl)
- {
- ArgumentNullException.ThrowIfNull(gl);
- gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
- gl.Enable(EnableCap.DepthTest);
- }
-
public WorldRegionData LoadRegion(IDatReaderWriter dats)
{
ArgumentNullException.ThrowIfNull(dats);
@@ -231,46 +197,6 @@ internal sealed class RetailWorldRenderCompositionFactory
environment.Initialize(region);
}
- public BindlessSupport RequireBindless(GL gl, Action log)
- {
- ArgumentNullException.ThrowIfNull(gl);
- ArgumentNullException.ThrowIfNull(log);
- if (BindlessSupport.TryCreate(gl, out BindlessSupport? bindless))
- {
- if (bindless!.HasShaderDrawParameters(gl))
- {
- log("[N.5] modern path capabilities present " +
- "(bindless + ARB_shader_draw_parameters)");
- return bindless;
- }
- log("[N.5] GL_ARB_shader_draw_parameters not present — " +
- "modern path not available");
- }
- else
- {
- log("[N.5] GL_ARB_bindless_texture not present — " +
- "modern path not available");
- }
-
- throw new NotSupportedException(
- "acdream requires GL_ARB_bindless_texture + " +
- "GL_ARB_shader_draw_parameters (GL 4.3+ with bindless support). " +
- "Your GPU/driver does not expose these extensions. If this is " +
- "unexpected, please file a bug report with your GPU vendor + " +
- "driver version.");
- }
-
- public TerrainAtlas AcquireTerrainAtlas(
- IGameRenderResourceLifetime lifetime,
- GL gl,
- IDatReaderWriter dats,
- BindlessSupport bindless)
- {
- ArgumentNullException.ThrowIfNull(lifetime);
- return lifetime.AcquireTerrainAtlas(
- () => TerrainAtlas.Build(gl, dats, bindless));
- }
-
public TerrainAtlas AcquireBackendNeutralTerrainAtlas(
IGameRenderResourceLifetime lifetime,
AcDream.App.Rendering.Gpu.IGpuDevice device,
@@ -289,15 +215,6 @@ internal sealed class RetailWorldRenderCompositionFactory
public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) =>
atlas.SetAnisotropic(level);
- public Shader CreateTerrainShader(GL gl, string shadersDirectory) =>
- new(
- gl,
- Path.Combine(shadersDirectory, "terrain_modern.vert"),
- Path.Combine(shadersDirectory, "terrain_modern.frag"),
- includeCommonPreamble: true);
-
- public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl);
-
public SceneLightingUboBinding CreateBackendNeutralSceneLighting(
ICurrentGpuFrameSource frameSource,
IWorldPassScope scope) =>
@@ -321,25 +238,6 @@ internal sealed class RetailWorldRenderCompositionFactory
string shadersDirectory) =>
new(device, frameSource, shadersDirectory);
- public TerrainModernRenderer CreateTerrain(
- GL gl,
- BindlessSupport bindless,
- Shader shader,
- TerrainAtlas atlas,
- AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
- IGpuResourceRetirementQueue retirement) =>
- // Campaign V slice V4t: terrain's atlas slots live in the device's one
- // texture table. This renderer only exists on GL — the `gl is not null`
- // gate at its call site is the same one — so the backend cast is a
- // statement of that fact rather than a narrowing.
- new(
- gl,
- bindless,
- shader,
- atlas,
- (AcDream.App.Rendering.Gpu.Gl.GlGpuDevice)gpuDevice,
- retirement);
-
public TerrainModernRenderer CreateBackendNeutralTerrain(
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
ICurrentGpuFrameSource frameSource,
@@ -400,13 +298,6 @@ internal sealed class RetailWorldRenderCompositionFactory
new ConcurrentDictionary());
}
- public Shader CreateMeshShader(GL gl, string shadersDirectory) =>
- new(
- gl,
- Path.Combine(shadersDirectory, "mesh_modern.vert"),
- Path.Combine(shadersDirectory, "mesh_modern.frag"),
- includeCommonPreamble: true);
-
public WbMeshAdapter CreateMeshAdapter(
GL? gl,
AcDream.App.Rendering.Gpu.IGpuDevice device,
@@ -424,18 +315,14 @@ internal sealed class RetailWorldRenderCompositionFactory
budgets);
public TextureCache CreateTextureCache(
- GL? gl,
AcDream.App.Rendering.Gpu.IGpuDevice device,
IDatReaderWriter dats,
- BindlessSupport? bindless,
IGpuResourceRetirementQueue retirement,
string diagnosticsDirectory,
ResidencyBudgetOptions budgets) =>
new(
- gl,
device,
dats,
- bindless,
retirement,
diagnosticsDirectory,
budgets);
@@ -501,19 +388,14 @@ internal sealed class RetailWorldRenderCompositionFactory
}
}
- public SamplerCache CreateSamplerCache(GL gl) => new(gl);
-
public void Release(IDisposable resource) => resource.Dispose();
}
internal enum WorldRenderCompositionPoint
{
- GlStateInitialized,
RegionLoaded,
EnvironmentInitialized,
- BindlessPublished,
TerrainAtlasAcquired,
- TerrainShaderPublished,
SceneLightingPublished,
DebugLinesPublished,
DebugFontCreated,
@@ -522,10 +404,8 @@ internal enum WorldRenderCompositionPoint
HudResourcesCompleted,
TerrainPublished,
TerrainBuildStatePublished,
- MeshShaderPublished,
MeshAdapterPublished,
TextureCachePublished,
- SamplerCachePublished,
}
///
@@ -571,91 +451,53 @@ internal sealed class WorldRenderCompositionPhase
var scope = new CompositionAcquisitionScope();
try
{
- // Campaign V slice V6h: null on a backend with no GL context. Every
- // world renderer below is still raw GL, so the Vulkan arm composes
- // the RHI-ported subset only — the texture cache's UI path, the
- // debug font, the text renderer, the debug lines — and leaves the
- // world absent until slices V4t/V4c/V4d land it.
- GL? gl = platform.Graphics.Gl;
var residency = new ResidencyManager(
_dependencies.ResidencyBudgets);
- if (gl is not null)
- _factory.InitializeGlState(gl);
- Fault(WorldRenderCompositionPoint.GlStateInitialized);
WorldRegionData region = _factory.LoadRegion(content.Dats);
Fault(WorldRenderCompositionPoint.RegionLoaded);
_factory.InitializeEnvironment(_dependencies.Environment, region.Region);
Fault(WorldRenderCompositionPoint.EnvironmentInitialized);
- BindlessSupport? bindless = gl is null
- ? null
- : _factory.RequireBindless(gl, _dependencies.Log);
- if (bindless is not null)
- _publication.PublishBindlessSupport(bindless);
- Fault(WorldRenderCompositionPoint.BindlessPublished);
-
- // Campaign V slice V6i-2: the atlas exists on BOTH arms now. GL
- // builds it from raw texture names as before; a backend with no GL
- // context builds the same layers through IGpuDevice.CreateTexture.
- // That is what makes the blending/T-code tables below real on
- // Vulkan — and, more to the point, it is what puts the new creation
- // path under the validation layer instead of leaving it a claim.
- TerrainAtlas? terrainAtlas = gl is not null && bindless is not null
- ? _factory.AcquireTerrainAtlas(
- _dependencies.RenderResources,
- gl,
- content.Dats,
- bindless)
- : _factory.AcquireBackendNeutralTerrainAtlas(
- _dependencies.RenderResources,
- _dependencies.GpuDevice,
- content.Dats);
+ // Campaign V slice V6i-2: the atlas builds through IGpuDevice on
+ // every remaining backend. The raw-GL arm that built it from raw
+ // texture names was deleted at slice V11.
+ TerrainAtlas terrainAtlas = _factory.AcquireBackendNeutralTerrainAtlas(
+ _dependencies.RenderResources,
+ _dependencies.GpuDevice,
+ content.Dats);
_factory.SetTerrainAnisotropic(
terrainAtlas,
settings.ResolvedQuality.AnisotropicLevel);
Fault(WorldRenderCompositionPoint.TerrainAtlasAcquired);
- // The rest of the world texture stack's creation path, exercised on
- // the arm that has no GL: one shared array of each format family and
- // one composite array, created and released here. Nothing draws
- // them; see the type's own documentation for why they are built
- // anyway. It claims no composition point and enters no acquisition
- // scope — the frozen publication order is a pinned assertion, and an
- // exercise that owns nothing past its own statement is not a
- // published owner.
- if (gl is null)
- {
- _factory.ExerciseBackendNeutralWorldTextures(
- _dependencies.GpuDevice,
- _dependencies.Log);
- }
+ // The rest of the world texture stack's creation path: one shared
+ // array of each format family and one composite array, created and
+ // released here. Nothing draws them; see the type's own
+ // documentation for why they are built anyway. It claims no
+ // composition point and enters no acquisition scope — the frozen
+ // publication order is a pinned assertion, and an exercise that
+ // owns nothing past its own statement is not a published owner.
+ _factory.ExerciseBackendNeutralWorldTextures(
+ _dependencies.GpuDevice,
+ _dependencies.Log);
string shadersDirectory = Path.Combine(
AppContext.BaseDirectory,
"Rendering",
"Shaders");
- Shader? terrainShader = AcquireAndPublishIf(
- gl is not null,
- scope,
- "terrain shader",
- () => _factory.CreateTerrainShader(gl!, shadersDirectory),
- _publication.PublishTerrainShader,
- WorldRenderCompositionPoint.TerrainShaderPublished);
- // Campaign V slice V6j: scene lighting exists on BOTH arms — the
- // world renderers read it on both, and on the RHI arm it publishes a
- // ring section instead of holding a global binding point.
- IWorldPassScope? worldPassScope = platform.Graphics.WorldPassScope;
- SceneLightingUboBinding? sceneLighting = AcquireAndPublish(
+ // Campaign V slice V6j: scene lighting publishes a ring section on
+ // the world pass scope; the raw-GL global-uniform-binding arm was
+ // deleted at slice V11.
+ IWorldPassScope worldPassScope = platform.Graphics.WorldPassScope
+ ?? throw new InvalidOperationException(
+ "The graphics backend must publish a world pass scope.");
+ SceneLightingUboBinding sceneLighting = AcquireAndPublish(
scope,
"scene lighting",
- () => gl is not null
- ? _factory.CreateSceneLighting(gl)
- : _factory.CreateBackendNeutralSceneLighting(
- _dependencies.GpuFrameSource,
- worldPassScope
- ?? throw new InvalidOperationException(
- "A backend without a GL context must publish a world pass scope.")),
+ () => _factory.CreateBackendNeutralSceneLighting(
+ _dependencies.GpuFrameSource,
+ worldPassScope),
_publication.PublishSceneLighting,
WorldRenderCompositionPoint.SceneLightingPublished);
DebugLineRenderer debugLines = AcquireAndPublish(
@@ -671,28 +513,18 @@ internal sealed class WorldRenderCompositionPhase
(BitmapFont? debugFont, TextRenderer? textRenderer) =
ComposeOptionalHudResources(scope, shadersDirectory);
- // Campaign V slice V6j: terrain exists on BOTH arms. The GL arm is
- // unchanged; the RHI arm records into the world pass and reads the
- // same backend-neutral atlas built above.
- TerrainModernRenderer? terrain = AcquireAndPublish(
+ // Campaign V slice V6j: terrain records into the world pass and
+ // reads the same backend-neutral atlas built above. The raw-GL arm
+ // was deleted at slice V11.
+ TerrainModernRenderer terrain = AcquireAndPublish(
scope,
"terrain renderer",
- () => gl is not null
- ? _factory.CreateTerrain(
- gl,
- bindless!,
- terrainShader!,
- terrainAtlas!,
- _dependencies.GpuDevice,
- _dependencies.ResourceRetirement)
- : _factory.CreateBackendNeutralTerrain(
- _dependencies.GpuDevice,
- _dependencies.GpuFrameSource,
- worldPassScope
- ?? throw new InvalidOperationException(
- "A backend without a GL context must publish a world pass scope."),
- terrainAtlas!,
- _dependencies.ResourceRetirement),
+ () => _factory.CreateBackendNeutralTerrain(
+ _dependencies.GpuDevice,
+ _dependencies.GpuFrameSource,
+ worldPassScope,
+ terrainAtlas,
+ _dependencies.ResourceRetirement),
_publication.PublishTerrain,
WorldRenderCompositionPoint.TerrainPublished);
@@ -707,25 +539,17 @@ internal sealed class WorldRenderCompositionPhase
terrainBuild.SurfaceCache);
Fault(WorldRenderCompositionPoint.TerrainBuildStatePublished);
- Shader? meshShader = AcquireAndPublishIf(
- gl is not null,
- scope,
- "mesh shader",
- () => _factory.CreateMeshShader(gl!, shadersDirectory),
- _publication.PublishMeshShader,
- WorldRenderCompositionPoint.MeshShaderPublished);
- if (meshShader is not null)
- _dependencies.Log("[N.5] mesh_modern shader loaded");
- // Campaign V slice V6i-3: the mesh pipeline exists on BOTH arms. Its
- // upload bodies reached IGpuBuffer, so a backend with no GL context
- // builds the same arena, the same atlases and the same render data —
- // which is what makes streaming's publication into GPU state real
- // there rather than a no-op.
+ // Campaign V slice V6i-3: the mesh pipeline builds the same arena,
+ // atlases and render data on every remaining backend, which is what
+ // makes streaming's publication into GPU state real rather than a
+ // no-op. gl is always null here — the raw-GL arm was deleted at
+ // slice V11, and the constructor's GL? gl parameter is Commit 3's
+ // (IMeshPipelineDevice.Gl) to remove.
WbMeshAdapter meshAdapter = AcquireAndPublish(
scope,
"WB mesh adapter",
() => _factory.CreateMeshAdapter(
- gl,
+ gl: null,
_dependencies.GpuDevice,
content.Dats,
content.PreparedAssets,
@@ -737,22 +561,13 @@ internal sealed class WorldRenderCompositionPhase
scope,
"texture cache",
() => _factory.CreateTextureCache(
- gl,
_dependencies.GpuDevice,
content.Dats,
- bindless,
_dependencies.ResourceRetirement,
_dependencies.DiagnosticsDirectory,
residency.Budgets),
_publication.PublishTextureCache,
WorldRenderCompositionPoint.TextureCachePublished);
- SamplerCache? samplers = AcquireAndPublishIf(
- gl is not null,
- scope,
- "sampler cache",
- () => _factory.CreateSamplerCache(gl!),
- _publication.PublishSamplerCache,
- WorldRenderCompositionPoint.SamplerCachePublished);
_factory.RegisterResidencySources(
residency,
meshAdapter,
@@ -769,18 +584,14 @@ internal sealed class WorldRenderCompositionPhase
terrainBuild,
new WorldRenderFoundation(
shadersDirectory,
- bindless,
terrainAtlas,
- terrainShader,
sceneLighting,
debugLines,
debugFont,
textRenderer,
terrain,
- meshShader,
meshAdapter,
textureCache,
- samplers,
residency));
}
catch (Exception failure)
@@ -843,27 +654,6 @@ internal sealed class WorldRenderCompositionPhase
return value;
}
- ///
- /// Campaign V slice V6h: acquires an owner the active backend may not have.
- /// The fault point still fires on both arms so a failure-injection test
- /// covers the same ordered sequence whichever backend composed it.
- ///
- private T? AcquireAndPublishIf(
- bool supported,
- CompositionAcquisitionScope scope,
- string name,
- Func factory,
- Action publish,
- WorldRenderCompositionPoint point)
- where T : class, IDisposable
- {
- T? value = supported
- ? scope.Acquire(name, factory, _factory.Release).Publish(publish)
- : null;
- Fault(point);
- return value;
- }
-
private void Fault(WorldRenderCompositionPoint point) =>
_faultInjection?.Invoke(point);
}
diff --git a/src/AcDream.App/Diagnostics/FrameScreenshotController.cs b/src/AcDream.App/Diagnostics/FrameScreenshotController.cs
index 70dd0e16..f6b9ada9 100644
--- a/src/AcDream.App/Diagnostics/FrameScreenshotController.cs
+++ b/src/AcDream.App/Diagnostics/FrameScreenshotController.cs
@@ -1,6 +1,4 @@
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-using SixLabors.ImageSharp;
+using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace AcDream.App.Diagnostics;
@@ -28,39 +26,6 @@ internal sealed class FrameScreenshotController
private readonly Dictionary _status =
new(StringComparer.OrdinalIgnoreCase);
- public FrameScreenshotController(
- GL gl,
- string directory,
- Action? log = null)
- : this(
- CreateReader(gl),
- directory,
- log)
- {
- }
-
- private static Func CreateReader(GL gl)
- {
- ArgumentNullException.ThrowIfNull(gl);
- var surface = new GlDefaultFramebufferSurface(gl);
- return (width, height) => ReadDefaultFramebuffer(surface, width, height);
- }
-
- ///
- /// Reads the default framebuffer through the same resolve-aware path the
- /// screenshot gates use. Shared so there is exactly one implementation of
- /// "read the backbuffer" in the process — see
- /// .
- ///
- internal static byte[] ReadDefaultFramebuffer(GL gl, int width, int height)
- {
- ArgumentNullException.ThrowIfNull(gl);
- return ReadDefaultFramebuffer(
- new GlDefaultFramebufferSurface(gl),
- width,
- height);
- }
-
internal FrameScreenshotController(
Func readRgba,
string directory,
@@ -186,7 +151,7 @@ internal sealed class FrameScreenshotController
///
/// Blits the whole colour buffer from the bound read framebuffer to the
/// bound draw framebuffer with GL_NEAREST and identical rectangles
- /// — the multisample resolve.
+ /// — the multisample resolve.
///
void BlitColorNearest(int width, int height);
@@ -194,7 +159,7 @@ internal sealed class FrameScreenshotController
}
///
- /// Reads the default framebuffer — framebuffer name 0, the backbuffer —
+ /// Reads the default framebuffer — framebuffer name 0, the backbuffer —
/// and nothing else.
///
///
@@ -204,13 +169,13 @@ internal sealed class FrameScreenshotController
/// offscreen target the previous renderer left bound. The frame this runs in
/// draws several: PrivateEntityViewportRenderer clears its paperdoll
/// and appraisal FBOs to exactly RGBA(0,0,0,0), which is what a leaked
- /// binding writes to disk — a fully transparent PNG that reads as a
+ /// binding writes to disk — a fully transparent PNG that reads as a
/// blank-world failure while the backbuffer on screen was correct.
///
///
/// This was latent for as long as something else rebound framebuffer 0 often
/// enough to mask it (before Campaign V slice V4c, GL BeginPass did so
- /// on every pass — see plan §5.4). The capture states its own source instead
+ /// on every pass — see plan §5.4). The capture states its own source instead
/// of inheriting one, and restores the caller's binding so a diagnostic
/// capture cannot perturb the frame it observes.
///
@@ -218,15 +183,15 @@ internal sealed class FrameScreenshotController
/// Multisampling. The window is created with the quality preset's
/// MSAA sample count, so the default framebuffer is normally 4x multisampled,
/// and glReadPixels against a multisampled read framebuffer is
- /// undefined per the GL spec (GL 4.6 §18.2: an INVALID_OPERATION is
+ /// undefined per the GL spec (GL 4.6 §18.2: an INVALID_OPERATION is
/// generated only for framebuffer objects; for the default framebuffer the
/// result is simply unspecified, and AMD returns real pixels most of the time
/// and something else the rest). Every automated pixel gate and every blank-
/// world verdict in Campaign V reads through here, so an unspecified read is
/// an unsound instrument, not a cosmetic issue. When the default framebuffer
- /// is multisampled the capture resolves it first — blit the whole colour
+ /// is multisampled the capture resolves it first — blit the whole colour
/// buffer into a single-sampled RGBA8 framebuffer with identical rectangles
- /// and GL_NEAREST, which is the defined resolve — and reads that.
+ /// and GL_NEAREST, which is the defined resolve — and reads that.
/// A single-sampled default framebuffer keeps the original direct read, so
/// non-MSAA captures stay byte-for-byte what they were.
///
@@ -271,7 +236,7 @@ internal sealed class FrameScreenshotController
uint resolve = surface.CreateResolveTarget(width, height);
try
{
- // Read is still framebuffer 0 — the multisampled source.
+ // Read is still framebuffer 0 — the multisampled source.
surface.BindDrawFramebuffer(resolve);
surface.BlitColorNearest(width, height);
surface.BindReadFramebuffer(resolve);
@@ -283,134 +248,4 @@ internal sealed class FrameScreenshotController
}
}
- private sealed class GlDefaultFramebufferSurface : IDefaultFramebufferSurface
- {
- private readonly GL _gl;
- private uint _resolveRenderbuffer;
-
- public GlDefaultFramebufferSurface(GL gl) => _gl = gl;
-
- public uint ReadFramebufferBinding
- {
- get
- {
- _gl.GetInteger(GetPName.ReadFramebufferBinding, out int binding);
- return (uint)binding;
- }
- }
-
- public uint DrawFramebufferBinding
- {
- get
- {
- _gl.GetInteger(GetPName.DrawFramebufferBinding, out int binding);
- return (uint)binding;
- }
- }
-
- public int DefaultFramebufferSamples
- {
- get
- {
- _gl.GetInteger(GetPName.Samples, out int samples);
- return samples;
- }
- }
-
- public void BindReadFramebuffer(uint framebuffer) =>
- _gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, framebuffer);
-
- public void BindDrawFramebuffer(uint framebuffer) =>
- _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, framebuffer);
-
- public uint CreateResolveTarget(int width, int height)
- {
- // RGBA8 matches the default framebuffer's colour encoding — the app
- // never enables GL_FRAMEBUFFER_SRGB — so the blit is a pure resolve
- // with no encoding conversion.
- uint renderbuffer = _gl.GenRenderbuffer();
- _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, renderbuffer);
- _gl.RenderbufferStorage(
- RenderbufferTarget.Renderbuffer,
- InternalFormat.Rgba8,
- (uint)width,
- (uint)height);
- _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0u);
-
- uint framebuffer = _gl.GenFramebuffer();
- _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, framebuffer);
- _gl.FramebufferRenderbuffer(
- FramebufferTarget.DrawFramebuffer,
- FramebufferAttachment.ColorAttachment0,
- RenderbufferTarget.Renderbuffer,
- renderbuffer);
- GLEnum status =
- _gl.CheckFramebufferStatus(FramebufferTarget.DrawFramebuffer);
- if (status != GLEnum.FramebufferComplete)
- {
- _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, 0u);
- _gl.DeleteFramebuffer(framebuffer);
- _gl.DeleteRenderbuffer(renderbuffer);
- throw new InvalidOperationException(
- $"multisample resolve framebuffer {width}x{height} is "
- + $"incomplete: {status}");
- }
-
- GLHelpers.ThrowOnResourceError(
- _gl,
- $"create screenshot resolve target {width}x{height}");
- _resolveRenderbuffer = renderbuffer;
- return framebuffer;
- }
-
- public void DeleteResolveTarget(uint framebuffer)
- {
- _gl.DeleteFramebuffer(framebuffer);
- if (_resolveRenderbuffer != 0u)
- _gl.DeleteRenderbuffer(_resolveRenderbuffer);
- _resolveRenderbuffer = 0u;
- }
-
- public void BlitColorNearest(int width, int height)
- {
- // A blit is subject to the scissor test, so a frame that left a
- // scissor rectangle armed would resolve only part of the image.
- // This operation states the state it needs and restores it — the
- // same self-contained-GL-state rule the render passes follow.
- bool scissor = _gl.IsEnabled(EnableCap.ScissorTest);
- if (scissor)
- _gl.Disable(EnableCap.ScissorTest);
- _gl.BlitFramebuffer(
- 0,
- 0,
- width,
- height,
- 0,
- 0,
- width,
- height,
- ClearBufferMask.ColorBufferBit,
- BlitFramebufferFilter.Nearest);
- if (scissor)
- _gl.Enable(EnableCap.ScissorTest);
- GLHelpers.ThrowOnResourceError(
- _gl,
- $"resolve default framebuffer {width}x{height}");
- }
-
- public unsafe void ReadRgba(int width, int height, byte[] destination)
- {
- fixed (byte* pointer = destination)
- {
- _gl.ReadPixels(
- 0,
- 0,
- (uint)width,
- (uint)height,
- PixelFormat.Rgba,
- PixelType.UnsignedByte,
- pointer);
- }
- }
- }
}
diff --git a/src/AcDream.App/Platform/GraphicalCapabilityRecord.cs b/src/AcDream.App/Platform/GraphicalCapabilityRecord.cs
deleted file mode 100644
index 7a5e3f2d..00000000
--- a/src/AcDream.App/Platform/GraphicalCapabilityRecord.cs
+++ /dev/null
@@ -1,525 +0,0 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using AcDream.App.Rendering;
-using Silk.NET.Input;
-using Silk.NET.GLFW;
-using Silk.NET.OpenGL;
-using Silk.NET.Windowing;
-
-namespace AcDream.App.Platform;
-
-internal sealed record GraphicalFramebufferCapabilities(
- int RedBits,
- int GreenBits,
- int BlueBits,
- int AlphaBits,
- int DepthBits,
- int StencilBits,
- int SampleBuffers,
- int Samples,
- bool FramebufferSrgbApi);
-
-internal sealed record GraphicalInputCapabilities(
- int KeyboardCount,
- int MouseCount,
- int GamepadCount,
- int JoystickCount);
-
-internal sealed record GraphicalWindowCapabilities(
- int LogicalWidth,
- int LogicalHeight,
- int FramebufferWidth,
- int FramebufferHeight,
- string MonitorName,
- double RefreshRateHz,
- bool VSync);
-
-internal sealed record GraphicalAudioCapabilities(
- bool Requested,
- bool Available,
- bool PlaybackSubmitted,
- bool DisposalComplete,
- string Backend);
-
-internal sealed record GraphicalSmokeLifecycleCapabilities(
- int OwnedWindowCount,
- int OwnedGlApiCount,
- int OwnedInputContextCount,
- int OwnedAudioEngineCount,
- bool ShutdownComplete);
-
-internal sealed record GraphicalFunctionProbeResult(
- bool BindlessTexture,
- bool ShaderDrawParameters,
- bool MultiDrawIndirect,
- bool ShaderStorageBuffer,
- bool TimerQuery,
- bool SrgbFramebuffer,
- bool? PersistentBufferStorage,
- IReadOnlyList Failures)
-{
- internal static GraphicalFunctionProbeResult NotRun { get; } = new(
- false,
- false,
- false,
- false,
- false,
- false,
- null,
- ["active OpenGL function probe did not run"]);
-}
-
-internal sealed record GraphicalCapabilityRecord(
- DateTimeOffset CapturedAtUtc,
- string RuntimeIdentifier,
- GraphicalHostOperatingSystem OperatingSystem,
- GraphicalDisplayProtocol RequestedDisplayProtocol,
- GraphicalDisplayProtocol ActiveDisplayProtocol,
- string DisplaySelectionReason,
- string WindowBackend,
- string WindowBackendVersion,
- string GlVendor,
- string GlRenderer,
- string GlVersion,
- string GlslVersion,
- int GlMajorVersion,
- int GlMinorVersion,
- int ContextProfileMask,
- int ContextFlags,
- bool HasBindlessTexture,
- bool HasShaderDrawParameters,
- bool HasMultiDrawIndirect,
- bool HasShaderStorageBuffer,
- bool HasBufferStorage,
- bool HasTimerQuery,
- int MaximumShaderStorageBufferBindings,
- int MaximumUniformBufferBindings,
- int MaximumTextureSize,
- int MaximumArrayTextureLayers,
- int MaximumCombinedTextureImageUnits,
- GraphicalFramebufferCapabilities Framebuffer,
- GraphicalInputCapabilities Input,
- GraphicalWindowCapabilities Window,
- GraphicalAudioCapabilities Audio,
- GraphicalSmokeLifecycleCapabilities Lifecycle,
- IReadOnlyList Extensions,
- GraphicalFunctionProbeResult FunctionProbe,
- IReadOnlyList SupportFailures)
-{
- internal bool IsSupported => SupportFailures.Count == 0;
-}
-
-internal static class GraphicalCapabilityRequirements
-{
- internal static IReadOnlyList Evaluate(
- GraphicalCapabilityRecord capabilities)
- {
- ArgumentNullException.ThrowIfNull(capabilities);
- var failures = new List();
-
- if (capabilities.GlMajorVersion < 4
- || capabilities.GlMajorVersion == 4
- && capabilities.GlMinorVersion < 3)
- {
- failures.Add(
- "OpenGL 4.3 core is required; the active context reports " +
- $"{capabilities.GlMajorVersion}.{capabilities.GlMinorVersion}.");
- }
- if (!capabilities.HasBindlessTexture)
- failures.Add("GL_ARB_bindless_texture is required.");
- if (!capabilities.HasShaderDrawParameters)
- failures.Add("GL_ARB_shader_draw_parameters is required.");
- if (!capabilities.HasMultiDrawIndirect)
- failures.Add("multi-draw indirect support is required.");
- if (!capabilities.HasShaderStorageBuffer)
- failures.Add("shader-storage buffers are required.");
- if (!capabilities.HasTimerQuery)
- failures.Add("OpenGL timer queries are required.");
- if (capabilities.Framebuffer.DepthBits < 24)
- {
- failures.Add(
- "the default framebuffer must provide at least 24 depth bits.");
- }
- if (capabilities.Framebuffer.StencilBits < 8)
- {
- failures.Add(
- "the default framebuffer must provide at least 8 stencil bits.");
- }
- if (!capabilities.Framebuffer.FramebufferSrgbApi)
- failures.Add("framebuffer sRGB support is required.");
- if (capabilities.Input.KeyboardCount < 1)
- failures.Add("the graphical input backend exposed no keyboard.");
- if (capabilities.Input.MouseCount < 1)
- failures.Add("the graphical input backend exposed no mouse.");
-
- if (capabilities.FunctionProbe.Failures.Count != 0)
- {
- failures.AddRange(
- capabilities.FunctionProbe.Failures.Select(
- failure => $"OpenGL function probe: {failure}"));
- }
- else
- {
- if (!capabilities.FunctionProbe.BindlessTexture)
- failures.Add("the bindless texture call probe did not pass.");
- if (!capabilities.FunctionProbe.ShaderDrawParameters)
- {
- failures.Add(
- "the shader draw-parameters compile probe did not pass.");
- }
- if (!capabilities.FunctionProbe.MultiDrawIndirect)
- failures.Add("the multi-draw indirect call probe did not pass.");
- if (!capabilities.FunctionProbe.ShaderStorageBuffer)
- failures.Add("the shader-storage buffer call probe did not pass.");
- if (!capabilities.FunctionProbe.TimerQuery)
- failures.Add("the timer-query call probe did not pass.");
- if (!capabilities.FunctionProbe.SrgbFramebuffer)
- failures.Add("the sRGB framebuffer call probe did not pass.");
- if (capabilities.HasBufferStorage
- && capabilities.FunctionProbe.PersistentBufferStorage != true)
- {
- failures.Add(
- "GL_ARB_buffer_storage was advertised but the persistent " +
- "mapping call probe did not pass.");
- }
- }
-
- return failures;
- }
-}
-
-internal static class GraphicalCapabilityProbe
-{
- private const int GlSampleBuffers = 0x80A8;
- private const int GlSamples = 0x80A9;
- private const int GlMaxShaderStorageBufferBindings = 0x90DD;
- private const int GlMaxUniformBufferBindings = 0x8A2F;
- private const int GlMaxCombinedTextureImageUnits = 0x8B4D;
-
- internal static GraphicalCapabilityRecord Capture(
- GL gl,
- IWindow window,
- IInputContext input,
- GraphicalHostPlatformServices platform)
- {
- ArgumentNullException.ThrowIfNull(gl);
- ArgumentNullException.ThrowIfNull(window);
- ArgumentNullException.ThrowIfNull(input);
- ArgumentNullException.ThrowIfNull(platform);
-
- int major = gl.GetInteger(GetPName.MajorVersion);
- int minor = gl.GetInteger(GetPName.MinorVersion);
- bool openGl43 = major > 4 || major == 4 && minor >= 3;
- bool timerQuery =
- major > 3
- || major == 3 && minor >= 3
- || gl.IsExtensionPresent("GL_ARB_timer_query");
- bool framebufferSrgb =
- major > 3
- || major == 3 && minor >= 0
- || gl.IsExtensionPresent("GL_ARB_framebuffer_sRGB")
- || gl.IsExtensionPresent("GL_EXT_framebuffer_sRGB");
-
- IReadOnlyList extensions = ReadExtensions(gl);
- IMonitor? monitor = window.Monitor;
- Silk.NET.Windowing.VideoMode mode =
- monitor?.VideoMode ?? Silk.NET.Windowing.VideoMode.Default;
- var framebuffer = window.FramebufferSize;
- var logical = window.Size;
-
- var captured = new GraphicalCapabilityRecord(
- DateTimeOffset.UtcNow,
- platform.RuntimeIdentifier,
- platform.OperatingSystem,
- platform.WindowBackend.RequestedProtocol,
- GlfwNativePlatformProbe.GetActiveProtocol(
- platform.OperatingSystem),
- platform.WindowBackend.Reason,
- "Silk.NET.Windowing.Glfw",
- GlfwNativePlatformProbe.GetVersion(
- platform.OperatingSystem),
- gl.GetStringS(GLEnum.Vendor),
- gl.GetStringS(GLEnum.Renderer),
- gl.GetStringS(GLEnum.Version),
- gl.GetStringS(GLEnum.ShadingLanguageVersion),
- major,
- minor,
- GetInteger(gl, (GetPName)GLEnum.ContextProfileMask),
- GetInteger(gl, (GetPName)GLEnum.ContextFlags),
- gl.IsExtensionPresent("GL_ARB_bindless_texture"),
- gl.IsExtensionPresent("GL_ARB_shader_draw_parameters"),
- openGl43
- || gl.IsExtensionPresent("GL_ARB_multi_draw_indirect"),
- openGl43
- || gl.IsExtensionPresent("GL_ARB_shader_storage_buffer_object"),
- major > 4
- || major == 4 && minor >= 4
- || gl.IsExtensionPresent("GL_ARB_buffer_storage"),
- timerQuery,
- GetInteger(
- gl,
- (GetPName)GlMaxShaderStorageBufferBindings),
- GetInteger(gl, (GetPName)GlMaxUniformBufferBindings),
- GetInteger(gl, GetPName.MaxTextureSize),
- GetInteger(gl, GetPName.MaxArrayTextureLayers),
- GetInteger(
- gl,
- (GetPName)GlMaxCombinedTextureImageUnits),
- new GraphicalFramebufferCapabilities(
- GetFramebufferAttachmentInteger(
- gl,
- (FramebufferAttachment)GLEnum.BackLeft,
- FramebufferAttachmentParameterName.RedSize),
- GetFramebufferAttachmentInteger(
- gl,
- (FramebufferAttachment)GLEnum.BackLeft,
- FramebufferAttachmentParameterName.GreenSize),
- GetFramebufferAttachmentInteger(
- gl,
- (FramebufferAttachment)GLEnum.BackLeft,
- FramebufferAttachmentParameterName.BlueSize),
- GetFramebufferAttachmentInteger(
- gl,
- (FramebufferAttachment)GLEnum.BackLeft,
- FramebufferAttachmentParameterName.AlphaSize),
- GetFramebufferAttachmentInteger(
- gl,
- (FramebufferAttachment)GLEnum.Depth,
- FramebufferAttachmentParameterName.DepthSize),
- GetFramebufferAttachmentInteger(
- gl,
- (FramebufferAttachment)GLEnum.Stencil,
- FramebufferAttachmentParameterName.StencilSize),
- GetInteger(gl, (GetPName)GlSampleBuffers),
- GetInteger(gl, (GetPName)GlSamples),
- framebufferSrgb),
- new GraphicalInputCapabilities(
- input.Keyboards.Count,
- input.Mice.Count,
- input.Gamepads.Count,
- input.Joysticks.Count),
- new GraphicalWindowCapabilities(
- logical.X,
- logical.Y,
- framebuffer.X,
- framebuffer.Y,
- monitor?.Name ?? "unknown",
- mode.RefreshRate ?? 0,
- window.VSync),
- new GraphicalAudioCapabilities(
- Requested: false,
- Available: false,
- PlaybackSubmitted: false,
- DisposalComplete: true,
- Backend: "not requested"),
- new GraphicalSmokeLifecycleCapabilities(
- OwnedWindowCount: 1,
- OwnedGlApiCount: 1,
- OwnedInputContextCount: 1,
- OwnedAudioEngineCount: 0,
- ShutdownComplete: false),
- extensions,
- GraphicalFunctionProbeResult.NotRun,
- []);
- return captured with
- {
- SupportFailures =
- GraphicalCapabilityRequirements.Evaluate(captured),
- };
- }
-
- internal static GraphicalCapabilityRecord WithFunctionProbe(
- GraphicalCapabilityRecord capabilities,
- GraphicalFunctionProbeResult functionProbe)
- {
- ArgumentNullException.ThrowIfNull(capabilities);
- ArgumentNullException.ThrowIfNull(functionProbe);
- GraphicalCapabilityRecord updated = capabilities with
- {
- FunctionProbe = functionProbe,
- SupportFailures = [],
- };
- return updated with
- {
- SupportFailures =
- GraphicalCapabilityRequirements.Evaluate(updated),
- };
- }
-
- private static int GetInteger(GL gl, GetPName name)
- {
- return GlResourceCommand.Execute(
- gl,
- $"query OpenGL integer 0x{(uint)name:X}",
- () =>
- {
- gl.GetInteger(name, out int value);
- return value;
- });
- }
-
- private static int GetFramebufferAttachmentInteger(
- GL gl,
- FramebufferAttachment attachment,
- FramebufferAttachmentParameterName name)
- {
- return GlResourceCommand.Execute(
- gl,
- $"query default framebuffer attachment {attachment}/{name}",
- () =>
- {
- gl.GetFramebufferAttachmentParameter(
- FramebufferTarget.Framebuffer,
- attachment,
- name,
- out int value);
- return value;
- });
- }
-
- private static IReadOnlyList ReadExtensions(GL gl)
- {
- int count = GetInteger(gl, GetPName.NumExtensions);
- var extensions = new string[Math.Max(0, count)];
- for (uint index = 0; index < extensions.Length; index++)
- extensions[index] = gl.GetStringS(GLEnum.Extensions, index);
- Array.Sort(extensions, StringComparer.Ordinal);
- return extensions;
- }
-}
-
-internal static class GraphicalCapabilityGuard
-{
- internal static GraphicalCapabilityRecord CaptureVerifyAndWrite(
- GL gl,
- IWindow window,
- IInputContext input,
- GraphicalHostPlatformServices platform,
- string reportPath)
- {
- GraphicalCapabilityRecord passive =
- GraphicalCapabilityProbe.Capture(
- gl,
- window,
- input,
- platform);
- GraphicalFunctionProbeResult functions =
- GraphicalGlFunctionProbe.Run(gl, passive);
- GraphicalCapabilityRecord verified =
- GraphicalCapabilityProbe.WithFunctionProbe(
- passive,
- functions);
- GraphicalCapabilityReportWriter.Write(reportPath, verified);
-
- return verified;
- }
-
- internal static void ThrowIfUnsupported(
- GraphicalCapabilityRecord capabilities,
- string reportPath)
- {
- ArgumentNullException.ThrowIfNull(capabilities);
- if (!capabilities.IsSupported)
- {
- throw new NotSupportedException(
- FormatUnsupportedMessage(capabilities, reportPath));
- }
- }
-
- internal static string FormatUnsupportedMessage(
- GraphicalCapabilityRecord capabilities,
- string reportPath)
- {
- ArgumentNullException.ThrowIfNull(capabilities);
- ArgumentException.ThrowIfNullOrWhiteSpace(reportPath);
- return
- "acdream's mandatory modern renderer is unsupported by the " +
- "active graphical backend.\n" +
- $"Platform: {capabilities.RuntimeIdentifier}, " +
- $"{capabilities.ActiveDisplayProtocol}, " +
- $"{capabilities.GlVendor} / {capabilities.GlRenderer}, " +
- $"{capabilities.GlVersion}\n" +
- string.Join(
- "\n",
- capabilities.SupportFailures.Select(
- failure => $" - {failure}")) +
- $"\nFull capability report: {Path.GetFullPath(reportPath)}";
- }
-}
-
-internal static class GraphicalCapabilityReportWriter
-{
- private static readonly JsonSerializerOptions Options = new()
- {
- WriteIndented = true,
- Converters =
- {
- new JsonStringEnumConverter(),
- },
- };
-
- internal static void Write(
- string path,
- GraphicalCapabilityRecord capabilities)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(path);
- ArgumentNullException.ThrowIfNull(capabilities);
- string fullPath = Path.GetFullPath(path);
- string? directory = Path.GetDirectoryName(fullPath);
- if (!string.IsNullOrEmpty(directory))
- Directory.CreateDirectory(directory);
-
- string temporaryPath = fullPath + ".tmp";
- File.WriteAllText(
- temporaryPath,
- JsonSerializer.Serialize(capabilities, Options));
- File.Move(temporaryPath, fullPath, overwrite: true);
- }
-}
-
-internal static unsafe class GlfwNativePlatformProbe
-{
- private const int GlfwWin32Platform = 0x00060001;
- private const int GlfwWaylandPlatform = 0x00060003;
- private const int GlfwX11Platform = 0x00060004;
-
- internal static GraphicalDisplayProtocol GetActiveProtocol(
- GraphicalHostOperatingSystem operatingSystem)
- {
- if (operatingSystem == GraphicalHostOperatingSystem.Windows)
- return GraphicalDisplayProtocol.Windows;
-
- if (!GraphicalWindowBackendConfigurator.TryGetConfiguredApi(
- out Glfw? glfw)
- || glfw is null
- || !glfw.Context.TryGetProcAddress(
- "glfwGetPlatform",
- out nint export))
- {
- return GraphicalDisplayProtocol.Unknown;
- }
-
- int platform =
- ((delegate* unmanaged[Cdecl])export)();
- return platform switch
- {
- GlfwX11Platform => GraphicalDisplayProtocol.X11,
- GlfwWaylandPlatform => GraphicalDisplayProtocol.Wayland,
- GlfwWin32Platform => GraphicalDisplayProtocol.Windows,
- _ => GraphicalDisplayProtocol.Unknown,
- };
- }
-
- internal static string GetVersion(
- GraphicalHostOperatingSystem operatingSystem)
- {
- if (!GraphicalWindowBackendConfigurator.TryGetConfiguredApi(
- out Glfw? glfw)
- || glfw is null)
- {
- return "unknown";
- }
-
- return glfw.GetVersionString() ?? "unknown";
- }
-}
diff --git a/src/AcDream.App/Platform/GraphicalGlFunctionProbe.cs b/src/AcDream.App/Platform/GraphicalGlFunctionProbe.cs
deleted file mode 100644
index c9f5a95a..00000000
--- a/src/AcDream.App/Platform/GraphicalGlFunctionProbe.cs
+++ /dev/null
@@ -1,513 +0,0 @@
-using AcDream.App.Rendering;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Platform;
-
-///
-/// Executes one minimal, ownership-balanced call path for every OpenGL feature
-/// the mandatory renderer relies upon. Advertisement alone is insufficient:
-/// several Linux driver failures present an extension string while returning
-/// a missing entry point, invalid bindless handle, or unusable framebuffer.
-///
-internal static unsafe class GraphicalGlFunctionProbe
-{
- private const string VertexSource = """
- #version 430 core
- #extension GL_ARB_shader_draw_parameters : require
- void main()
- {
- float identity =
- float(gl_DrawIDARB + gl_BaseInstanceARB) * 0.0;
- gl_Position = vec4(identity, 0.0, 0.0, 1.0);
- }
- """;
-
- private const string FragmentSource = """
- #version 430 core
- layout(location = 0) out vec4 outColor;
- void main()
- {
- outColor = vec4(1.0);
- }
- """;
-
- internal static GraphicalFunctionProbeResult Run(
- GL gl,
- GraphicalCapabilityRecord capabilities)
- {
- ArgumentNullException.ThrowIfNull(gl);
- ArgumentNullException.ThrowIfNull(capabilities);
- var failures = new List();
-
- bool bindless = capabilities.HasBindlessTexture
- && Attempt(
- "bindless texture handle/residency",
- () => ProbeBindless(gl),
- failures);
-
- bool shaderDraw = capabilities.HasShaderDrawParameters
- && Attempt(
- "shader draw parameters and multi-draw indirect",
- () => ProbeShaderDrawAndMultiDraw(gl),
- failures);
- bool multiDraw = shaderDraw
- && capabilities.HasMultiDrawIndirect;
-
- bool shaderStorage = capabilities.HasShaderStorageBuffer
- && Attempt(
- "shader-storage buffer",
- () => ProbeShaderStorageBuffer(gl),
- failures);
-
- bool timer = capabilities.HasTimerQuery
- && Attempt(
- "timer query",
- () => ProbeTimerQuery(gl),
- failures);
-
- bool srgb = capabilities.Framebuffer.FramebufferSrgbApi
- && Attempt(
- "sRGB depth/stencil framebuffer",
- () => ProbeSrgbFramebuffer(gl),
- failures);
-
- bool? persistent = capabilities.HasBufferStorage
- ? Attempt(
- "persistent coherent buffer storage",
- () => ProbePersistentBufferStorage(gl),
- failures)
- : null;
-
- if (!capabilities.HasBindlessTexture)
- failures.Add("GL_ARB_bindless_texture was not advertised.");
- if (!capabilities.HasShaderDrawParameters)
- failures.Add("GL_ARB_shader_draw_parameters was not advertised.");
- if (!capabilities.HasMultiDrawIndirect)
- failures.Add("multi-draw indirect was not advertised.");
- if (!capabilities.HasShaderStorageBuffer)
- failures.Add("shader-storage buffers were not advertised.");
- if (!capabilities.HasTimerQuery)
- failures.Add("timer queries were not advertised.");
- if (!capabilities.Framebuffer.FramebufferSrgbApi)
- failures.Add("framebuffer sRGB was not advertised.");
-
- return new GraphicalFunctionProbeResult(
- bindless,
- shaderDraw,
- multiDraw,
- shaderStorage,
- timer,
- srgb,
- persistent,
- failures);
- }
-
- private static bool Attempt(
- string name,
- Action action,
- List failures)
- {
- try
- {
- action();
- return true;
- }
- catch (Exception error)
- {
- failures.Add($"{name}: {error.GetType().Name}: {error.Message}");
- return false;
- }
- }
-
- private static void ProbeBindless(GL gl)
- {
- if (!BindlessSupport.TryCreate(gl, out BindlessSupport? bindless)
- || bindless is null)
- {
- throw new NotSupportedException(
- "the ARB bindless extension object could not be loaded.");
- }
-
- uint texture = GlResourceCommand.CreateTexture(
- gl,
- "graphical capability bindless texture");
- ulong handle = 0;
- try
- {
- byte* pixel = stackalloc byte[4] { 255, 255, 255, 255 };
- GlResourceCommand.Execute(
- gl,
- "initialize graphical capability bindless texture",
- () =>
- {
- gl.BindTexture(TextureTarget.Texture2D, texture);
- gl.TexImage2D(
- TextureTarget.Texture2D,
- 0,
- InternalFormat.Rgba8,
- 1,
- 1,
- 0,
- PixelFormat.Rgba,
- PixelType.UnsignedByte,
- pixel);
- gl.TexParameter(
- TextureTarget.Texture2D,
- TextureParameterName.TextureMinFilter,
- (int)TextureMinFilter.Nearest);
- gl.TexParameter(
- TextureTarget.Texture2D,
- TextureParameterName.TextureMagFilter,
- (int)TextureMagFilter.Nearest);
- });
- handle = bindless.GetResidentHandle(texture);
- bindless.MakeNonResident(handle);
- handle = 0;
- }
- finally
- {
- if (handle != 0)
- bindless.MakeNonResident(handle);
- gl.BindTexture(TextureTarget.Texture2D, 0);
- GlResourceCommand.DeleteTexture(
- gl,
- texture,
- "delete graphical capability bindless texture");
- }
- }
-
- private static void ProbeShaderDrawAndMultiDraw(GL gl)
- {
- uint program = ShaderProgramConstruction.Build(
- new GlShaderProgramBuildApi(gl),
- VertexSource,
- FragmentSource);
- uint vertexArray = 0;
- uint elementBuffer = 0;
- uint indirectBuffer = 0;
- try
- {
- vertexArray = GlResourceCommand.CreateName(
- gl,
- "graphical capability vertex array",
- gl.GenVertexArray,
- gl.DeleteVertexArray);
- elementBuffer = GlResourceCommand.CreateName(
- gl,
- "graphical capability element buffer",
- gl.GenBuffer,
- gl.DeleteBuffer);
- indirectBuffer = GlResourceCommand.CreateName(
- gl,
- "graphical capability indirect buffer",
- gl.GenBuffer,
- gl.DeleteBuffer);
-
- uint* index = stackalloc uint[1] { 0 };
- uint* command = stackalloc uint[5]
- {
- 0,
- 1,
- 0,
- 0,
- 0,
- };
- GLHelpers.ThrowOnResourceError(
- gl,
- "execute graphical capability multi-draw indirect call " +
- "(precondition)");
- gl.UseProgram(program);
- gl.BindVertexArray(vertexArray);
- gl.BindBuffer(
- BufferTargetARB.ElementArrayBuffer,
- elementBuffer);
- gl.BufferData(
- BufferTargetARB.ElementArrayBuffer,
- (nuint)sizeof(uint),
- index,
- BufferUsageARB.StaticDraw);
- gl.BindBuffer(
- BufferTargetARB.DrawIndirectBuffer,
- indirectBuffer);
- gl.BufferData(
- BufferTargetARB.DrawIndirectBuffer,
- (nuint)(sizeof(uint) * 5),
- command,
- BufferUsageARB.StaticDraw);
- gl.MultiDrawElementsIndirect(
- PrimitiveType.Triangles,
- DrawElementsType.UnsignedInt,
- null,
- 1,
- 0);
- GLHelpers.ThrowOnResourceError(
- gl,
- "execute graphical capability multi-draw indirect call");
- }
- finally
- {
- gl.UseProgram(0);
- gl.BindVertexArray(0);
- gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, 0);
- if (indirectBuffer != 0)
- {
- GlResourceCommand.DeleteBuffer(
- gl,
- indirectBuffer,
- "delete graphical capability indirect buffer");
- }
- if (elementBuffer != 0)
- {
- GlResourceCommand.DeleteBuffer(
- gl,
- elementBuffer,
- "delete graphical capability element buffer");
- }
- if (vertexArray != 0)
- {
- GlResourceCommand.DeleteVertexArray(
- gl,
- vertexArray,
- "delete graphical capability vertex array");
- }
- GlResourceCommand.DeleteProgram(
- gl,
- program,
- "delete graphical capability shader program");
- }
- }
-
- private static void ProbeShaderStorageBuffer(GL gl)
- {
- uint buffer = GlResourceCommand.CreateName(
- gl,
- "graphical capability shader-storage buffer",
- gl.GenBuffer,
- gl.DeleteBuffer);
- try
- {
- uint* value = stackalloc uint[1] { 0xACD0_0001u };
- GLHelpers.ThrowOnResourceError(
- gl,
- "bind graphical capability shader-storage buffer " +
- "(precondition)");
- gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, buffer);
- gl.BufferData(
- BufferTargetARB.ShaderStorageBuffer,
- (nuint)sizeof(uint),
- value,
- BufferUsageARB.StaticDraw);
- gl.BindBufferBase(
- BufferTargetARB.ShaderStorageBuffer,
- 0,
- buffer);
- gl.BindBufferBase(
- BufferTargetARB.ShaderStorageBuffer,
- 0,
- 0);
- GLHelpers.ThrowOnResourceError(
- gl,
- "bind graphical capability shader-storage buffer");
- }
- finally
- {
- gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, 0);
- GlResourceCommand.DeleteBuffer(
- gl,
- buffer,
- "delete graphical capability shader-storage buffer");
- }
- }
-
- private static void ProbeTimerQuery(GL gl)
- {
- uint query = GlResourceCommand.CreateName(
- gl,
- "graphical capability timer query",
- gl.GenQuery,
- gl.DeleteQuery);
- try
- {
- GlResourceCommand.Execute(
- gl,
- "execute graphical capability timer query",
- () =>
- {
- gl.BeginQuery(QueryTarget.TimeElapsed, query);
- gl.EndQuery(QueryTarget.TimeElapsed);
- });
- }
- finally
- {
- GlResourceCommand.Execute(
- gl,
- "delete graphical capability timer query",
- () => gl.DeleteQuery(query));
- }
- }
-
- private static void ProbeSrgbFramebuffer(GL gl)
- {
- uint texture = 0;
- uint depthStencil = 0;
- uint framebuffer = 0;
- try
- {
- texture = GlResourceCommand.CreateTexture(
- gl,
- "graphical capability sRGB color texture");
- depthStencil = GlResourceCommand.CreateName(
- gl,
- "graphical capability depth/stencil renderbuffer",
- gl.GenRenderbuffer,
- gl.DeleteRenderbuffer);
- framebuffer = GlResourceCommand.CreateName(
- gl,
- "graphical capability framebuffer",
- gl.GenFramebuffer,
- gl.DeleteFramebuffer);
-
- GlResourceCommand.Execute(
- gl,
- "configure graphical capability sRGB framebuffer",
- () =>
- {
- gl.BindTexture(TextureTarget.Texture2D, texture);
- gl.TexImage2D(
- TextureTarget.Texture2D,
- 0,
- InternalFormat.Srgb8Alpha8,
- 2,
- 2,
- 0,
- PixelFormat.Rgba,
- PixelType.UnsignedByte,
- null);
-
- gl.BindRenderbuffer(
- RenderbufferTarget.Renderbuffer,
- depthStencil);
- gl.RenderbufferStorage(
- RenderbufferTarget.Renderbuffer,
- InternalFormat.Depth24Stencil8,
- 2,
- 2);
-
- gl.BindFramebuffer(
- FramebufferTarget.Framebuffer,
- framebuffer);
- gl.FramebufferTexture2D(
- FramebufferTarget.Framebuffer,
- FramebufferAttachment.ColorAttachment0,
- TextureTarget.Texture2D,
- texture,
- 0);
- gl.FramebufferRenderbuffer(
- FramebufferTarget.Framebuffer,
- FramebufferAttachment.DepthStencilAttachment,
- RenderbufferTarget.Renderbuffer,
- depthStencil);
- if (gl.CheckFramebufferStatus(
- FramebufferTarget.Framebuffer)
- != GLEnum.FramebufferComplete)
- {
- throw new InvalidOperationException(
- "the sRGB depth/stencil framebuffer is incomplete.");
- }
-
- gl.Enable(EnableCap.FramebufferSrgb);
- gl.Clear(
- ClearBufferMask.ColorBufferBit
- | ClearBufferMask.DepthBufferBit
- | ClearBufferMask.StencilBufferBit);
- gl.Disable(EnableCap.FramebufferSrgb);
- });
- }
- finally
- {
- gl.Disable(EnableCap.FramebufferSrgb);
- gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
- gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0);
- gl.BindTexture(TextureTarget.Texture2D, 0);
- if (framebuffer != 0)
- {
- GlResourceCommand.Execute(
- gl,
- "delete graphical capability framebuffer",
- () => gl.DeleteFramebuffer(framebuffer));
- }
- if (depthStencil != 0)
- {
- GlResourceCommand.Execute(
- gl,
- "delete graphical capability depth/stencil renderbuffer",
- () => gl.DeleteRenderbuffer(depthStencil));
- }
- if (texture != 0)
- {
- GlResourceCommand.DeleteTexture(
- gl,
- texture,
- "delete graphical capability sRGB texture");
- }
- }
- }
-
- private static void ProbePersistentBufferStorage(GL gl)
- {
- uint buffer = GlResourceCommand.CreateName(
- gl,
- "graphical capability persistent buffer",
- gl.GenBuffer,
- gl.DeleteBuffer);
- void* mapped = null;
- try
- {
- GlResourceCommand.Execute(
- gl,
- "allocate graphical capability persistent buffer",
- () =>
- {
- gl.BindBuffer(BufferTargetARB.ArrayBuffer, buffer);
- gl.BufferStorage(
- GLEnum.ArrayBuffer,
- 64,
- null,
- (uint)(
- BufferStorageMask.MapWriteBit
- | BufferStorageMask.MapPersistentBit
- | BufferStorageMask.MapCoherentBit
- | BufferStorageMask.DynamicStorageBit));
- mapped = gl.MapBufferRange(
- BufferTargetARB.ArrayBuffer,
- 0,
- 64,
- MapBufferAccessMask.WriteBit
- | MapBufferAccessMask.PersistentBit
- | MapBufferAccessMask.CoherentBit);
- if (mapped is null)
- {
- throw new InvalidOperationException(
- "persistent mapping returned a null pointer.");
- }
- *((byte*)mapped) = 0x5A;
- });
- }
- finally
- {
- if (mapped is not null)
- {
- GlResourceCommand.Execute(
- gl,
- "unmap graphical capability persistent buffer",
- () => gl.UnmapBuffer(BufferTargetARB.ArrayBuffer));
- }
- gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
- GlResourceCommand.DeleteBuffer(
- gl,
- buffer,
- "delete graphical capability persistent buffer");
- }
- }
-}
diff --git a/src/AcDream.App/Platform/GraphicalWindowBackendSelection.cs b/src/AcDream.App/Platform/GraphicalWindowBackendSelection.cs
index 55931dce..15810643 100644
--- a/src/AcDream.App/Platform/GraphicalWindowBackendSelection.cs
+++ b/src/AcDream.App/Platform/GraphicalWindowBackendSelection.cs
@@ -187,3 +187,58 @@ internal static class GraphicalWindowBackendConfigurator
DefaultPathResolver.BaseDirectoryResolver);
}
}
+
+///
+/// Reads the GLFW platform actually selected at runtime (as opposed to the one
+/// requested — GLFW's Automatic hint can resolve to either X11 or
+/// Wayland). Moved here from the deleted (Campaign V slice V11) raw-GL
+/// GraphicalCapabilityRecord.cs:
+/// depends on this for its own capability report, so it survived the GL arm
+/// that used to sit alongside it.
+///
+internal static unsafe class GlfwNativePlatformProbe
+{
+ private const int GlfwWin32Platform = 0x00060001;
+ private const int GlfwWaylandPlatform = 0x00060003;
+ private const int GlfwX11Platform = 0x00060004;
+
+ internal static GraphicalDisplayProtocol GetActiveProtocol(
+ GraphicalHostOperatingSystem operatingSystem)
+ {
+ if (operatingSystem == GraphicalHostOperatingSystem.Windows)
+ return GraphicalDisplayProtocol.Windows;
+
+ if (!GraphicalWindowBackendConfigurator.TryGetConfiguredApi(
+ out Glfw? glfw)
+ || glfw is null
+ || !glfw.Context.TryGetProcAddress(
+ "glfwGetPlatform",
+ out nint export))
+ {
+ return GraphicalDisplayProtocol.Unknown;
+ }
+
+ int platform =
+ ((delegate* unmanaged[Cdecl])export)();
+ return platform switch
+ {
+ GlfwX11Platform => GraphicalDisplayProtocol.X11,
+ GlfwWaylandPlatform => GraphicalDisplayProtocol.Wayland,
+ GlfwWin32Platform => GraphicalDisplayProtocol.Windows,
+ _ => GraphicalDisplayProtocol.Unknown,
+ };
+ }
+
+ internal static string GetVersion(
+ GraphicalHostOperatingSystem operatingSystem)
+ {
+ if (!GraphicalWindowBackendConfigurator.TryGetConfiguredApi(
+ out Glfw? glfw)
+ || glfw is null)
+ {
+ return "unknown";
+ }
+
+ return glfw.GetVersionString() ?? "unknown";
+ }
+}
diff --git a/src/AcDream.App/RenderBackendKind.cs b/src/AcDream.App/RenderBackendKind.cs
deleted file mode 100644
index 64e5ad31..00000000
--- a/src/AcDream.App/RenderBackendKind.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace AcDream.App;
-
-///
-/// Campaign V slice V5: which rendering backend the graphical host starts.
-///
-/// Slice V10 flipped the default. is what an unset
-/// ACDREAM_RENDER_BACKEND now selects; remains reachable
-/// for one slice by setting that variable to gl, and slice V11 deletes it
-/// along with the escape hatch.
-///
-/// This enum is public only because is public and
-/// exposes it as a property. The backend-neutral
-/// AcDream.App.Rendering.Gpu.GpuBackendKind stays internal and describes a
-/// live device rather than a startup request; the two are deliberately separate
-/// because a Vulkan-requested process can still fail its capability gate and
-/// never own a Vulkan device at all.
-///
-public enum RenderBackendKind
-{
- ///
- /// OpenGL 4.3 core + bindless + MDI. The escape hatch after slice V10,
- /// reachable only by ACDREAM_RENDER_BACKEND=gl, deleted at V11.
- ///
- Gl,
-
- /// Vulkan 1.3 core. The default backend as of Campaign V slice V10.
- Vulkan,
-}
diff --git a/src/AcDream.App/Rendering/BindlessTextureMutationGuard.cs b/src/AcDream.App/Rendering/BindlessTextureMutationGuard.cs
deleted file mode 100644
index f3b5d2f0..00000000
--- a/src/AcDream.App/Rendering/BindlessTextureMutationGuard.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System.Runtime.ExceptionServices;
-
-namespace AcDream.App.Rendering;
-
-///
-/// Temporarily removes bindless residency around texture-state mutation while
-/// retaining the obligation to restore the pair across failed mutation or
-/// failed reacquisition attempts.
-///
-internal sealed class BindlessTextureMutationGuard(BindlessTexturePair pair)
-{
- private bool _restoreRequired;
- private bool _operationActive;
-
- internal bool RestoreRequired => _restoreRequired;
-
- public void Execute(Action mutation)
- {
- ArgumentNullException.ThrowIfNull(mutation);
- if (_operationActive)
- throw new InvalidOperationException("A bindless texture mutation is already active.");
-
- _operationActive = true;
- Exception? mutationFailure = null;
- Exception? restoreFailure = null;
- try
- {
- _restoreRequired |= pair.HasAnyResident;
- if (pair.HasAnyResident)
- pair.Release();
-
- mutation();
- }
- catch (Exception failure)
- {
- mutationFailure = failure;
- }
- finally
- {
- if (_restoreRequired)
- {
- try
- {
- _ = pair.Acquire();
- _restoreRequired = false;
- }
- catch (Exception failure)
- {
- restoreFailure = failure;
- }
- }
-
- _operationActive = false;
- }
-
- if (mutationFailure is not null && restoreFailure is not null)
- {
- throw new AggregateException(
- "Texture-state mutation failed and bindless residency could not be restored.",
- mutationFailure,
- restoreFailure);
- }
- if (mutationFailure is not null)
- ExceptionDispatchInfo.Capture(mutationFailure).Throw();
- if (restoreFailure is not null)
- ExceptionDispatchInfo.Capture(restoreFailure).Throw();
- }
-}
diff --git a/src/AcDream.App/Rendering/BindlessTexturePair.cs b/src/AcDream.App/Rendering/BindlessTexturePair.cs
deleted file mode 100644
index 2b0cd50b..00000000
--- a/src/AcDream.App/Rendering/BindlessTexturePair.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-namespace AcDream.App.Rendering;
-
-///
-/// Owns the two independent residency edges used by TerrainAtlas. Acquisition
-/// and release publish each successful edge immediately, so a partial failure
-/// retries only work that is still pending.
-///
-internal sealed class BindlessTexturePair(
- uint firstTexture,
- uint secondTexture,
- Func acquire,
- Action release)
-{
- private ulong _firstHandle;
- private ulong _secondHandle;
- private bool _firstResident;
- private bool _secondResident;
-
- public bool IsFullyResident => _firstResident && _secondResident;
- public bool HasAnyResident => _firstResident || _secondResident;
-
- public (ulong First, ulong Second) Acquire()
- {
- if (!_firstResident)
- {
- _firstHandle = acquire(firstTexture);
- _firstResident = true;
- }
-
- if (!_secondResident)
- {
- try
- {
- _secondHandle = acquire(secondTexture);
- _secondResident = true;
- }
- catch (Exception acquisitionFailure)
- {
- try
- {
- Release();
- }
- catch (Exception cleanupFailure)
- {
- throw new AggregateException(
- "Second bindless texture acquisition failed and the resident prefix did not cleanly roll back.",
- acquisitionFailure,
- cleanupFailure);
- }
-
- throw;
- }
- }
-
- return (_firstHandle, _secondHandle);
- }
-
- public void Release()
- {
- List? failures = null;
- if (_firstResident)
- {
- try
- {
- release(_firstHandle);
- _firstResident = false;
- _firstHandle = 0;
- }
- catch (Exception failure)
- {
- (failures ??= []).Add(failure);
- }
- }
-
- if (_secondResident)
- {
- try
- {
- release(_secondHandle);
- _secondResident = false;
- _secondHandle = 0;
- }
- catch (Exception failure)
- {
- (failures ??= []).Add(failure);
- }
- }
-
- if (failures is not null)
- throw new AggregateException(
- "One or more bindless texture handles could not be made non-resident.",
- failures);
- }
-}
diff --git a/src/AcDream.App/Rendering/ClipFrame.cs b/src/AcDream.App/Rendering/ClipFrame.cs
index cf65065a..7f8020f1 100644
--- a/src/AcDream.App/Rendering/ClipFrame.cs
+++ b/src/AcDream.App/Rendering/ClipFrame.cs
@@ -1,8 +1,8 @@
// ClipFrame.cs
//
-// Phase U.3: the GPU-side container + uploader for the SHARED per-frame clip
-// data consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert
-// (UBO binding=2). This is the "shared" half of the U.3 clip mechanism; the
+// Phase U.3: the per-frame container for the SHARED per-frame clip data
+// consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert (UBO
+// binding=2). This is the "shared" half of the U.3 clip mechanism; the
// per-instance slot index buffer (SSBO binding=3) is PER-RENDERER and owned by
// each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance
// buffer — it is NOT here.
@@ -22,21 +22,23 @@
// terrain OutsideView planes, then points each renderer's per-instance slot
// buffer at the right slots.
//
-// Pure CPU byte-packing + a thin GL upload. The byte layout is asserted by
-// ClipFrameLayoutTests so a silent
-// std430/std140 drift can't reach the GPU.
+// Pure CPU byte-packing. The GL upload machinery this file used to carry
+// alongside the packing (a per-flight-slot region SSBO + terrain UBO arena,
+// reservation/upload-once bookkeeping, and their disposal) was deleted at
+// Campaign V slice V11: the RHI arm (see RhiWorldPassSurface.PrepareClipFrame /
+// SetTerrainClip in WorldPassSurface.cs) reads RegionBytes/TerrainBytes below and
+// copies them into a frame ring allocation instead, so nothing here owns a GPU
+// resource anymore. The byte layout is asserted by ClipFrameLayoutTests so a
+// silent std430/std140 drift can't reach the GPU.
using System;
-using System.Collections.Generic;
using System.Numerics;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
///
-/// Per-frame container + uploader for the SHARED clip data: the binding=2 mesh
-/// SSBO (one CellClip per slot, slot 0 reserved no-clip) and the binding=2
-/// terrain UBO (the single OutsideView region). See the file header for the exact
+/// Per-frame container for the SHARED clip data: the binding=2 mesh SSBO (one
+/// CellClip per slot, slot 0 reserved no-clip) and the binding=2 terrain
+/// UBO (the single OutsideView region). See the file header for the exact
/// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by
/// each renderer, not here.
///
@@ -61,13 +63,12 @@ public sealed class ClipFrame : IDisposable
/// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family.
public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144
- /// SSBO binding index for the shared per-cell clip regions
- /// (mesh_modern.vert binding=2).
- public const uint MeshClipSsboBinding = 2;
-
/// UBO binding index for the terrain OutsideView clip region
- /// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO
- /// binding=2 above.
+ /// (terrain_modern.vert binding=2). Read directly by both the RHI world-pass
+ /// section binder (WorldFrameSectionBinding.BindTerrainClip) and
+ /// PortalDepthMaskRenderer, so unlike the mesh SSBO binding (which the
+ /// RHI arm addresses through its own GpuBindingModel.StorageClipRegions
+ /// instead) this one is still genuinely shared.
public const uint TerrainClipUboBinding = 2;
// ---- CPU-side state ------------------------------------------------------
@@ -79,55 +80,14 @@ public sealed class ClipFrame : IDisposable
// Packed std140 bytes for the terrain UBO (always TerrainUboBytes long).
private readonly byte[] _terrainBytes = new byte[TerrainUboBytes];
- // ---- GL-side state (lazily created on first upload) ----------------------
-
- private uint _regionSsbo;
- private uint _terrainUbo;
- private RetryableResourceReleaseLedger? _disposeResources;
- private bool _disposing;
- private bool _disposed;
-
- internal const int FrameSlotCount = GpuFrameFlightController.DefaultMaximumFramesInFlight;
-
- private sealed class RegionBuffer
- {
- public uint Name;
- public int CapacityBytes;
- public ClipBufferCapacityPolicy CapacityPolicy;
- }
-
- private sealed class TerrainBufferArena
- {
- public uint Name;
- public int CapacityBytes;
- public ClipBufferCapacityPolicy CapacityPolicy;
- }
-
- // A full clip-region table is immutable once ClipFrameAssembler has packed
- // the frame. Keep exactly one SSBO per GPU-fenced frame slot. Terrain clip
- // state changes between outside-view slices, so each frame slot owns one
- // UBO arena and each draw binds a distinct aligned range within that arena.
- // This bounds native object retention at six buffers total instead of one
- // region/terrain pair per outside-view slice.
- private readonly ClipFrameResourceRing _regionBuffers =
- new(FrameSlotCount);
- private readonly ClipFrameResourceRing _terrainBuffers =
- new(FrameSlotCount);
- private readonly ClipFrameUploadState _uploadState = new();
- private int _dynamicFrameSlot;
- private bool _dynamicFrameStarted;
- private int _terrainRecordStrideBytes;
- private TerrainClipBufferBinding _terrainBinding;
-
- internal int DynamicBufferSetCount =>
- Math.Max(_regionBuffers.Count, _terrainBuffers.Count);
- internal int RegionBufferCount => _regionBuffers.Count;
- internal int TerrainBufferCount => _terrainBuffers.Count;
- internal int TerrainRecordStrideBytes => _terrainRecordStrideBytes;
-
- // GL reference captured on the first upload so Dispose can delete each
- // frame-slot buffer. ClipFrame is long-lived and owns their teardown.
- private GL? _gl;
+ ///
+ /// The GL arm's per-flight-slot region/terrain buffer rings this used to
+ /// report on were deleted at Campaign V slice V11: every publication now
+ /// comes from the current frame's own ring allocator (see
+ /// RhiWorldPassSurface.Publish), which lives until its frame retires,
+ /// so there is no persistent dynamic-buffer pool left to size.
+ ///
+ internal int DynamicBufferSetCount => 0;
private ClipFrame(byte[] regionBytes, int slotCount)
{
@@ -156,9 +116,10 @@ public sealed class ClipFrame : IDisposable
///
/// Phase U.4: reset this frame back to the NoClip state — exactly slot 0
/// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new
- /// frame or new GL buffers. The single long-lived _clipFrame in
- /// GameWindow is reset + re-packed every frame by ,
- /// then uploaded through one SSBO and one terrain arena per fenced frame slot.
+ /// frame. The single long-lived _clipFrame in GameWindow is reset +
+ /// re-packed every frame by , then published
+ /// through one frame-ring allocation per section (see
+ /// RhiWorldPassSurface.PrepareClipFrame).
///
public void Reset()
{
@@ -175,35 +136,18 @@ public sealed class ClipFrame : IDisposable
Array.Clear(_terrainBytes);
}
- /// The shared mesh-clip SSBO id, or 0 before the first
- /// . Renderers may bind this directly if they don't
- /// receive it via a parameter; already binds it to
- /// .
- public uint RegionSsbo => _regionSsbo;
-
- /// The terrain-clip UBO id, or 0 before the first
- /// . The buffer alone does not identify the
- /// active range; consumers should use .
- public uint TerrainUbo => _terrainUbo;
-
- /// The currently installed terrain-clip range. The buffer name is
- /// stable for the current GPU-fenced frame slot; the offset advances once
- /// for every outside-view draw.
- public TerrainClipBufferBinding TerrainBinding => _terrainBinding;
-
///
- /// Selects one GPU-fenced frame slot. Its region SSBO and terrain arena are
- /// safe to reuse because the frame-flight controller already waited for the
- /// slot's preceding submission.
+ /// Per-frame hook kept for the shared render-frame-begin ordering — it is
+ /// called once per GPU-fenced frame slot alongside every other renderer's
+ /// BeginFrame, regardless of backend. The GL arm's frame-slot-scoped
+ /// region SSBO + terrain UBO arena this used to select was deleted at
+ /// Campaign V slice V11: the RHI arm takes a fresh ring allocation from the
+ /// current GPU frame for every publication instead, so there is no
+ /// frame-slot state left here to reset — only the argument to validate.
///
public void BeginFrame(int frameSlot)
{
- if ((uint)frameSlot >= FrameSlotCount)
- throw new ArgumentOutOfRangeException(nameof(frameSlot));
- _dynamicFrameSlot = frameSlot;
- _dynamicFrameStarted = true;
- _terrainBinding = default;
- _uploadState.BeginFrame();
+ ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
}
///
@@ -272,266 +216,14 @@ public sealed class ClipFrame : IDisposable
}
///
- /// Compatibility entry point for one-region/one-terrain callers. Complex
- /// PView frames should reserve their complete terrain sequence, upload the
- /// regions once, then call per slice.
+ /// No-op: this container owns no GPU resource of its own on the RHI arm —
+ /// the deleted GL arm's per-frame-slot region SSBO + terrain UBO arena was
+ /// the only thing to release. Kept as a method (and
+ /// kept as ) so GameWindowLifetime's ordered
+ /// shutdown doesn't need a special case for this one resource.
///
- public unsafe void UploadShared(GL gl)
- {
- ReserveTerrainUploads(gl, 1);
- UploadRegions(gl);
- UploadTerrainClip(gl);
- }
-
- ///
- /// Allocates the current frame slot's terrain arena before any draw uses it.
- /// The arena has one alignment-safe range per subsequent
- /// call, so later slices never overwrite
- /// uniform data referenced by earlier GPU commands.
- ///
- public void ReserveTerrainUploads(GL gl, int uploadCount)
- {
- ValidateGlContext(gl);
- ArgumentOutOfRangeException.ThrowIfLessThan(uploadCount, 1);
- _uploadState.ValidateTerrainReservation(uploadCount);
-
- if (_terrainRecordStrideBytes == 0)
- {
- GLHelpers.ThrowOnResourceError(gl, "ClipFrame terrain alignment query (precondition)");
- gl.GetInteger(GetPName.UniformBufferOffsetAlignment, out int alignment);
- GLHelpers.ThrowOnResourceError(gl, "ClipFrame terrain alignment query");
- _terrainRecordStrideBytes = ClipFrameArenaLayout.RecordStride(alignment);
- }
-
- TerrainBufferArena arena = GetOrCreateTerrainArena(gl);
- int requiredBytes = ClipFrameArenaLayout.RequiredBytes(
- uploadCount,
- _terrainRecordStrideBytes);
- int targetCapacity = arena.CapacityPolicy.SelectCapacity(
- arena.CapacityBytes,
- requiredBytes);
- if (targetCapacity != arena.CapacityBytes)
- {
- ClipBufferCapacityTransaction.Resize(
- ref arena.CapacityBytes,
- targetCapacity,
- (previousBytes, newBytes) =>
- TrackedGlResource.AllocateBufferStorage(
- gl,
- GLEnum.UniformBuffer,
- arena.Name,
- previousBytes,
- newBytes,
- GLEnum.DynamicDraw,
- "ClipFrame terrain UBO arena resize"));
- }
-
- _terrainUbo = arena.Name;
- _uploadState.CommitTerrainReservation(uploadCount);
- }
-
- /// Uploads and binds the immutable clip-region table once for the
- /// assembled frame.
- public unsafe void UploadRegions(GL gl)
- {
- ValidateGlContext(gl);
- _uploadState.ValidateRegionsNotUploaded();
-
- RegionBuffer region = GetOrCreateRegionBuffer(gl);
- int regionByteCount = checked(_slotCount * CellClipStrideBytes);
- int targetCapacity = region.CapacityPolicy.SelectCapacity(
- region.CapacityBytes,
- regionByteCount);
- if (targetCapacity != region.CapacityBytes)
- {
- ClipBufferCapacityTransaction.Resize(
- ref region.CapacityBytes,
- targetCapacity,
- (previousBytes, newBytes) =>
- TrackedGlResource.AllocateBufferStorage(
- gl,
- GLEnum.ShaderStorageBuffer,
- region.Name,
- previousBytes,
- newBytes,
- GLEnum.DynamicDraw,
- "ClipFrame region SSBO resize"));
- }
-
- fixed (byte* p = _regionBytes)
- {
- TrackedGlResource.UpdateBufferSubData(
- gl,
- GLEnum.ShaderStorageBuffer,
- region.Name,
- 0,
- regionByteCount,
- p,
- "ClipFrame region SSBO update");
- }
- gl.BindBufferBase(
- BufferTargetARB.ShaderStorageBuffer,
- MeshClipSsboBinding,
- region.Name);
- GLHelpers.ThrowOnResourceError(gl, "ClipFrame region SSBO binding");
-
- _regionSsbo = region.Name;
- _uploadState.MarkRegionsUploaded();
- }
-
- /// Uploads the current terrain clip bytes into the next unique
- /// aligned arena record and installs that range at UBO binding 2.
- public unsafe TerrainClipBufferBinding UploadTerrainClip(GL gl)
- {
- ValidateGlContext(gl);
- int recordIndex = _uploadState.NextTerrainRecord();
- TerrainBufferArena arena = _terrainBuffers.GetRequired(_dynamicFrameSlot);
- int offsetBytes = ClipFrameArenaLayout.RecordOffset(
- recordIndex,
- _terrainRecordStrideBytes);
-
- fixed (byte* p = _terrainBytes)
- {
- TrackedGlResource.UpdateBufferSubData(
- gl,
- GLEnum.UniformBuffer,
- arena.Name,
- offsetBytes,
- TerrainUboBytes,
- p,
- "ClipFrame terrain UBO range update");
- }
-
- _terrainBinding = new TerrainClipBufferBinding(
- arena.Name,
- offsetBytes,
- TerrainUboBytes);
- _terrainBinding.Bind(gl);
- _terrainUbo = arena.Name;
- return _terrainBinding;
- }
-
- public void BindTerrainClip(GL gl)
- {
- ValidateGlContext(gl);
- if (!_terrainBinding.IsValid)
- throw new InvalidOperationException("No terrain clip range has been uploaded for this frame.");
- _terrainBinding.Bind(gl);
- }
-
- private RegionBuffer GetOrCreateRegionBuffer(GL gl)
- {
- if (_regionBuffers.TryGet(_dynamicFrameSlot, out RegionBuffer? existing))
- return existing!;
-
- uint name = TrackedGlResource.CreateBuffer(gl, "ClipFrame region SSBO creation");
- var created = new RegionBuffer { Name = name };
- try
- {
- _regionBuffers.Set(_dynamicFrameSlot, created);
- return created;
- }
- catch
- {
- TrackedGlResource.DeleteBuffer(gl, name, 0, "ClipFrame region SSBO rollback");
- throw;
- }
- }
-
- private TerrainBufferArena GetOrCreateTerrainArena(GL gl)
- {
- if (_terrainBuffers.TryGet(_dynamicFrameSlot, out TerrainBufferArena? existing))
- return existing!;
-
- uint name = TrackedGlResource.CreateBuffer(gl, "ClipFrame terrain UBO creation");
- var created = new TerrainBufferArena { Name = name };
- try
- {
- _terrainBuffers.Set(_dynamicFrameSlot, created);
- return created;
- }
- catch
- {
- TrackedGlResource.DeleteBuffer(gl, name, 0, "ClipFrame terrain UBO rollback");
- throw;
- }
- }
-
- private void ValidateGlContext(GL gl)
- {
- ArgumentNullException.ThrowIfNull(gl);
- ObjectDisposedException.ThrowIf(_disposed, this);
- if (!_dynamicFrameStarted)
- throw new InvalidOperationException("BeginFrame must be called before uploading clip data.");
- if (_gl is null)
- _gl = gl;
- else if (!ReferenceEquals(_gl, gl))
- throw new InvalidOperationException("ClipFrame cannot span OpenGL contexts.");
- }
-
public void Dispose()
{
- if (_disposed || _disposing) return;
- _disposing = true;
- try
- {
- if (_disposeResources is null)
- {
- var releases = new List<(string Name, Action Release)>();
- if (_gl is not null)
- {
- int regionIndex = 0;
- foreach (RegionBuffer set in _regionBuffers.Values)
- {
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- set.Name,
- set.CapacityBytes,
- "ClipFrame region SSBO disposal");
- releases.Add(($"region-ssbo-{regionIndex++}", release.Run));
- }
-
- int terrainIndex = 0;
- foreach (TerrainBufferArena arena in _terrainBuffers.Values)
- {
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- arena.Name,
- arena.CapacityBytes,
- "ClipFrame terrain UBO disposal");
- releases.Add(($"terrain-ubo-{terrainIndex++}", release.Run));
- }
- }
-
- _disposeResources = new RetryableResourceReleaseLedger(releases);
- }
-
- ResourceReleaseAttempt attempt = _disposeResources.Advance();
- if (!_disposeResources.IsComplete)
- {
- throw attempt.ToException(
- "One or more ClipFrame GPU buffers could not be released.");
- }
-
- _regionSsbo = 0;
- _terrainUbo = 0;
- _terrainBinding = default;
- _dynamicFrameStarted = false;
- _disposeResources = null;
- _disposed = true;
-
- if (attempt.HasFailures)
- {
- throw attempt.ToException(
- "ClipFrame GPU buffers released with exceptional committed outcomes.");
- }
- }
- finally
- {
- _disposing = false;
- }
}
// ---- byte helpers (little-endian; matches x86/x64 GPU upload) ------------
@@ -573,11 +265,11 @@ public sealed class ClipFrame : IDisposable
///
/// The packed std430 region table for slots 0..-1.
///
- /// Campaign V slice V6j: the GL arm hands these to
- /// glBufferSubData against a renderer-owned SSBO; the RHI arm copies
- /// them straight into a frame ring slice and publishes the range. Same bytes,
- /// two destinations — which is why the packing above is backend-neutral and
- /// stays where it is.
+ /// The raw-GL arm this used to feed via glBufferSubData against
+ /// a renderer-owned SSBO was deleted at Campaign V slice V11; the RHI arm
+ /// (RhiWorldPassSurface.PrepareClipFrame) now copies these bytes
+ /// straight into a frame ring slice and publishes the range. The packing
+ /// above stays backend-neutral regardless.
///
internal ReadOnlySpan RegionBytes =>
_regionBytes.AsSpan(0, _slotCount * CellClipStrideBytes);
@@ -594,230 +286,3 @@ public sealed class ClipFrame : IDisposable
/// Test seam: the packed std140 terrain UBO bytes.
internal ReadOnlySpan TerrainBytesForTest => TerrainBytes;
}
-
-/// A single std140 terrain-clip record within a frame-slot UBO arena.
-public readonly record struct TerrainClipBufferBinding(
- uint Buffer,
- int OffsetBytes,
- int SizeBytes)
-{
- public bool IsValid => Buffer != 0 && OffsetBytes >= 0 && SizeBytes > 0;
-
- public void Bind(GL gl)
- {
- ArgumentNullException.ThrowIfNull(gl);
- if (!IsValid)
- throw new InvalidOperationException("Cannot bind an empty terrain clip range.");
-
- gl.BindBufferRange(
- BufferTargetARB.UniformBuffer,
- ClipFrame.TerrainClipUboBinding,
- Buffer,
- (nint)OffsetBytes,
- (nuint)SizeBytes);
- GLHelpers.ThrowOnResourceError(gl, "ClipFrame terrain UBO range binding");
- }
-}
-
-internal static class ClipFrameArenaLayout
-{
- public static int RecordStride(int uniformBufferOffsetAlignment)
- {
- ArgumentOutOfRangeException.ThrowIfLessThan(uniformBufferOffsetAlignment, 1);
- long stride = ((long)ClipFrame.TerrainUboBytes + uniformBufferOffsetAlignment - 1L)
- / uniformBufferOffsetAlignment
- * uniformBufferOffsetAlignment;
- if (stride > int.MaxValue)
- throw new OverflowException("Terrain clip UBO record stride exceeds Int32.MaxValue.");
- return (int)stride;
- }
-
- public static int RecordOffset(int recordIndex, int recordStrideBytes)
- {
- ArgumentOutOfRangeException.ThrowIfNegative(recordIndex);
- ArgumentOutOfRangeException.ThrowIfLessThan(recordStrideBytes, ClipFrame.TerrainUboBytes);
- return checked(recordIndex * recordStrideBytes);
- }
-
- public static int RequiredBytes(int recordCount, int recordStrideBytes)
- {
- ArgumentOutOfRangeException.ThrowIfLessThan(recordCount, 1);
- ArgumentOutOfRangeException.ThrowIfLessThan(recordStrideBytes, ClipFrame.TerrainUboBytes);
- return checked(recordCount * recordStrideBytes);
- }
-}
-
-internal static class ClipBufferCapacityTransaction
-{
- /// Publishes capacity only after BufferData succeeds, and before
- /// any later upload/bind operation can throw.
- public static void Resize(
- ref int publishedCapacityBytes,
- int targetCapacityBytes,
- Action allocate)
- {
- ArgumentOutOfRangeException.ThrowIfNegative(publishedCapacityBytes);
- ArgumentOutOfRangeException.ThrowIfLessThan(targetCapacityBytes, 1);
- ArgumentNullException.ThrowIfNull(allocate);
-
- int previousCapacityBytes = publishedCapacityBytes;
- allocate(previousCapacityBytes, targetCapacityBytes);
- publishedCapacityBytes = targetCapacityBytes;
- }
-}
-
-///
-/// Growth is immediate; shrinking requires repeated severe under-use. This
-/// avoids reallocating during ordinary portal-count jitter while ensuring a
-/// one-off pathological flood does not permanently pin its peak GPU storage.
-///
-internal struct ClipBufferCapacityPolicy
-{
- internal const int ShrinkAfterUnderusedFrames = 3;
- internal const int ShrinkUtilizationDivisor = 4;
-
- private int _underusedFrames;
-
- public int SelectCapacity(int currentBytes, int requiredBytes)
- {
- ArgumentOutOfRangeException.ThrowIfNegative(currentBytes);
- ArgumentOutOfRangeException.ThrowIfLessThan(requiredBytes, 1);
-
- if (requiredBytes > currentBytes)
- {
- _underusedFrames = 0;
- return DynamicBufferCapacity.Grow(currentBytes, requiredBytes);
- }
-
- if ((long)requiredBytes * ShrinkUtilizationDivisor <= currentBytes)
- {
- _underusedFrames++;
- if (_underusedFrames >= ShrinkAfterUnderusedFrames)
- {
- _underusedFrames = 0;
- return DynamicBufferCapacity.Grow(0, requiredBytes);
- }
- }
- else
- {
- _underusedFrames = 0;
- }
-
- return currentBytes;
- }
-}
-
-/// Fixed-cardinality resource ownership indexed by the same frame
-/// slots protected by .
-internal sealed class ClipFrameResourceRing(int slotCount) where T : class
-{
- private readonly T?[] _slots = new T?[slotCount > 0
- ? slotCount
- : throw new ArgumentOutOfRangeException(nameof(slotCount))];
-
- public int Count { get; private set; }
-
- public bool TryGet(int slot, out T? resource)
- {
- ValidateSlot(slot);
- resource = _slots[slot];
- return resource is not null;
- }
-
- public T GetRequired(int slot)
- {
- ValidateSlot(slot);
- return _slots[slot]
- ?? throw new InvalidOperationException($"Frame slot {slot} has no resource.");
- }
-
- public void Set(int slot, T resource)
- {
- ValidateSlot(slot);
- ArgumentNullException.ThrowIfNull(resource);
- if (_slots[slot] is not null)
- throw new InvalidOperationException($"Frame slot {slot} already owns a resource.");
- _slots[slot] = resource;
- Count++;
- }
-
- public IEnumerable Values
- {
- get
- {
- for (int i = 0; i < _slots.Length; i++)
- if (_slots[i] is T value)
- yield return value;
- }
- }
-
- private void ValidateSlot(int slot)
- {
- if ((uint)slot >= (uint)_slots.Length)
- throw new ArgumentOutOfRangeException(nameof(slot));
- }
-}
-
-/// Pure sequencing state for one ClipFrame render submission.
-internal sealed class ClipFrameUploadState
-{
- private bool _frameStarted;
- private bool _regionsUploaded;
- private int _terrainReserved;
- private int _terrainCursor;
-
- public int TerrainReserved => _terrainReserved;
- public int TerrainUploaded => _terrainCursor;
- public bool RegionsUploaded => _regionsUploaded;
-
- public void BeginFrame()
- {
- _frameStarted = true;
- _regionsUploaded = false;
- _terrainReserved = 0;
- _terrainCursor = 0;
- }
-
- public void ValidateRegionsNotUploaded()
- {
- EnsureFrameStarted();
- if (_regionsUploaded)
- throw new InvalidOperationException("Clip regions may be uploaded only once per frame.");
- }
-
- public void MarkRegionsUploaded()
- {
- ValidateRegionsNotUploaded();
- _regionsUploaded = true;
- }
-
- public void ValidateTerrainReservation(int uploadCount)
- {
- EnsureFrameStarted();
- ArgumentOutOfRangeException.ThrowIfLessThan(uploadCount, 1);
- if (_terrainReserved != 0)
- throw new InvalidOperationException("Terrain uploads have already been reserved for this frame.");
- }
-
- public void CommitTerrainReservation(int uploadCount)
- {
- ValidateTerrainReservation(uploadCount);
- _terrainReserved = uploadCount;
- }
-
- public int NextTerrainRecord()
- {
- EnsureFrameStarted();
- if (_terrainReserved == 0)
- throw new InvalidOperationException("ReserveTerrainUploads must run before terrain clip uploads.");
- if (_terrainCursor >= _terrainReserved)
- throw new InvalidOperationException("Terrain clip uploads exceeded the reserved arena record count.");
- return _terrainCursor++;
- }
-
- private void EnsureFrameStarted()
- {
- if (!_frameStarted)
- throw new InvalidOperationException("BeginFrame must be called before uploading clip data.");
- }
-}
diff --git a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs
index 405c796a..aa279667 100644
--- a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs
+++ b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs
@@ -1,8 +1,6 @@
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Gpu.Gl;
using AcDream.Core.Textures;
using AcDream.Core.World;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -163,192 +161,6 @@ internal interface ICompositeTextureArrayBackend
void Delete(CompositeTextureArrayResource resource);
}
-///
-/// Narrow GL backend for composite arrays. Unlike ManagedGLTextureArray it
-/// deliberately has one mip level, no PBO, and one resident handle: those are
-/// the semantics of the standalone composite textures this pool replaces.
-///
-internal sealed unsafe class GlCompositeTextureArrayBackend : ICompositeTextureArrayBackend
-{
- private readonly GL _gl;
- private readonly Wb.BindlessSupport _bindless;
- private readonly GlGpuDevice _device;
-
- public GlCompositeTextureArrayBackend(GL gl, Wb.BindlessSupport bindless, GlGpuDevice device)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
- _device = device ?? throw new ArgumentNullException(nameof(device));
- _gl.GetInteger(GetPName.MaxArrayTextureLayers, out int maximumLayers);
- MaximumArrayLayers = Math.Max(1, maximumLayers);
- }
-
- public int MaximumArrayLayers { get; }
-
- public CompositeTextureArrayResource Create(int width, int height, int capacity)
- {
- uint name = _gl.GenTexture();
- if (name == 0)
- throw new InvalidOperationException("OpenGL did not create a composite texture array.");
-
- bool resident = false;
- ulong handle = 0;
- long bytes = 0;
- bool tracked = false;
- try
- {
- // Composite creation/upload runs in the render thread's pre-draw
- // preparation phase. Normalize that phase to texture unit zero
- // instead of synchronously reading driver binding state.
- _gl.ActiveTexture(TextureUnit.Texture0);
- Wb.RenderStateCache.CurrentAtlas = 0;
- _gl.BindTexture(TextureTarget.Texture2DArray, name);
- _gl.TexStorage3D(
- TextureTarget.Texture2DArray,
- levels: 1,
- SizedInternalFormat.Rgba8,
- checked((uint)width),
- checked((uint)height),
- checked((uint)capacity));
- _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureBaseLevel, 0);
- _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMaxLevel, 0);
- _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
- _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
- _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
- handle = _bindless.GetResidentHandle(name);
- resident = true;
- Wb.GLHelpers.ThrowOnResourceError(
- _gl,
- $"creating composite texture array {width}x{height}x{capacity}");
- bytes = checked((long)width * height * 4L * capacity);
- Wb.GpuMemoryTracker.TrackResourceAllocation(Wb.GpuResourceType.Texture);
- Wb.GpuMemoryTracker.TrackAllocation(bytes, Wb.GpuResourceType.Texture);
- tracked = true;
- // Campaign V slice V4t: intern the resident handle into the device's
- // one texture table. A table-exhaustion throw here is caught by the
- // same rollback below, and nothing was added to the table if it did.
- GpuTextureSlot slot = _device.RegisterWorldTextureHandle(handle);
- return new CompositeTextureArrayResource
- {
- Name = name,
- Handle = handle,
- Slot = slot,
- Width = width,
- Height = height,
- Capacity = capacity,
- Bytes = bytes,
- };
- }
- catch (Exception creationFailure)
- {
- List? cleanupFailures = null;
- void Attempt(Action cleanup)
- {
- try { cleanup(); }
- catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
- }
-
- bool residencyReleased = !resident;
- if (resident)
- {
- Attempt(() =>
- {
- _bindless.MakeNonResident(handle);
- Wb.GLHelpers.ThrowOnResourceError(_gl, "rolling back composite texture residency");
- residencyReleased = true;
- });
- }
- if (residencyReleased)
- {
- Attempt(() =>
- {
- _gl.DeleteTexture(name);
- Wb.GLHelpers.ThrowOnResourceError(_gl, "rolling back composite texture array");
- if (tracked)
- {
- Wb.GpuMemoryTracker.TrackDeallocation(bytes, Wb.GpuResourceType.Texture);
- Wb.GpuMemoryTracker.TrackResourceDeallocation(Wb.GpuResourceType.Texture);
- }
- });
- }
-
- if (cleanupFailures is not null)
- {
- cleanupFailures.Insert(0, creationFailure);
- throw new AggregateException(
- "Composite texture-array construction and rollback both failed.",
- cleanupFailures);
- }
- throw;
- }
- finally
- {
- _gl.BindTexture(TextureTarget.Texture2DArray, 0);
- _gl.ActiveTexture(TextureUnit.Texture0);
- }
- }
-
- public void Upload(CompositeTextureArrayResource resource, int layer, byte[] rgba)
- {
- _gl.ActiveTexture(TextureUnit.Texture0);
- Wb.RenderStateCache.CurrentAtlas = 0;
- try
- {
- _gl.BindBuffer(BufferTargetARB.PixelUnpackBuffer, 0);
- _gl.BindTexture(TextureTarget.Texture2DArray, resource.Name);
- fixed (byte* pixels = rgba)
- {
- _gl.TexSubImage3D(
- TextureTarget.Texture2DArray,
- level: 0,
- xoffset: 0,
- yoffset: 0,
- zoffset: layer,
- checked((uint)resource.Width),
- checked((uint)resource.Height),
- depth: 1,
- PixelFormat.Rgba,
- PixelType.UnsignedByte,
- pixels);
- }
- Wb.GLHelpers.ThrowOnResourceError(
- _gl,
- $"uploading composite texture layer {layer} ({resource.Width}x{resource.Height})");
- }
- finally
- {
- _gl.BindTexture(TextureTarget.Texture2DArray, 0);
- _gl.BindBuffer(BufferTargetARB.PixelUnpackBuffer, 0);
- _gl.ActiveTexture(TextureUnit.Texture0);
- }
- }
-
- public void MakeNonResident(CompositeTextureArrayResource resource)
- {
- Wb.GLHelpers.ThrowOnResourceError(
- _gl,
- $"releasing composite texture handle {resource.Handle} (precondition)");
- // Campaign V slice V4t: retire the table entry before the handle it
- // names stops being resident. Idempotent, so this stays correct when
- // the retryable release ledger re-runs the operation.
- _device.ReleaseWorldTextureHandle(resource.Handle);
- _bindless.MakeNonResident(resource.Handle);
- Wb.GLHelpers.ThrowOnResourceError(_gl, $"releasing composite texture handle {resource.Handle}");
- }
-
- public void Delete(CompositeTextureArrayResource resource)
- {
- Wb.GLHelpers.ThrowOnResourceError(
- _gl,
- $"deleting composite texture array {resource.Name} (precondition)");
- _gl.DeleteTexture(resource.Name);
- Wb.GLHelpers.ThrowOnResourceError(_gl, $"deleting composite texture array {resource.Name}");
- Wb.GpuMemoryTracker.TrackDeallocation(resource.Bytes, Wb.GpuResourceType.Texture);
- Wb.GpuMemoryTracker.TrackResourceDeallocation(Wb.GpuResourceType.Texture);
- }
-}
-
///
/// Campaign V slice V6i-2: the backend-neutral composite array backend.
///
@@ -525,25 +337,6 @@ internal sealed class CompositeTextureArrayCache : IDisposable
Accounted,
}
- public CompositeTextureArrayCache(
- GL gl,
- Wb.BindlessSupport bindless,
- GlGpuDevice device,
- IGpuResourceRetirementQueue retirementQueue,
- long unownedBudgetBytes = DefaultUnownedBudgetBytes,
- long physicalBudgetBytes = DefaultPhysicalBudgetBytes,
- int maximumUploadsPerFrame = DefaultMaximumUploadsPerFrame,
- long maximumUploadBytesPerFrame = DefaultMaximumUploadBytesPerFrame)
- : this(
- new GlCompositeTextureArrayBackend(gl, bindless, device),
- retirementQueue,
- unownedBudgetBytes,
- physicalBudgetBytes,
- maximumUploadsPerFrame,
- maximumUploadBytesPerFrame)
- {
- }
-
internal CompositeTextureArrayCache(
ICompositeTextureArrayBackend backend,
IGpuResourceRetirementQueue retirementQueue,
diff --git a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs
index 824e585b..f27b37d3 100644
--- a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs
+++ b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs
@@ -1,13 +1,11 @@
using System.Numerics;
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Gpu.Gl;
using AcDream.App.Rendering.Wb;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.App.World;
using AcDream.Core.Lighting;
using AcDream.Core.World;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -377,8 +375,7 @@ internal sealed class CreatureAppraisalViewportRenderer :
private readonly PrivateEntityViewportRenderer _renderer;
public CreatureAppraisalViewportRenderer(
- GL? gl,
- IWorldPassScope? scope,
+ IWorldPassScope scope,
AcDream.App.Rendering.Gpu.IGpuDevice device,
ICurrentGpuFrameSource frames,
WbDrawDispatcher dispatcher,
@@ -387,7 +384,6 @@ internal sealed class CreatureAppraisalViewportRenderer :
IWbMeshAdapter meshAdapter)
{
_renderer = new PrivateEntityViewportRenderer(
- gl,
scope,
device,
frames,
diff --git a/src/AcDream.App/Rendering/FrameProfilerGpuMeasurement.cs b/src/AcDream.App/Rendering/FrameProfilerGpuMeasurement.cs
deleted file mode 100644
index fe7d927a..00000000
--- a/src/AcDream.App/Rendering/FrameProfilerGpuMeasurement.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using AcDream.App.Diagnostics;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering;
-
-///
-/// Production render-transaction measurement adapter. CPU frame boundaries
-/// remain owned by while this adapter places the
-/// GPU query around only the GL-producing render phases.
-///
-internal sealed class FrameProfilerGpuMeasurement : IRenderFrameGpuMeasurement
-{
- private readonly FrameProfiler _profiler;
- private readonly GL _gl;
-
- public FrameProfilerGpuMeasurement(FrameProfiler profiler, GL gl)
- {
- _profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- }
-
- public void BeginFrame()
- {
- _profiler.FrameBoundary(_gl);
- _profiler.BeginGpuFrame();
- }
-
- public void EndFrame()
- {
- _profiler.EndGpuFrame();
- }
-}
diff --git a/src/AcDream.App/Rendering/GameRenderResourceLifetime.cs b/src/AcDream.App/Rendering/GameRenderResourceLifetime.cs
index 71aeb182..2c9f4b47 100644
--- a/src/AcDream.App/Rendering/GameRenderResourceLifetime.cs
+++ b/src/AcDream.App/Rendering/GameRenderResourceLifetime.cs
@@ -12,15 +12,9 @@ internal interface IGameRenderResourceLifetime
internal sealed class GameRenderResourceLifetime : IGameRenderResourceLifetime
{
private readonly OwnedResourceSlot _terrainAtlas = new();
- private readonly OwnedResourceSlot _skyShader = new();
public TerrainAtlas AcquireTerrainAtlas(Func factory) =>
_terrainAtlas.Acquire(factory);
- public Shader AcquireSkyShader(Func factory) =>
- _skyShader.Acquire(factory);
-
public void ReleaseTerrainAtlas() => _terrainAtlas.Release();
-
- public void ReleaseSkyShader() => _skyShader.Release();
}
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index e5f4e510..f60e9e24 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -16,7 +16,6 @@ using AcDream.Runtime.Session;
using DatReaderWriter;
using Silk.NET.Input;
using Silk.NET.Maths;
-using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace AcDream.App.Rendering;
@@ -56,15 +55,11 @@ public sealed class GameWindow :
AcDream.UI.Abstractions.Settings.QualityPreset.High);
private IInputContext? _input;
private TerrainModernRenderer? _terrain;
- /// Phase N.5b: terrain_modern.vert/.frag program. Owned by
- /// at draw time but allocated + disposed here.
- private Shader? _terrainModernShader;
private CameraController? _cameraController;
private IDatReaderWriter? _dats;
private IPreparedAssetSource? _preparedAssets;
private readonly AcDream.App.Input.PointerPositionState _pointerPosition = new();
private AcDream.App.Input.CameraPointerInputController? _cameraPointerInput;
- private Shader? _meshShader;
private TextureCache? _textureCache;
/// Phase N.4+: WB-backed rendering pipeline adapter. Always non-null
/// after OnLoad completes (modern path is mandatory as of N.5).
@@ -77,11 +72,6 @@ public sealed class GameWindow :
private AcDream.App.Rendering.Selection.RetailSelectionScene? _retailSelectionScene;
private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery;
private AcDream.App.Interaction.SelectionInteractionController? _selectionInteractions;
- /// Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters
- /// support. Required at startup — missing bindless throws
- /// in OnLoad.
- private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport;
- private SamplerCache? _samplerCache;
private DebugLineRenderer? _debugLines;
// K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder
// wireframes are noisy outdoors and confuse first-time users into
@@ -128,8 +118,8 @@ public sealed class GameWindow :
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
_renderResourceLifetime = new();
- private readonly AcDream.App.Rendering.GlConstructionCleanupLedger
- _glConstructionCleanup = new();
+ private readonly AcDream.App.Rendering.ResourceConstructionCleanupLedger
+ _constructionCleanup = new();
private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment;
private readonly GameWindowLifetime _lifetime = new();
private readonly DisplayFramePacingController _displayFramePacing;
@@ -518,7 +508,6 @@ public sealed class GameWindow :
private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings;
private readonly GraphicalHostPlatformServices _platformServices;
private readonly ApplicationPathSet _applicationPaths;
- private GraphicalCapabilityRecord? _graphicalCapabilities;
private static AcDream.UI.Abstractions.Input.KeyBindings LoadStartupKeyBindings(
string path)
@@ -687,8 +676,7 @@ public sealed class GameWindow :
// attribute, so it must come from this same snapshot rather than a
// second settings load during OnLoad.
RuntimeSettingsSnapshot startup = _runtimeSettings.Startup;
- if (_options.RenderBackend == RenderBackendKind.Vulkan
- && _options.VulkanCapabilityProbe)
+ if (_options.VulkanCapabilityProbe)
{
// Campaign V slice V6h reduced the V5 bring-up host to what its name
// says: a capability probe. It opens its own window, runs the gate,
@@ -706,39 +694,17 @@ public sealed class GameWindow :
FramePacingPolicy startupPacing =
_displayFramePacing.InitializeStartup(startup.Display.VSync);
- // Campaign V slice V6h: the ONLY startup difference between the two
- // backends. Vulkan needs a client-API-less window (the surface comes from
- // VK_KHR_surface), and neither MSAA nor the stencil bit count is a window
+ // Vulkan needs a client-API-less window (the surface comes from
+ // VK_KHR_surface); neither MSAA nor the stencil bit count is a window
// attribute there — both are attachment properties the RHI device
- // configures, so they are passed to VulkanGraphicsContext instead.
- var options = _options.RenderBackend == RenderBackendKind.Vulkan
- ? WindowOptions.DefaultVulkan with
- {
- Size = new Vector2D(1280, 720),
- Title = "acdream — Vulkan",
- VSync = startupPacing.UseVSync,
- }
- : WindowOptions.Default with
- {
- Size = new Vector2D(1280, 720),
- Title = "acdream — phase 1",
- API = new GraphicsAPI(
- ContextAPI.OpenGL,
- ContextProfile.Core,
- ContextFlags.ForwardCompatible,
- new APIVersion(4, 3)),
- VSync = startupPacing.UseVSync,
- // A.5 T22.5: MSAA from quality preset (0 = disabled, 2/4/8 = multisample).
- // Silk.NET passes this to SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES).
- // Cannot be changed at runtime; Quality changes mid-session that would
- // alter MsaaSamples are logged as a restart-required warning.
- Samples = startup.Quality.MsaaSamples,
- // #117 (2026-06-11): the aperture punch's depth gate needs a
- // stencil buffer (PortalDepthMaskRenderer two-pass mark+punch).
- // GLFW defaults to 8 stencil bits, but make the requirement
- // explicit rather than platform-implicit.
- PreferredStencilBufferBits = 8,
- };
+ // configures instead. The raw-GL window options this used to fork to
+ // were deleted at Campaign V slice V11.
+ var options = WindowOptions.DefaultVulkan with
+ {
+ Size = new Vector2D(1280, 720),
+ Title = "acdream — Vulkan",
+ VSync = startupPacing.UseVSync,
+ };
_startupPacing = startupPacing;
_startupQuality = startup.Quality;
@@ -766,7 +732,7 @@ public sealed class GameWindow :
}
catch (Exception failure)
{
- _glConstructionCleanup.RetainFrom(failure);
+ _constructionCleanup.RetainFrom(failure);
throw;
}
}
@@ -912,19 +878,6 @@ public sealed class GameWindow :
_audioSink = value.HookSink;
}
- void IGameWindowWorldRenderPublication.PublishBindlessSupport(
- BindlessSupport value) =>
- PublishCompositionOwner(
- ref _bindlessSupport,
- value,
- "bindless support");
-
- void IGameWindowWorldRenderPublication.PublishTerrainShader(Shader value) =>
- PublishCompositionOwner(
- ref _terrainModernShader,
- value,
- "terrain shader");
-
void IGameWindowWorldRenderPublication.PublishSceneLighting(
SceneLightingUboBinding value) =>
PublishCompositionOwner(
@@ -975,9 +928,6 @@ public sealed class GameWindow :
_surfaceCache = surfaceCache;
}
- void IGameWindowWorldRenderPublication.PublishMeshShader(Shader value) =>
- PublishCompositionOwner(ref _meshShader, value, "mesh shader");
-
void IGameWindowWorldRenderPublication.PublishWbMeshAdapter(
WbMeshAdapter value) =>
PublishCompositionOwner(ref _wbMeshAdapter, value, "WB mesh adapter");
@@ -986,10 +936,6 @@ public sealed class GameWindow :
TextureCache value) =>
PublishCompositionOwner(ref _textureCache, value, "texture cache");
- void IGameWindowWorldRenderPublication.PublishSamplerCache(
- SamplerCache value) =>
- PublishCompositionOwner(ref _samplerCache, value, "sampler cache");
-
void IGameWindowInteractionRetainedUiPublication.PublishInteractionRetainedUi(
InteractionRetainedUiResult result)
{
@@ -1192,41 +1138,31 @@ public sealed class GameWindow :
}
///
- /// Campaign V slice V6h: how the UI probe reads a completed frame.
- ///
- /// applies the bottom-up flip
- /// glReadPixels needs, so GL hands it the raw read and Vulkan — whose
- /// is documented top-left-origin —
- /// pre-flips so the two cancel. Routing GL through the same
- /// CaptureBackbuffer seam would double-flip, which is exactly the kind
- /// of "usually right" instrument §5.5 of the campaign plan spent three
- /// slices removing.
+ /// How the UI probe reads a completed frame.
+ /// is documented top-left-origin,
+ /// so flips it to match the PNG's
+ /// row order. The raw-GL read this used to fork from — whose rows already
+ /// come out bottom-up, needing no flip — was deleted at Campaign V slice
+ /// V11.
///
private static Func CreateBackbufferReader(
GameWindowGraphics graphics,
IGpuDevice device) =>
- graphics.Gl is { } gl
- ? (width, height) =>
- AcDream.App.Diagnostics.FrameScreenshotController
- .ReadDefaultFramebuffer(gl, width, height)
- : (width, height) =>
- AcDream.App.Diagnostics.FrameScreenshotController.FlipRows(
- device.CaptureBackbuffer(width, height),
- width,
- height);
+ (width, height) =>
+ AcDream.App.Diagnostics.FrameScreenshotController.FlipRows(
+ device.CaptureBackbuffer(width, height),
+ width,
+ height);
///
- /// Campaign V slice V6h: the backend fork, in one expression. Vulkan's
- /// context acquisition runs its own capability gate inside
+ /// Vulkan's context acquisition runs its own capability gate inside
/// ,
/// throwing into the same exit-code-4
- /// contract Program.cs publishes for GL.
+ /// contract Program.cs publishes. The raw-GL fork this used to make
+ /// was deleted at Campaign V slice V11.
///
private GameWindowGraphics CreateGraphics()
{
- if (_options.RenderBackend != RenderBackendKind.Vulkan)
- return new OpenGlGameWindowGraphics(GL.GetApi(_window!));
-
AcDream.App.Rendering.Gpu.Vk.VulkanGraphicsContext vulkan =
AcDream.App.Rendering.Gpu.Vk.VulkanGraphicsContext.Acquire(
_window!,
@@ -1245,32 +1181,10 @@ public sealed class GameWindow :
// run narrow-phase BSP tests during FindObjCollisions.
GameWindowPlatformResult platform = AcquirePlatform();
- // The GL capability gate reads GL extension strings, so it runs only on
- // the GL arm. Vulkan's equivalent gate already ran inside
- // VulkanGraphicsContext.Acquire and wrote its own report.
- if (platform.Graphics.Gl is { } capabilityGl)
- {
- string capabilityReportPath = Path.Combine(
- _applicationPaths.DiagnosticsDirectory,
- "graphical-capabilities.json");
- _graphicalCapabilities =
- GraphicalCapabilityGuard.CaptureVerifyAndWrite(
- capabilityGl,
- _window!,
- platform.Input,
- _platformServices,
- capabilityReportPath);
- GraphicalCapabilityGuard.ThrowIfUnsupported(
- _graphicalCapabilities,
- capabilityReportPath);
- Console.WriteLine(
- "graphics: capability gate passed " +
- $"({_graphicalCapabilities.ActiveDisplayProtocol}, " +
- $"{_graphicalCapabilities.GlVendor}, " +
- $"{_graphicalCapabilities.GlRenderer}, " +
- $"{_graphicalCapabilities.GlVersion}); " +
- $"report={capabilityReportPath}");
- }
+ // The raw-GL capability gate that used to run here (reading GL
+ // extension strings) was deleted at Campaign V slice V11. Vulkan's
+ // equivalent gate already ran inside VulkanGraphicsContext.Acquire and
+ // wrote its own report.
GameWindowCompositionPipeline.Run<
GameWindowPlatformResult,
@@ -1727,12 +1641,9 @@ public sealed class GameWindow :
_clipFrame,
_skyRenderer,
_particleRenderer,
- _samplerCache,
_textureCache,
_wbMeshAdapter,
- _meshShader,
_terrain,
- _terrainModernShader,
_sceneLightingUbo,
_debugLines,
_textRenderer,
@@ -1740,7 +1651,7 @@ public sealed class GameWindow :
_displayFramePacing,
_frameProfiler,
_renderResourceLifetime,
- _glConstructionCleanup),
+ _constructionCleanup),
new PlatformShutdownRoots(
_dats,
_preparedAssets,
diff --git a/src/AcDream.App/Rendering/GameWindowLifetime.cs b/src/AcDream.App/Rendering/GameWindowLifetime.cs
index 8e8f29f1..72f66e87 100644
--- a/src/AcDream.App/Rendering/GameWindowLifetime.cs
+++ b/src/AcDream.App/Rendering/GameWindowLifetime.cs
@@ -25,7 +25,6 @@ using AcDream.Runtime.Session;
using AcDream.UI.Abstractions.Input;
using DatReaderWriter;
using Silk.NET.Input;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -111,12 +110,9 @@ internal sealed record RenderShutdownRoots(
ClipFrame? ClipFrame,
SkyRenderer? Sky,
ParticleRenderer? Particles,
- SamplerCache? Samplers,
TextureCache? Textures,
WbMeshAdapter? MeshAdapter,
- Shader? MeshShader,
TerrainModernRenderer? Terrain,
- Shader? TerrainShader,
SceneLightingUboBinding? SceneLighting,
DebugLineRenderer? DebugLines,
TextRenderer? TextRenderer,
@@ -124,7 +120,7 @@ internal sealed record RenderShutdownRoots(
DisplayFramePacingController FramePacing,
FrameProfiler FrameProfiler,
GameRenderResourceLifetime DedicatedResources,
- GlConstructionCleanupLedger ConstructionCleanup);
+ ResourceConstructionCleanupLedger ConstructionCleanup);
internal sealed record PlatformShutdownRoots(
IDatReaderWriter? Dats,
@@ -439,7 +435,6 @@ internal static class GameWindowShutdownManifest
]),
new ResourceShutdownStage("shared texture owners",
[
- Hard("sampler cache", () => render.Samplers?.Dispose()),
Hard("texture cache", () => render.Textures?.Dispose()),
]),
new ResourceShutdownStage("mesh adapter",
@@ -448,9 +443,7 @@ internal static class GameWindowShutdownManifest
]),
new ResourceShutdownStage("remaining render owners",
[
- Hard("mesh shader", () => render.MeshShader?.Dispose()),
Hard("terrain", () => render.Terrain?.Dispose()),
- Hard("terrain shader", () => render.TerrainShader?.Dispose()),
Hard("scene lighting", () => render.SceneLighting?.Dispose()),
Hard("debug lines", () => render.DebugLines?.Dispose()),
Hard("text renderer", () => render.TextRenderer?.Dispose()),
@@ -461,12 +454,11 @@ internal static class GameWindowShutdownManifest
]),
new ResourceShutdownStage("dedicated render resources",
[
- Hard("sky shader", render.DedicatedResources.ReleaseSkyShader),
Hard("terrain atlas", render.DedicatedResources.ReleaseTerrainAtlas),
]),
new ResourceShutdownStage("failed render construction cleanup",
[
- Hard("GL construction ledger", render.ConstructionCleanup.Dispose),
+ Hard("resource construction ledger", render.ConstructionCleanup.Dispose),
]),
new ResourceShutdownStage("frame flight owner",
[
@@ -481,7 +473,7 @@ internal static class GameWindowShutdownManifest
[
Hard("input context", () => platform.Input?.Dispose()),
]),
- new ResourceShutdownStage("OpenGL context",
+ new ResourceShutdownStage("graphics API context",
[
Hard("graphics API", () => platform.Graphics?.Dispose()),
]));
diff --git a/src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs b/src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs
deleted file mode 100644
index e1bc73ef..00000000
--- a/src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-namespace AcDream.App.Rendering;
-
-internal interface IRetryableResourceCleanup
-{
- bool IsCleanupComplete { get; }
- void RetryCleanup();
-}
-
-internal sealed class GlResourceConstructionException : AggregateException,
- IRetryableResourceCleanup
-{
- private readonly IRetryableResourceCleanup _cleanup;
-
- public GlResourceConstructionException(
- string message,
- IRetryableResourceCleanup cleanup,
- IEnumerable failures)
- : base(message, failures)
- {
- _cleanup = cleanup ?? throw new ArgumentNullException(nameof(cleanup));
- }
-
- public bool IsCleanupComplete => _cleanup.IsCleanupComplete;
-
- public void RetryCleanup() => _cleanup.RetryCleanup();
-}
-
-///
-/// Lifetime root for cleanup work that could not finish before a throwing GL
-/// factory returned control. The original exception remains the retry owner;
-/// this ledger prevents it and its exact pending names from becoming local-only.
-///
-internal sealed class GlConstructionCleanupLedger : IDisposable
-{
- private readonly List _pending = [];
- private bool _disposing;
-
- public bool IsComplete => _pending.Count == 0;
-
- public bool RetainFrom(Exception failure)
- {
- ArgumentNullException.ThrowIfNull(failure);
- bool retained = false;
- Visit(failure);
- return retained;
-
- void Visit(Exception current)
- {
- if (current is IRetryableResourceCleanup cleanup)
- {
- if (!cleanup.IsCleanupComplete && !_pending.Contains(cleanup))
- _pending.Add(cleanup);
- retained = true;
- }
-
- if (current is AggregateException aggregate)
- {
- foreach (Exception inner in aggregate.InnerExceptions)
- Visit(inner);
- }
- else if (current.InnerException is { } inner)
- {
- Visit(inner);
- }
- }
- }
-
- public void Dispose()
- {
- if (_disposing || _pending.Count == 0)
- return;
-
- _disposing = true;
- List? failures = null;
- try
- {
- for (int i = _pending.Count - 1; i >= 0; i--)
- {
- IRetryableResourceCleanup cleanup = _pending[i];
- try
- {
- cleanup.RetryCleanup();
- if (cleanup.IsCleanupComplete)
- _pending.RemoveAt(i);
- }
- catch (Exception failure)
- {
- (failures ??= []).Add(failure);
- }
- }
- }
- finally
- {
- _disposing = false;
- }
-
- if (failures is not null)
- throw new AggregateException(
- "One or more failed GL construction transactions remain pending.",
- failures);
- }
-}
diff --git a/src/AcDream.App/Rendering/GlResourceCommand.cs b/src/AcDream.App/Rendering/GlResourceCommand.cs
deleted file mode 100644
index 57478bb5..00000000
--- a/src/AcDream.App/Rendering/GlResourceCommand.cs
+++ /dev/null
@@ -1,152 +0,0 @@
-using System.Runtime.ExceptionServices;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering;
-
-///
-/// Always-on commit boundary for GL resource commands. OpenGL reports ordinary
-/// command failures through its error flag, so an ownership state machine may
-/// advance only after the post-command check succeeds.
-///
-internal static class GlResourceCommand
-{
- public static void Execute(GL gl, string context, Action command)
- {
- ArgumentNullException.ThrowIfNull(gl);
- ArgumentException.ThrowIfNullOrWhiteSpace(context);
- ArgumentNullException.ThrowIfNull(command);
-
- GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
- command();
- GLHelpers.ThrowOnResourceError(gl, context);
- }
-
- public static T Execute(GL gl, string context, Func command)
- {
- ArgumentNullException.ThrowIfNull(gl);
- ArgumentException.ThrowIfNullOrWhiteSpace(context);
- ArgumentNullException.ThrowIfNull(command);
-
- GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
- T result = command();
- GLHelpers.ThrowOnResourceError(gl, context);
- return result;
- }
-
- public static uint CreateName(
- GL gl,
- string resourceName,
- Func create,
- Action delete)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
- ArgumentNullException.ThrowIfNull(create);
- ArgumentNullException.ThrowIfNull(delete);
-
- return CreateNameCore(
- resourceName,
- () => GLHelpers.ThrowOnResourceError(
- gl,
- $"create {resourceName} (precondition)"),
- create,
- () => GLHelpers.ThrowOnResourceError(gl, $"create {resourceName}"),
- ownedName => Execute(
- gl,
- $"rollback {resourceName} name {ownedName}",
- () => delete(ownedName)));
- }
-
- internal static uint CreateNameCore(
- string resourceName,
- Action precondition,
- Func create,
- Action postcondition,
- Action deleteChecked)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
- ArgumentNullException.ThrowIfNull(precondition);
- ArgumentNullException.ThrowIfNull(create);
- ArgumentNullException.ThrowIfNull(postcondition);
- ArgumentNullException.ThrowIfNull(deleteChecked);
-
- uint name = 0;
- Exception? creationFailure = null;
- try
- {
- precondition();
- name = create();
- if (name == 0)
- throw new InvalidOperationException($"OpenGL returned no {resourceName} name.");
- postcondition();
- return name;
- }
- catch (Exception failure)
- {
- creationFailure = failure;
- }
-
- if (name != 0)
- {
- var cleanup = new SingleNameCleanup(name, deleteChecked);
- try
- {
- cleanup.RetryCleanup();
- }
- catch (Exception cleanupFailure)
- {
- throw new GlResourceConstructionException(
- $"Creating {resourceName} failed and its returned GL name could not be released.",
- cleanup,
- [creationFailure, cleanupFailure]);
- }
- }
-
- ExceptionDispatchInfo.Capture(creationFailure).Throw();
- throw new InvalidOperationException("Unreachable GL resource-creation path.");
- }
-
- public static uint CreateTexture(GL gl, string context) =>
- CreateName(gl, context, gl.GenTexture, gl.DeleteTexture);
-
- public static void DeleteTexture(GL gl, uint texture, string context) =>
- Execute(gl, context, () => gl.DeleteTexture(texture));
-
- public static void DeleteShader(GL gl, uint shader, string context) =>
- Execute(gl, context, () => gl.DeleteShader(shader));
-
- public static void DeleteProgram(GL gl, uint program, string context) =>
- Execute(gl, context, () => gl.DeleteProgram(program));
-
- public static void DeleteBuffer(GL gl, uint buffer, string context) =>
- Execute(gl, context, () => gl.DeleteBuffer(buffer));
-
- public static void DeleteVertexArray(GL gl, uint vertexArray, string context) =>
- Execute(gl, context, () => gl.DeleteVertexArray(vertexArray));
-
- private sealed class SingleNameCleanup(uint name, Action delete)
- : IRetryableResourceCleanup
- {
- private uint _name = name;
- private bool _running;
-
- public bool IsCleanupComplete => _name == 0;
-
- public void RetryCleanup()
- {
- if (_running || _name == 0)
- return;
-
- _running = true;
- try
- {
- delete(_name);
- _name = 0;
- }
- finally
- {
- _running = false;
- }
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/GlTextureConstructionTransaction.cs b/src/AcDream.App/Rendering/GlTextureConstructionTransaction.cs
deleted file mode 100644
index a7a04e69..00000000
--- a/src/AcDream.App/Rendering/GlTextureConstructionTransaction.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-namespace AcDream.App.Rendering;
-
-internal interface IGlTextureNameApi
-{
- uint GenTexture();
- void DeleteTexture(uint texture);
-}
-
-internal sealed class GlTextureNameApi(Silk.NET.OpenGL.GL gl) : IGlTextureNameApi
-{
- public uint GenTexture() =>
- GlResourceCommand.CreateTexture(gl, "terrain construction texture");
-
- public void DeleteTexture(uint texture) =>
- GlResourceCommand.DeleteTexture(
- gl,
- texture,
- $"delete terrain construction texture {texture}");
-}
-
-///
-/// Retains every texture name immediately after allocation until the complete
-/// aggregate owner has been constructed. A failed factory rolls names back in
-/// reverse acquisition order and attempts every deletion.
-///
-internal sealed class GlTextureConstructionTransaction(IGlTextureNameApi api)
- : IRetryableResourceCleanup
-{
- private readonly List _ownedNames = [];
- private bool _finished;
-
- public bool IsCleanupComplete => _finished;
-
- public uint Allocate()
- {
- if (_finished)
- throw new InvalidOperationException("The texture construction transaction is finished.");
-
- uint texture = api.GenTexture();
- if (texture == 0)
- throw new InvalidOperationException("OpenGL returned no texture name.");
- _ownedNames.Add(texture);
- return texture;
- }
-
- public void Commit()
- {
- if (_finished)
- throw new InvalidOperationException("The texture construction transaction is finished.");
- _ownedNames.Clear();
- _finished = true;
- }
-
- public void Rollback()
- {
- if (_finished)
- return;
-
- List? failures = null;
- for (int i = _ownedNames.Count - 1; i >= 0; i--)
- {
- uint texture = _ownedNames[i];
- try
- {
- api.DeleteTexture(texture);
- _ownedNames.RemoveAt(i);
- }
- catch (Exception failure)
- {
- (failures ??= []).Add(new InvalidOperationException(
- $"Texture construction rollback could not delete OpenGL name {texture}.",
- failure));
- }
- }
-
- if (failures is not null)
- throw new AggregateException(
- "Texture construction rollback did not release every allocated OpenGL name.",
- failures);
-
- _finished = true;
- }
-
- public void RetryCleanup() => Rollback();
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs
deleted file mode 100644
index af113ff4..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs
+++ /dev/null
@@ -1,313 +0,0 @@
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// The narrow slice of GL that reads and
-/// writes. It exists so the save/restore transaction can be exercised without a
-/// GL context: the property that matters is "every value this pass can change
-/// is put back, including when a draw throws", and that is a property of the
-/// bookkeeping, not of the driver.
-///
-/// Slice V6d generalized this from ITextRenderGlStateApi, which
-/// TextRenderer owned privately and which restored a strict subset of the
-/// same values. That renderer no longer touches GL at all, and the guarantee now
-/// lives in one place for every RHI pass.
-///
-internal interface IGlAmbientStateApi
-{
- bool IsEnabled(EnableCap capability);
-
- int GetInteger(GetPName parameter);
-
- bool GetBoolean(GetPName parameter);
-
- /// The four colour-mask channels, in RGBA order.
- bool[] GetColorMask();
-
- void SetCapability(EnableCap capability, bool enabled);
-
- void DepthMask(bool enabled);
-
- void DepthFunc(DepthFunction function);
-
- void BlendFuncSeparate(
- BlendingFactor sourceRgb,
- BlendingFactor destinationRgb,
- BlendingFactor sourceAlpha,
- BlendingFactor destinationAlpha);
-
- void CullFace(TriangleFace face);
-
- void FrontFace(FrontFaceDirection direction);
-
- void StencilFunc(StencilFunction function, int reference, uint mask);
-
- void StencilOp(StencilOp fail, StencilOp depthFail, StencilOp pass);
-
- void StencilMask(uint mask);
-
- void ColorMask(bool red, bool green, bool blue, bool alpha);
-
- void UseProgram(uint program);
-
- void BindVertexArray(uint vertexArray);
-
- void BindBuffer(BufferTargetARB target, uint buffer);
-
- void ActiveTexture(TextureUnit unit);
-
- void BindTexture(TextureTarget target, uint texture);
-}
-
-internal sealed class SilkGlAmbientStateApi : IGlAmbientStateApi
-{
- private readonly GL _gl;
-
- public SilkGlAmbientStateApi(GL gl) => _gl = gl ?? throw new ArgumentNullException(nameof(gl));
-
- public bool IsEnabled(EnableCap capability) => _gl.IsEnabled(capability);
-
- public int GetInteger(GetPName parameter)
- {
- _gl.GetInteger(parameter, out int value);
- return value;
- }
-
- public bool GetBoolean(GetPName parameter) => _gl.GetBoolean(parameter);
-
- public unsafe bool[] GetColorMask()
- {
- var values = new bool[4];
- fixed (bool* first = values)
- _gl.GetBoolean(GetPName.ColorWritemask, first);
- return values;
- }
-
- public void SetCapability(EnableCap capability, bool enabled)
- {
- if (enabled)
- _gl.Enable(capability);
- else
- _gl.Disable(capability);
- }
-
- public void DepthMask(bool enabled) => _gl.DepthMask(enabled);
-
- public void DepthFunc(DepthFunction function) => _gl.DepthFunc(function);
-
- public void BlendFuncSeparate(
- BlendingFactor sourceRgb,
- BlendingFactor destinationRgb,
- BlendingFactor sourceAlpha,
- BlendingFactor destinationAlpha) =>
- _gl.BlendFuncSeparate(sourceRgb, destinationRgb, sourceAlpha, destinationAlpha);
-
- public void CullFace(TriangleFace face) => _gl.CullFace(face);
-
- public void FrontFace(FrontFaceDirection direction) => _gl.FrontFace(direction);
-
- public void StencilFunc(StencilFunction function, int reference, uint mask) =>
- _gl.StencilFunc(function, reference, mask);
-
- public void StencilOp(StencilOp fail, StencilOp depthFail, StencilOp pass) =>
- _gl.StencilOp(fail, depthFail, pass);
-
- public void StencilMask(uint mask) => _gl.StencilMask(mask);
-
- public void ColorMask(bool red, bool green, bool blue, bool alpha) =>
- _gl.ColorMask(red, green, blue, alpha);
-
- public void UseProgram(uint program) => _gl.UseProgram(program);
-
- public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray);
-
- public void BindBuffer(BufferTargetARB target, uint buffer) => _gl.BindBuffer(target, buffer);
-
- public void ActiveTexture(TextureUnit unit) => _gl.ActiveTexture(unit);
-
- public void BindTexture(TextureTarget target, uint texture) => _gl.BindTexture(target, texture);
-}
-
-///
-/// Every ambient GL capability/binding a bind (or a
-/// dynamic setter, or the pass's own sample count) can change, captured by raw
-/// query and restored by raw call.
-///
-/// Transitional for as long as raw-GL renderers coexist with RHI-ported ones:
-/// each raw-GL renderer assumes whatever state the previous one left behind is
-/// still there, so a pass that changes state and does not put it back is
-/// invisible until the world silhouette changes. That is precisely how the first
-/// V4a attempt lost multisampling (plan §7.1 rule 1). Deleted at V4h.
-///
-internal readonly struct GlAmbientCapabilityState
-{
- private readonly int _program;
- private readonly int _vertexArray;
- private readonly int _arrayBuffer;
- private readonly int _activeTexture;
- private readonly int _texture0Binding2D;
- private readonly bool _depthTest;
- private readonly bool _depthWrite;
- private readonly int _depthFunc;
- private readonly bool _blend;
- private readonly int _blendSourceRgb;
- private readonly int _blendDestinationRgb;
- private readonly int _blendSourceAlpha;
- private readonly int _blendDestinationAlpha;
- private readonly bool _cullFace;
- private readonly int _cullFaceMode;
- private readonly int _frontFace;
- private readonly bool _alphaToCoverage;
- private readonly bool _multisample;
- // Slice V6l: the stencil dimension. #117's portal punch is the only pipeline
- // that enables it, and it draws in the middle of a frame whose other
- // renderers are still raw GL and assume the test is off.
- private readonly bool _stencilTest;
- private readonly int _stencilFunc;
- private readonly int _stencilReference;
- private readonly int _stencilValueMask;
- private readonly int _stencilWriteMask;
- private readonly int _stencilFail;
- private readonly int _stencilDepthFail;
- private readonly int _stencilPass;
- // Slice V6l: GpuPipelineDescription.ColorWrite has had no consumer until the
- // portal depth mask, which is colour-invisible by construction. A pass that
- // left the mask off would black out every raw-GL renderer that followed it,
- // which is §7.1 rule 1's exact failure mode wearing a different name.
- private readonly bool _colorMaskRed;
- private readonly bool _colorMaskGreen;
- private readonly bool _colorMaskBlue;
- private readonly bool _colorMaskAlpha;
-
- private GlAmbientCapabilityState(
- int program, int vertexArray, int arrayBuffer, int activeTexture, int texture0Binding2D,
- bool depthTest, bool depthWrite, int depthFunc,
- bool blend, int blendSourceRgb, int blendDestinationRgb, int blendSourceAlpha, int blendDestinationAlpha,
- bool cullFace, int cullFaceMode, int frontFace,
- bool alphaToCoverage, bool multisample,
- bool stencilTest, int stencilFunc, int stencilReference, int stencilValueMask,
- int stencilWriteMask, int stencilFail, int stencilDepthFail, int stencilPass,
- bool colorMaskRed, bool colorMaskGreen, bool colorMaskBlue, bool colorMaskAlpha)
- {
- _program = program;
- _vertexArray = vertexArray;
- _arrayBuffer = arrayBuffer;
- _activeTexture = activeTexture;
- _texture0Binding2D = texture0Binding2D;
- _depthTest = depthTest;
- _depthWrite = depthWrite;
- _depthFunc = depthFunc;
- _blend = blend;
- _blendSourceRgb = blendSourceRgb;
- _blendDestinationRgb = blendDestinationRgb;
- _blendSourceAlpha = blendSourceAlpha;
- _blendDestinationAlpha = blendDestinationAlpha;
- _cullFace = cullFace;
- _cullFaceMode = cullFaceMode;
- _frontFace = frontFace;
- _alphaToCoverage = alphaToCoverage;
- _multisample = multisample;
- _stencilTest = stencilTest;
- _stencilFunc = stencilFunc;
- _stencilReference = stencilReference;
- _stencilValueMask = stencilValueMask;
- _stencilWriteMask = stencilWriteMask;
- _stencilFail = stencilFail;
- _stencilDepthFail = stencilDepthFail;
- _stencilPass = stencilPass;
- _colorMaskRed = colorMaskRed;
- _colorMaskGreen = colorMaskGreen;
- _colorMaskBlue = colorMaskBlue;
- _colorMaskAlpha = colorMaskAlpha;
- }
-
- internal static GlAmbientCapabilityState Capture(IGlAmbientStateApi gl)
- {
- ArgumentNullException.ThrowIfNull(gl);
- int program = gl.GetInteger(GetPName.CurrentProgram);
- int vertexArray = gl.GetInteger(GetPName.VertexArrayBinding);
- int arrayBuffer = gl.GetInteger(GetPName.ArrayBufferBinding);
- int activeTexture = gl.GetInteger(GetPName.ActiveTexture);
-
- int texture0Binding2D;
- try
- {
- gl.ActiveTexture(TextureUnit.Texture0);
- texture0Binding2D = gl.GetInteger(GetPName.TextureBinding2D);
- }
- finally
- {
- gl.ActiveTexture((TextureUnit)activeTexture);
- }
-
- bool[] colorMask = gl.GetColorMask();
- return new GlAmbientCapabilityState(
- program, vertexArray, arrayBuffer, activeTexture, texture0Binding2D,
- gl.IsEnabled(EnableCap.DepthTest),
- gl.GetBoolean(GetPName.DepthWritemask),
- gl.GetInteger(GetPName.DepthFunc),
- gl.IsEnabled(EnableCap.Blend),
- gl.GetInteger(GetPName.BlendSrcRgb),
- gl.GetInteger(GetPName.BlendDstRgb),
- gl.GetInteger(GetPName.BlendSrcAlpha),
- gl.GetInteger(GetPName.BlendDstAlpha),
- gl.IsEnabled(EnableCap.CullFace),
- gl.GetInteger(GetPName.CullFaceMode),
- gl.GetInteger(GetPName.FrontFace),
- gl.IsEnabled(EnableCap.SampleAlphaToCoverage),
- gl.IsEnabled(EnableCap.Multisample),
- gl.IsEnabled(EnableCap.StencilTest),
- gl.GetInteger(GetPName.StencilFunc),
- gl.GetInteger(GetPName.StencilRef),
- gl.GetInteger(GetPName.StencilValueMask),
- gl.GetInteger(GetPName.StencilWritemask),
- gl.GetInteger(GetPName.StencilFail),
- gl.GetInteger(GetPName.StencilPassDepthFail),
- gl.GetInteger(GetPName.StencilPassDepthPass),
- colorMask[0], colorMask[1], colorMask[2], colorMask[3]);
- }
-
- internal void Restore(IGlAmbientStateApi gl)
- {
- ArgumentNullException.ThrowIfNull(gl);
- gl.UseProgram((uint)_program);
- gl.BindVertexArray((uint)_vertexArray);
- gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer);
-
- gl.ActiveTexture(TextureUnit.Texture0);
- gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D);
- gl.ActiveTexture((TextureUnit)_activeTexture);
-
- gl.SetCapability(EnableCap.DepthTest, _depthTest);
- gl.DepthMask(_depthWrite);
- gl.DepthFunc((DepthFunction)_depthFunc);
-
- gl.SetCapability(EnableCap.Blend, _blend);
- gl.BlendFuncSeparate(
- (BlendingFactor)_blendSourceRgb,
- (BlendingFactor)_blendDestinationRgb,
- (BlendingFactor)_blendSourceAlpha,
- (BlendingFactor)_blendDestinationAlpha);
-
- gl.SetCapability(EnableCap.CullFace, _cullFace);
- gl.CullFace((TriangleFace)_cullFaceMode);
- gl.FrontFace((FrontFaceDirection)_frontFace);
-
- gl.SetCapability(EnableCap.SampleAlphaToCoverage, _alphaToCoverage);
- gl.SetCapability(EnableCap.Multisample, _multisample);
-
- gl.SetCapability(EnableCap.StencilTest, _stencilTest);
- gl.StencilFunc(
- (StencilFunction)_stencilFunc,
- _stencilReference,
- (uint)_stencilValueMask);
- gl.StencilOp(
- (StencilOp)_stencilFail,
- (StencilOp)_stencilDepthFail,
- (StencilOp)_stencilPass);
- gl.StencilMask((uint)_stencilWriteMask);
-
- gl.ColorMask(_colorMaskRed, _colorMaskGreen, _colorMaskBlue, _colorMaskAlpha);
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlDirtySlotRuns.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlDirtySlotRuns.cs
deleted file mode 100644
index bc0766a4..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlDirtySlotRuns.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// Tracks which texture-table slots changed since the last flush and gives them
-/// back as maximal runs of consecutive dirty slots.
-///
-/// Why runs, and not one merged range. The ring can flush a single
-/// span covering everything written since the last draw, because every byte in
-/// that span was allocated this frame and nothing in flight reads it. The
-/// texture table cannot: it is a long-lived array of bindless handles, and two
-/// registrations in one frame can land on slots 5 and 50 with forty-four live
-/// slots in between. Those live slots are read by draws already submitted this
-/// frame, so a single mapped write over [5, 51) — mapped with
-/// GL_MAP_INVALIDATE_RANGE_BIT, which lets the driver discard the whole
-/// range's contents while it is mapped, and GL_MAP_UNSYNCHRONIZED_BIT,
-/// which stops it from waiting first — would expose those draws to torn
-/// handles. Splitting on the gaps keeps every mapped range made only of slots
-/// that no submitted draw can be reading.
-///
-/// Why a dirty slot is safe to write unsynchronized. Two producers
-/// touch a slot: GlGpuDevice.RegisterTexture, which writes a slot fresh
-/// from and therefore one no batch has ever
-/// indexed; and ReleaseTextureSlot's zeroing write, which runs inside a
-/// retirement callback, i.e. after the fence covering every frame that could
-/// still have referenced it. Both are already past the point where the GPU can
-/// read the old value — which is exactly the assertion
-/// GL_MAP_UNSYNCHRONIZED_BIT makes.
-///
-/// GL-free by design, like and
-/// , so the bookkeeping is unit tested
-/// without a context. Enumeration is allocation-free: the tracker keeps the
-/// min/max window that has been dirtied so a clean table costs one comparison,
-/// and a dirty one scans only that window.
-///
-internal sealed class GlDirtySlotRuns
-{
- private readonly bool[] _dirty;
- private int _windowStart = -1;
- private int _windowEnd = -1;
-
- public GlDirtySlotRuns(uint capacity)
- {
- ArgumentOutOfRangeException.ThrowIfZero(capacity);
- _dirty = new bool[capacity];
- }
-
- public uint Capacity => (uint)_dirty.Length;
-
- public bool HasDirtySlots => _windowStart >= 0;
-
- public void Mark(uint slot)
- {
- if (slot >= (uint)_dirty.Length)
- {
- throw new ArgumentOutOfRangeException(
- nameof(slot),
- slot,
- $"The tracked table holds {_dirty.Length} slots.");
- }
-
- _dirty[slot] = true;
- _windowStart = _windowStart < 0 ? (int)slot : Math.Min(_windowStart, (int)slot);
- _windowEnd = Math.Max(_windowEnd, (int)slot + 1);
- }
-
- ///
- /// Takes the lowest remaining run of consecutive dirty slots, clearing it,
- /// and reports its first slot and length. Returns false once none
- /// remain, so a caller drains with a while loop.
- ///
- public bool TryTakeNextRun(out uint firstSlot, out uint slotCount)
- {
- firstSlot = 0;
- slotCount = 0;
- if (_windowStart < 0)
- return false;
-
- int index = _windowStart;
- while (index < _windowEnd && !_dirty[index])
- index++;
- if (index >= _windowEnd)
- {
- CloseWindow();
- return false;
- }
-
- int start = index;
- while (index < _windowEnd && _dirty[index])
- {
- _dirty[index] = false;
- index++;
- }
-
- firstSlot = (uint)start;
- slotCount = (uint)(index - start);
- if (index >= _windowEnd)
- CloseWindow();
- else
- _windowStart = index;
- return true;
- }
-
- private void CloseWindow()
- {
- _windowStart = -1;
- _windowEnd = -1;
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlEnumMapping.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlEnumMapping.cs
deleted file mode 100644
index f9ed094a..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlEnumMapping.cs
+++ /dev/null
@@ -1,145 +0,0 @@
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-/// One vertex attribute's GL shape: component count, element type, and whether integer values normalize to [0,1]/[-1,1].
-///
-/// How one vertex attribute reaches GL. selects
-/// glVertexAttribIPointer over glVertexAttribPointer: GL requires
-/// the integer entry point for an integer shader input (uvec4 and friends)
-/// and leaves the value undefined otherwise.
-///
-internal readonly record struct GlVertexAttributeShape(
- int ComponentCount,
- VertexAttribPointerType Type,
- bool Normalized,
- bool Integer = false);
-
-///
-/// Pure, GL-context-free mappings from the RHI's backend-neutral enums to
-/// Silk.NET's OpenGL enum values. Kept as small switch expressions so each
-/// mapping is independently unit-testable and — per the campaign spec's
-/// "when in doubt, set the state" rule — every backend-neutral value has an
-/// explicit case rather than a fallthrough default.
-///
-internal static class GlEnumMapping
-{
- public static GlVertexAttributeShape VertexShapeOf(GpuVertexFormat format) => format switch
- {
- GpuVertexFormat.Float1 => new GlVertexAttributeShape(1, VertexAttribPointerType.Float, false),
- GpuVertexFormat.Float2 => new GlVertexAttributeShape(2, VertexAttribPointerType.Float, false),
- GpuVertexFormat.Float3 => new GlVertexAttributeShape(3, VertexAttribPointerType.Float, false),
- GpuVertexFormat.Float4 => new GlVertexAttributeShape(4, VertexAttribPointerType.Float, false),
- GpuVertexFormat.UByte4Normalized => new GlVertexAttributeShape(4, VertexAttribPointerType.UnsignedByte, true),
- // Integer attributes carry Integer = true; the encoder must route them
- // through glVertexAttribIPointer, not the normalized float path.
- GpuVertexFormat.UByte4UInt => new GlVertexAttributeShape(4, VertexAttribPointerType.UnsignedByte, false, Integer: true),
- // Slice V6l: particle.vert's per-instance `in uint aTextureIndex`.
- GpuVertexFormat.UInt1 => new GlVertexAttributeShape(1, VertexAttribPointerType.UnsignedInt, false, Integer: true),
- _ => throw new NotSupportedException($"No GL vertex attribute shape for {format}."),
- };
-
- /// Slice V6l: the stencil comparison, which shares GL's depth-function enum values.
- public static StencilFunction StencilFunctionOf(GpuCompareOp compare) => compare switch
- {
- GpuCompareOp.Never => StencilFunction.Never,
- GpuCompareOp.Less => StencilFunction.Less,
- GpuCompareOp.LessOrEqual => StencilFunction.Lequal,
- GpuCompareOp.Equal => StencilFunction.Equal,
- GpuCompareOp.Greater => StencilFunction.Greater,
- GpuCompareOp.GreaterOrEqual => StencilFunction.Gequal,
- GpuCompareOp.Always => StencilFunction.Always,
- _ => throw new NotSupportedException($"No GL stencil function for {compare}."),
- };
-
- /// Slice V6l: what a stencil outcome does to the stored value.
- public static StencilOp StencilOpOf(GpuStencilOp op) => op switch
- {
- GpuStencilOp.Keep => StencilOp.Keep,
- GpuStencilOp.Zero => StencilOp.Zero,
- GpuStencilOp.Replace => StencilOp.Replace,
- _ => throw new NotSupportedException($"No GL stencil operation for {op}."),
- };
-
- public static PrimitiveType PrimitiveTypeOf(GpuPrimitiveTopology topology) => topology switch
- {
- GpuPrimitiveTopology.TriangleList => PrimitiveType.Triangles,
- GpuPrimitiveTopology.LineList => PrimitiveType.Lines,
- _ => throw new NotSupportedException($"No GL primitive type for {topology}."),
- };
-
- public static DrawElementsType DrawElementsTypeOf(GpuIndexType indexType) => indexType switch
- {
- GpuIndexType.UInt16 => DrawElementsType.UnsignedShort,
- GpuIndexType.UInt32 => DrawElementsType.UnsignedInt,
- _ => throw new NotSupportedException($"No GL index type for {indexType}."),
- };
-
- public static int IndexSizeBytesOf(GpuIndexType indexType) => indexType switch
- {
- GpuIndexType.UInt16 => sizeof(ushort),
- GpuIndexType.UInt32 => sizeof(uint),
- _ => throw new NotSupportedException($"No index byte size for {indexType}."),
- };
-
- public static DepthFunction DepthFunctionOf(GpuCompareOp compareOp) => compareOp switch
- {
- GpuCompareOp.Never => DepthFunction.Never,
- GpuCompareOp.Less => DepthFunction.Less,
- GpuCompareOp.LessOrEqual => DepthFunction.Lequal,
- GpuCompareOp.Equal => DepthFunction.Equal,
- GpuCompareOp.Greater => DepthFunction.Greater,
- GpuCompareOp.GreaterOrEqual => DepthFunction.Gequal,
- GpuCompareOp.Always => DepthFunction.Always,
- _ => throw new NotSupportedException($"No GL depth function for {compareOp}."),
- };
-
- public static TriangleFace CullFaceModeOf(GpuCullMode cullMode) => cullMode switch
- {
- GpuCullMode.Back => TriangleFace.Back,
- GpuCullMode.Front => TriangleFace.Front,
- // GpuCullMode.None never reaches glCullFace — culling is disabled instead.
- _ => throw new NotSupportedException($"No GL cull face mode for {cullMode}."),
- };
-
- public static FrontFaceDirection FrontFaceDirectionOf(GpuFrontFace frontFace) => frontFace switch
- {
- GpuFrontFace.CounterClockwise => FrontFaceDirection.Ccw,
- GpuFrontFace.Clockwise => FrontFaceDirection.CW,
- _ => throw new NotSupportedException($"No GL front-face direction for {frontFace}."),
- };
-
- public static (BlendingFactor Source, BlendingFactor Destination) BlendFactorsOf(GpuBlendMode blend) => blend switch
- {
- GpuBlendMode.StraightAlpha => (BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha),
- GpuBlendMode.Additive => (BlendingFactor.SrcAlpha, BlendingFactor.One),
- GpuBlendMode.InverseAlpha => (BlendingFactor.OneMinusSrcAlpha, BlendingFactor.SrcAlpha),
- // GpuBlendMode.None never reaches glBlendFunc — blending is disabled instead.
- _ => throw new NotSupportedException($"No GL blend factors for {blend}."),
- };
-
- public static TextureMinFilter MinFilterOf(GpuFilter filter, GpuMipFilter mipFilter) => (filter, mipFilter) switch
- {
- (GpuFilter.Nearest, GpuMipFilter.None) => TextureMinFilter.Nearest,
- (GpuFilter.Linear, GpuMipFilter.None) => TextureMinFilter.Linear,
- (GpuFilter.Nearest, GpuMipFilter.Nearest) => TextureMinFilter.NearestMipmapNearest,
- (GpuFilter.Linear, GpuMipFilter.Nearest) => TextureMinFilter.LinearMipmapNearest,
- (GpuFilter.Nearest, GpuMipFilter.Linear) => TextureMinFilter.NearestMipmapLinear,
- (GpuFilter.Linear, GpuMipFilter.Linear) => TextureMinFilter.LinearMipmapLinear,
- _ => throw new NotSupportedException($"No GL min filter for {filter}/{mipFilter}."),
- };
-
- public static TextureMagFilter MagFilterOf(GpuFilter filter) => filter switch
- {
- GpuFilter.Nearest => TextureMagFilter.Nearest,
- GpuFilter.Linear => TextureMagFilter.Linear,
- _ => throw new NotSupportedException($"No GL mag filter for {filter}."),
- };
-
- public static TextureWrapMode WrapModeOf(GpuAddressMode addressMode) => addressMode switch
- {
- GpuAddressMode.Repeat => TextureWrapMode.Repeat,
- GpuAddressMode.ClampToEdge => TextureWrapMode.ClampToEdge,
- _ => throw new NotSupportedException($"No GL wrap mode for {addressMode}."),
- };
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuBuffer.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuBuffer.cs
deleted file mode 100644
index 63ce8249..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuBuffer.cs
+++ /dev/null
@@ -1,287 +0,0 @@
-using AcDream.App.Rendering;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// A plain GL buffer object. Deliberately not one of the existing
-/// ManagedGL* wrappers: those implement Chorizite's IVertexBuffer/
-/// IIndexBuffer (generic-over-IVertex, single-usage) and are
-/// constructed against , which the campaign
-/// explicitly sheds — is a fresh root. One GL buffer
-/// object already serves every combination (the
-/// usage only matters at bind time), so a single small class covers the whole
-/// surface without forking per usage.
-///
-/// Allocated once at the description's size via glBufferData with null
-/// data. There are two write paths after that, and which one applies is a
-/// property of the buffer's role rather than of the buffer object:
-/// is an ordinary synchronized glBufferSubData, used
-/// for one-off and streaming writes the driver must order for us (the mesh
-/// arena, texture staging); maps the
-/// range and is used only by the per-frame upload ring, whose non-overlap
-/// invariant the caller can state. Neither path holds a persistent mapping.
-///
-/// The glBufferData usage hint follows
-/// :
-/// is written rarely and read by
-/// many draws, so it takes StaticDraw; the host-writable rings and
-/// tables are rewritten every frame and take DynamicDraw. The hint is
-/// advisory to the driver, but keeping it per-residency is what let the mesh
-/// arena (Campaign V slice V4b) move onto this class without changing the
-/// allocation it has always requested.
-///
-internal sealed class GlGpuBuffer : IGpuBuffer
-{
- private readonly GL _gl;
- private readonly IGpuResourceRetirementQueue _retirement;
- private uint _name;
-
- public GlGpuBuffer(GL gl, IGpuResourceRetirementQueue retirement, GpuBufferDescription description)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
- Name = description.Name;
- SizeBytes = description.SizeBytes;
- Usage = description.Usage;
- Residency = description.Residency;
-
- _name = GlResourceCommand.CreateName(_gl, $"buffer '{Name}'", _gl.GenBuffer, _gl.DeleteBuffer);
- try
- {
- _gl.BindBuffer(GLEnum.CopyWriteBuffer, _name);
- unsafe
- {
- _gl.BufferData(GLEnum.CopyWriteBuffer, (nuint)SizeBytes, null, UsageHintFor(Residency));
- }
- GLHelpers.ThrowOnResourceError(_gl, $"allocate buffer '{Name}' ({SizeBytes} bytes)");
- _gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
- }
- catch
- {
- // A rejected data store (GL_OUT_OF_MEMORY is a real outcome for the
- // mesh arena's 384 MiB growth destination) must not strand the name
- // that was already created for it. The caller sees the original
- // failure and owns nothing.
- GlResourceCommand.DeleteBuffer(
- _gl,
- _name,
- $"rollback buffer '{Name}' after a failed allocation");
- _name = 0;
- throw;
- }
- }
-
- public string Name { get; }
- public long SizeBytes { get; }
- public GpuBufferUsage Usage { get; }
- public GpuMemoryResidency Residency { get; }
-
- /// The physical GL buffer name. For bind calls issued by .
- internal uint GlName => _name;
-
- private static BufferUsageARB UsageHintFor(GpuMemoryResidency residency) =>
- residency == GpuMemoryResidency.DeviceLocal
- ? BufferUsageARB.StaticDraw
- : BufferUsageARB.DynamicDraw;
-
- ///
- /// Deletes the physical buffer on the calling thread instead of deferring it
- /// through the device's retirement queue the way does.
- ///
- /// Campaign V slice V4b: the mesh arena (GlobalMeshBuffer) already gates
- /// every arena delete behind its own GpuRetirementLedger and decrements
- /// its MaximumPhysicalArenaBytes accounting in the same retirement stage.
- /// Routing the physical free through the queue a second time would delay it by
- /// a further flight generation, so the arena's physical-capacity accounting
- /// would run ahead of real GPU residency and could admit a migration that
- /// breaches the 896 MiB dual-generation ceiling. A caller of this method must
- /// therefore already have proved no submitted frame can still reference the
- /// buffer.
- ///
- /// Retryable by construction: the managed name is cleared only after the driver
- /// reports success, so a failed delete is re-issued by the next attempt, and any
- /// later call (including ) is a no-op.
- ///
- internal void DeleteRetired(string context)
- {
- uint name = _name;
- if (name == 0)
- return;
-
- _gl.DeleteBuffer(name);
- // Per the GL error contract a command which generates an error does not
- // change object state, so validation stays in the same stage as the
- // mutation and the name is only surrendered once deletion committed.
- GLHelpers.ThrowOnResourceError(_gl, context);
- _name = 0;
- }
-
- public void Upload(long offsetBytes, ReadOnlySpan data)
- {
- ThrowIfDisposed();
- if (offsetBytes < 0 || offsetBytes + data.Length > SizeBytes)
- {
- throw new ArgumentOutOfRangeException(
- nameof(offsetBytes),
- $"Upload of {data.Length} bytes at offset {offsetBytes} exceeds buffer '{Name}' ({SizeBytes} bytes).");
- }
- if (data.IsEmpty)
- return;
-
- _gl.BindBuffer(GLEnum.CopyWriteBuffer, _name);
- unsafe
- {
- fixed (byte* pointer = data)
- _gl.BufferSubData(GLEnum.CopyWriteBuffer, (nint)offsetBytes, (nuint)data.Length, pointer);
- }
- GLHelpers.ThrowOnResourceError(_gl, $"upload {data.Length} bytes to buffer '{Name}' at offset {offsetBytes}");
- _gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
- }
-
- ///
- /// Writes at through
- /// glMapBufferRange with
- /// GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT
- /// — the canonical GL upload-ring idiom, and the only write path the
- /// per-frame ring uses.
- ///
- /// Why not . A partial glBufferSubData
- /// into a buffer object that already-submitted draws are still reading leaves
- /// the driver to guess how to keep both truths alive: it can stall, it can
- /// rename the whole data store and copy the untouched remainder forward, or
- /// it can route the write through an internal staging copy. Which one it
- /// picks is a heuristic, and the heuristic is fed by the update pattern —
- /// Campaign V's V4c revert (plan §5.5) saw a world that rendered blank
- /// roughly one launch in three, with no GL error at any point, on the
- /// connected path that issues 10–40 such partial updates per frame, and zero
- /// times on the offline path that issues 2–4. Mapping the range instead
- /// removes the guess: the three bits together say "I am writing this range,
- /// I am overwriting all of it, and nothing in flight reads it," which is
- /// exactly the ring's actual invariant.
- ///
- /// The precondition is the caller's to prove.
- /// GL_MAP_UNSYNCHRONIZED_BIT means the driver inserts no wait, so the
- /// caller asserts that no GL command already submitted and not yet complete
- /// reads any byte of [offsetBytes, offsetBytes + data.Length). The ring
- /// has that invariant on both axes: within a frame the allocation cursor only
- /// moves forward and refuses a write below the
- /// flushed high-water mark, and across frames a slot is only rewritten after
- /// GpuFrameFlightController.BeginFrame has waited on the fence for the
- /// last frame that used it. GL_MAP_INVALIDATE_RANGE_BIT is likewise
- /// only sound because every byte of the mapped range is then written.
- ///
- /// A false return from glUnmapBuffer means the data store
- /// was lost while mapped (a GPU reset or display-mode change), so the write
- /// did not land. That throws rather than being retried: the frame's uploads
- /// are already partly gone, and silently continuing would draw from a
- /// half-written ring.
- ///
- internal unsafe void WriteRangeUnsynchronized(long offsetBytes, ReadOnlySpan data, string context)
- {
- ThrowIfDisposed();
- if (offsetBytes < 0 || offsetBytes + data.Length > SizeBytes)
- {
- throw new ArgumentOutOfRangeException(
- nameof(offsetBytes),
- $"Mapped write of {data.Length} bytes at offset {offsetBytes} exceeds buffer '{Name}' ({SizeBytes} bytes).");
- }
- if (data.IsEmpty)
- return;
-
- _gl.BindBuffer(GLEnum.CopyWriteBuffer, _name);
- void* mapped = _gl.MapBufferRange(
- GLEnum.CopyWriteBuffer,
- (nint)offsetBytes,
- (nuint)data.Length,
- MapBufferAccessMask.WriteBit
- | MapBufferAccessMask.UnsynchronizedBit
- | MapBufferAccessMask.InvalidateRangeBit);
- if (mapped is null)
- {
- // A null return always sets a GL error, so surface that first — it
- // names the actual rejection instead of the symptom.
- GLHelpers.ThrowOnResourceError(
- _gl,
- $"map {data.Length} bytes of buffer '{Name}' at offset {offsetBytes} ({context})");
- throw new InvalidOperationException(
- $"glMapBufferRange returned no pointer for {data.Length} bytes of buffer '{Name}' " +
- $"at offset {offsetBytes} ({context}).");
- }
-
- data.CopyTo(new Span(mapped, data.Length));
-
- bool unmapped = _gl.UnmapBuffer(GLEnum.CopyWriteBuffer);
- GLHelpers.ThrowOnResourceError(_gl, $"unmap buffer '{Name}' after writing {context}");
- _gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
- if (!unmapped)
- {
- throw new InvalidOperationException(
- $"Buffer '{Name}' lost its data store while {data.Length} bytes at offset " +
- $"{offsetBytes} ({context}) were mapped; the write did not land.");
- }
- }
-
- public void CopyTo(IGpuBuffer destination, long sourceOffsetBytes, long destinationOffsetBytes, long byteCount)
- {
- ThrowIfDisposed();
- ArgumentNullException.ThrowIfNull(destination);
- if (destination is not GlGpuBuffer target)
- throw new ArgumentException("A GL device can only copy into a GL buffer.", nameof(destination));
- if (byteCount == 0)
- return;
-
- _gl.BindBuffer(GLEnum.CopyReadBuffer, _name);
- _gl.BindBuffer(GLEnum.CopyWriteBuffer, target._name);
- _gl.CopyBufferSubData(
- GLEnum.CopyReadBuffer,
- GLEnum.CopyWriteBuffer,
- (nint)sourceOffsetBytes,
- (nint)destinationOffsetBytes,
- (nuint)byteCount);
- GLHelpers.ThrowOnResourceError(
- _gl,
- $"copy {byteCount} bytes from buffer '{Name}' to '{target.Name}'");
- _gl.BindBuffer(GLEnum.CopyReadBuffer, 0);
- _gl.BindBuffer(GLEnum.CopyWriteBuffer, 0);
- }
-
- public void Read(long offsetBytes, Span destination)
- {
- ThrowIfDisposed();
- if (destination.IsEmpty)
- return;
-
- _gl.BindBuffer(GLEnum.CopyReadBuffer, _name);
- unsafe
- {
- fixed (byte* pointer = destination)
- _gl.GetBufferSubData(GLEnum.CopyReadBuffer, (nint)offsetBytes, (nuint)destination.Length, pointer);
- }
- GLHelpers.ThrowOnResourceError(_gl, $"read {destination.Length} bytes from buffer '{Name}' at offset {offsetBytes}");
- _gl.BindBuffer(GLEnum.CopyReadBuffer, 0);
- }
-
- public void Dispose()
- {
- uint name = _name;
- if (name == 0)
- return;
- _name = 0;
-
- GL gl = _gl;
- string label = Name;
- _retirement.Retire(() =>
- {
- gl.DeleteBuffer(name);
- GLHelpers.ThrowOnResourceError(gl, $"delete buffer '{label}' ({name})");
- });
- }
-
- private void ThrowIfDisposed()
- {
- if (_name == 0)
- throw new ObjectDisposedException(Name);
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs
deleted file mode 100644
index 9671122d..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs
+++ /dev/null
@@ -1,667 +0,0 @@
-using System.Numerics;
-using System.Runtime.InteropServices;
-using AcDream.App.Diagnostics;
-using AcDream.App.Rendering;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// OpenGL 4.3 implementation of — Campaign V slice V1.
-/// See docs/plans/2026-07-27-vulkan-campaign.md §3 for the pinned
-/// contract this backend fills and §5 for what this slice covers.
-///
-/// Deliberately NOT derived from Chorizite's BaseGraphicsDevice/
-/// — this is a fresh root, which is one of
-/// the things Campaign V sheds. It owns its own
-/// instance (created in the constructor) rather than sharing the legacy WB
-/// render path's; both simply wrap the same stateless
-/// GL_ARB_bindless_texture extension, so two instances coexist safely,
-/// and it lets this device be constructed the moment a GL context and a
-/// exist — no dependency on when the
-/// legacy path happens to detect bindless support during composition.
-///
-/// Ring capacity. Each flight slot gets one managed staging
-/// byte[] and one same-sized GL buffer, both fixed at construction
-/// (default 16 MiB — see ). V1
-/// chose "throw with a message naming the needed size" over "grow by create-
-/// copy-retire" for an over-capacity request: nothing consumes this device
-/// yet, so there is no real per-frame data volume to size against, and a
-/// silent/implicit grow would hide a future renderer's actual working set
-/// from the person porting it. is the pure
-/// piece that enforces this.
-///
-/// Flush discipline. Ring bytes and the texture-handle table are
-/// both flushed immediately before every
-/// Draw/DrawIndexed/MultiDrawIndexedIndirect — see
-/// — never at bind time, so a renderer that
-/// writes after binding still uploads correctly. Both flushes go through
-/// , i.e.
-/// glMapBufferRange with
-/// GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT,
-/// rather than glBufferSubData: these are the only two buffers this
-/// backend rewrites while the frame's own draws are still in flight, and that
-/// is precisely the case where a partial glBufferSubData leaves the
-/// result to a driver heuristic. See that method's remarks and campaign plan
-/// §5.5 for the failure it produced. The two flushes differ in shape because
-/// their non-overlap proofs differ: the ring writes one span (its cursor only
-/// moves forward within a frame), the table writes one span per run of
-/// consecutive dirty slots (see ).
-///
-internal sealed class GlGpuDevice : IGpuDevice
-{
- internal const int DefaultRingCapacityBytesPerSlot = 16 * 1024 * 1024;
-
- // GL tokens not exposed as named Silk.NET GetPName members (mirrors the
- // same raw-hex pattern GraphicalCapabilityRecord.cs already uses for
- // GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS).
- private const int GlMaxShaderStorageBufferBindings = 0x90DD;
- private const int GlMaxClipDistances = 0x0D32;
- private const int GlMaxSamples = 0x8D57;
- private const int GlShaderStorageBufferOffsetAlignment = 0x90DF;
-
- private readonly GL _gl;
- private readonly GpuFrameFlightController _frameFlights;
- private readonly BindlessSupport _bindless;
- private readonly string _shadersDirectory;
- private readonly int _ringCapacityBytesPerSlot;
-
- private readonly GlRingBufferState[] _ringStates;
- private readonly byte[][] _ringStaging;
- private readonly GlGpuBuffer[] _ringBuffers;
-
- private readonly GlTextureSlotAllocator _textureSlotAllocator =
- new(GpuBindingModel.TextureTableCapacity);
- private readonly ulong[] _textureHandleTable = new ulong[GpuBindingModel.TextureTableCapacity];
- private readonly GlGpuBuffer _textureTableBuffer;
- private readonly GlDirtySlotRuns _textureTableDirtySlots =
- new(GpuBindingModel.TextureTableCapacity);
-
- private readonly GlRenderStateCache _renderState = new();
- private readonly GlGpuPushConstantBinder _pushConstants;
- private readonly GlGpuTimerPool _timerPool;
- private readonly Dictionary _samplers = [];
- private readonly List _queuedActions = [];
- private readonly GlGpuTexture _defaultTexture;
-
- private long _nextSerial;
- private bool _disposed;
-
- public GlGpuDevice(
- GL gl,
- GpuFrameFlightController frameFlights,
- string shadersDirectory,
- int ringCapacityBytesPerSlot = DefaultRingCapacityBytesPerSlot)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _frameFlights = frameFlights ?? throw new ArgumentNullException(nameof(frameFlights));
- ArgumentException.ThrowIfNullOrWhiteSpace(shadersDirectory);
- _shadersDirectory = shadersDirectory;
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero(ringCapacityBytesPerSlot);
- _ringCapacityBytesPerSlot = ringCapacityBytesPerSlot;
-
- if (!BindlessSupport.TryCreate(_gl, out BindlessSupport? bindless) || bindless is null)
- {
- throw new NotSupportedException(
- "GlGpuDevice requires GL_ARB_bindless_texture. The startup capability gate " +
- "(GraphicalCapabilityGuard) should already have rejected a driver without it, " +
- "so reaching this means the device was constructed before that gate ran.");
- }
- _bindless = bindless;
-
- Capabilities = CaptureCapabilities();
-
- int slotCount = _frameFlights.SlotCount;
- _ringStates = new GlRingBufferState[slotCount];
- _ringStaging = new byte[slotCount][];
- _ringBuffers = new GlGpuBuffer[slotCount];
- for (int slot = 0; slot < slotCount; slot++)
- {
- _ringStates[slot] = new GlRingBufferState(_ringCapacityBytesPerSlot);
- _ringStaging[slot] = new byte[_ringCapacityBytesPerSlot];
- _ringBuffers[slot] = new GlGpuBuffer(
- _gl,
- Retirement,
- new GpuBufferDescription(
- $"gpu-ring-slot-{slot}",
- _ringCapacityBytesPerSlot,
- GpuBufferUsage.Storage | GpuBufferUsage.Uniform | GpuBufferUsage.Indirect,
- GpuMemoryResidency.HostWritable));
- }
-
- _textureTableBuffer = new GlGpuBuffer(
- _gl,
- Retirement,
- new GpuBufferDescription(
- "gpu-texture-table",
- GpuBindingModel.TextureTableCapacity * sizeof(ulong),
- GpuBufferUsage.Storage,
- GpuMemoryResidency.HostWritable));
-
- _pushConstants = new GlGpuPushConstantBinder(_gl);
- _timerPool = new GlGpuTimerPool(new SilkGlTimerQueryApi(_gl), Capabilities.SupportsTimestampQueries);
-
- _defaultTexture = new GlGpuTexture(
- _gl,
- Retirement,
- new GpuTextureDescription(
- "default-white",
- GpuTextureKind.Texture2D,
- GpuTextureFormat.Rgba8Unorm,
- Width: 1,
- Height: 1,
- LayerCount: 1,
- MipLevelCount: 1));
- _defaultTexture.Upload(0, 0, [255, 255, 255, 255]);
- IGpuSampler defaultSampler = CreateSampler(GpuSamplerDescription.UiNearest);
- DefaultTextureSlot = RegisterTexture(_defaultTexture, defaultSampler);
- }
-
- public GpuBackendKind Backend => GpuBackendKind.OpenGl;
- public GpuCapabilityRecord Capabilities { get; }
- public IGpuResourceRetirementQueue Retirement => _frameFlights;
- public IGpuTimerPool Timers => _timerPool;
- public GpuTextureSlot DefaultTextureSlot { get; }
-
- internal GL Gl => _gl;
- internal GlGpuPushConstantBinder PushConstants => _pushConstants;
- internal GlGpuTimerPool TimerPool => _timerPool;
-
- ///
- /// GL name of the buffer emulating the global texture table
- /// (). Slice V6d:
- /// binds it on every pipeline
- /// bind, which is the GL analogue of the Vulkan backend binding descriptor
- /// set 2 on every draw. Without it an RHI shader that samples the table
- /// would read whichever raw-GL renderer's private handle table was left at
- /// binding 9 — a different slot numbering entirely, which is the loudest
- /// possible way to sample the wrong texture.
- ///
- internal uint TextureTableGlName => _textureTableBuffer.GlName;
-
- internal GlRenderStateSnapshot CurrentRenderState { get; private set; }
-
- public IGpuBuffer CreateBuffer(in GpuBufferDescription description)
- {
- ThrowIfDisposed();
- return new GlGpuBuffer(_gl, Retirement, description);
- }
-
- public IGpuTexture CreateTexture(in GpuTextureDescription description)
- {
- ThrowIfDisposed();
- return new GlGpuTexture(_gl, Retirement, description);
- }
-
- public IGpuSampler CreateSampler(in GpuSamplerDescription description)
- {
- ThrowIfDisposed();
- if (_samplers.TryGetValue(description, out GlGpuSampler? existing))
- return existing;
-
- var created = new GlGpuSampler(_gl, Retirement, description);
- _samplers.Add(description, created);
- return created;
- }
-
- public IGpuPipeline CreatePipeline(GpuPipelineDescription description)
- {
- ThrowIfDisposed();
- ArgumentNullException.ThrowIfNull(description);
- string vertexPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.vert");
- string fragmentPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.frag");
- // Campaign V slice V6d: every RHI pipeline gets the shared preamble,
- // unconditionally. The Vulkan backend injects its own preamble into
- // every shader it compiles, so making the GL side selective would mean
- // one source file compiling against two different sets of definitions
- // depending on which pipeline happened to ask for it. A shader that
- // reads nothing from the preamble simply carries an unused declaration.
- string common = File.ReadAllText(Path.Combine(_shadersDirectory, "common.glsl"));
- string vertexSource = Shader.InjectPreamble(File.ReadAllText(vertexPath), common);
- string fragmentSource = Shader.InjectPreamble(File.ReadAllText(fragmentPath), common);
- return new GlGpuPipeline(_gl, Retirement, description, vertexSource, fragmentSource);
- }
-
- public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description)
- {
- ThrowIfDisposed();
- return new GlGpuRenderTarget(_gl, Retirement, description);
- }
-
- public GpuTextureSlot RegisterTexture(IGpuTexture texture, IGpuSampler sampler)
- {
- ThrowIfDisposed();
- ArgumentNullException.ThrowIfNull(texture);
- ArgumentNullException.ThrowIfNull(sampler);
- if (texture is not GlGpuTexture glTexture)
- throw new ArgumentException("The GL backend can only register a GL texture.", nameof(texture));
- if (sampler is not GlGpuSampler glSampler)
- throw new ArgumentException("The GL backend can only register a GL sampler.", nameof(sampler));
-
- uint slot = _textureSlotAllocator.Allocate();
- ulong handle = _bindless.GetResidentHandle(glTexture.GlName, glSampler.GlName);
- WriteHandle(slot, handle);
- return new GpuTextureSlot(slot);
- }
-
- public void ReleaseTextureSlot(GpuTextureSlot slot)
- {
- ThrowIfDisposed();
- if (!slot.IsAssigned)
- throw new ArgumentException("Cannot release an unassigned texture slot.", nameof(slot));
-
- uint index = slot.Index;
- // Deferred through the retirement queue: a submitted-but-not-yet-
- // retired frame may still read this slot's handle, so the free list
- // (and thus reuse) must wait for that frame to retire.
- Retirement.Retire(() =>
- {
- WriteHandle(index, 0);
- _textureSlotAllocator.Release(index);
- });
- }
-
- public IGpuFrame BeginFrame()
- {
- ThrowIfDisposed();
- _frameFlights.BeginFrame();
- long serial = ++_nextSerial;
- int slot = _frameFlights.CurrentSlot;
- _ringStates[slot].Reset();
- return new GlGpuFrame(this, slot, serial);
- }
-
- internal void EndFrame() => _frameFlights.EndFrame();
-
- internal GpuRingAllocation AllocateRing(int slotIndex, int byteCount, GpuRingUsage usage)
- {
- uint alignment = usage switch
- {
- GpuRingUsage.Storage => Capabilities.MinStorageBufferOffsetAlignment,
- GpuRingUsage.Uniform => Capabilities.MinUniformBufferOffsetAlignment,
- _ => 4u,
- };
- uint offset = _ringStates[slotIndex].Allocate(byteCount, alignment);
- Span data = byteCount == 0
- ? Span.Empty
- : _ringStaging[slotIndex].AsSpan((int)offset, byteCount);
- return new GpuRingAllocation(_ringBuffers[slotIndex], offset, data);
- }
-
- ///
- /// Writes this slot's dirty ring bytes and every dirty run of the texture
- /// table into their GL buffers, then clears both watermarks. Called
- /// immediately before every draw — never at bind time, because a renderer
- /// may still write into a ring allocation after binding it.
- ///
- /// Both writes are mapped and unsynchronized (see the class remarks and
- /// ). The ring's dirty
- /// span may include alignment padding between allocations; those bytes are
- /// below the cursor, were allocated this frame, and are read by nothing, so
- /// covering them in one map is sound and saves a call. The table has no
- /// equivalent licence, which is why it drains run by run.
- ///
- internal void FlushBeforeDraw(int slotIndex)
- {
- (int start, int length) = _ringStates[slotIndex].TakeDirtyRange();
- if (length > 0)
- {
- _ringBuffers[slotIndex].WriteRangeUnsynchronized(
- start,
- _ringStaging[slotIndex].AsSpan(start, length),
- $"ring slot {slotIndex}");
- }
-
- FlushTextureTable();
- }
-
- ///
- /// Drains every dirty run of the texture table into its GL buffer. Called by
- /// for RHI draws, and directly by the still-raw-GL
- /// world renderers before their own draws — see the V4t remarks on
- /// .
- ///
- internal void FlushTextureTable()
- {
- while (_textureTableDirtySlots.TryTakeNextRun(out uint firstSlot, out uint slotCount))
- {
- ReadOnlySpan bytes = MemoryMarshal.AsBytes(
- _textureHandleTable.AsSpan((int)firstSlot, (int)slotCount));
- _textureTableBuffer.WriteRangeUnsynchronized(
- (long)firstSlot * sizeof(ulong),
- bytes,
- $"texture table slots {firstSlot}..{firstSlot + slotCount - 1}");
- }
- }
-
- internal void BeginPass(GpuPassDescription description)
- {
- ThrowIfDisposed();
- ArgumentNullException.ThrowIfNull(description);
- if (description.Color.Store == GpuStoreOp.Resolve
- || description.Depth is { Store: GpuStoreOp.Resolve })
- {
- throw new NotSupportedException(
- "GpuStoreOp.Resolve is a Vulkan-only path; Campaign V slice V1's GL backend " +
- "only ever renders single-sampled targets.");
- }
-
- uint framebuffer = 0;
- if (description.Color.Target is { } target)
- {
- if (target is not GlGpuRenderTarget glTarget)
- throw new ArgumentException("The GL backend can only render into a GL render target.");
- framebuffer = glTarget.GlFramebufferName;
- }
- _gl.BindFramebuffer(GLEnum.Framebuffer, framebuffer);
-
- bool clearsColor = description.Color.Load == GpuLoadOp.Clear;
- bool clearsDepth = description.Depth is { Load: GpuLoadOp.Clear };
- if (clearsColor || clearsDepth)
- {
- // Force the write masks on before clearing, regardless of what
- // the previous pass's last draw left them at (e.g. depth-write
- // disabled mid-translucent-pass) — glClear silently no-ops for a
- // buffer whose mask is off.
- ClearBufferMask mask = 0;
- if (clearsColor)
- {
- _gl.ColorMask(true, true, true, true);
- Vector4 clearColor = description.Color.ClearColor;
- _gl.ClearColor(clearColor.X, clearColor.Y, clearColor.Z, clearColor.W);
- mask |= ClearBufferMask.ColorBufferBit;
- }
- if (description.Depth is { Load: GpuLoadOp.Clear } depth)
- {
- _gl.DepthMask(true);
- _gl.ClearDepth(depth.ClearDepth);
- _gl.ClearStencil((int)depth.ClearStencil);
- mask |= ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit;
- }
- _gl.Clear(mask);
- GLHelpers.ThrowOnResourceError(_gl, $"clear pass '{description.Name}'");
- }
-
- // Campaign V slice V4a (2026-07-27 revert postmortem, plan §7.1 rule 2):
- // reset unconditionally on EVERY pass, not only a clearing one. While
- // raw-GL renderers coexist with RHI-ported ones (through V4h), a
- // raw-GL renderer can run between two RHI passes within the same frame
- // and change GL program/blend/depth/cull state the cache never
- // observes. A reset gated on "this pass cleared" left the cache
- // trusting a stale belief in that case, so a later BindPipeline
- // skipped re-issuing glUseProgram and the following push-constant
- // upload threw GL_INVALID_OPERATION against whatever program was
- // actually bound — exactly the failure the first V4a attempt hit.
- // Resetting on every BeginPass costs one redundant state application
- // on the pass's first bind and is removed at V4h once nothing raw-GL
- // remains.
- _renderState.Reset();
- }
-
- // Campaign V slice V6k deleted the V4a pre-approved transitional seam
- // (RegisterExternalColorTexture / TryResolveExternalColorTexture, campaign
- // doc §7.1's final paragraph). It existed so the retained UI could blit the
- // paperdoll and creature-appraisal viewport textures while their renderer
- // still owned a hand-rolled FBO the RHI knew nothing about. That renderer now
- // creates an IGpuRenderTarget and registers its colour attachment through
- // RegisterTexture like anything else, so the escape hatch has no caller —
- // exactly the end §7.1 wrote for it.
-
- // ── V4t transitional seam: the world texture stack's entry to this table ──
- //
- // Campaign V slice V4t moves the world's CPU data model off the raw 64-bit
- // ARB_bindless_texture handle and onto GpuTextureSlot, but §5.5.6 closed the
- // GL re-land of V4c/V4d, so WbDrawDispatcher, EnvCellRenderer,
- // TerrainModernRenderer and ParticleRenderer still submit through raw GL and
- // cannot reach FlushBeforeDraw. They therefore call FlushTextureTable and
- // bind TextureTableGlName at GpuBindingModel.StorageTextureTable themselves,
- // immediately before their own draws — the same shape their retired private
- // GlBindlessHandleTable had, against the one table that is now the device's.
- //
- // Residency ownership does NOT move. Each world texture is created, made
- // resident and destroyed by its own cache (TerrainAtlas,
- // CompositeTextureArrayCache, StandaloneBindlessTextureCache,
- // ManagedGLTextureArray); this device only owns the table entry, keyed 1:1 by
- // the caller's already-resident handle. That is what separates this from
- // RegisterTexture, which owns the residency it creates.
- //
- // Deleted with the raw-GL world path when the Vulkan world arm lands, at
- // which point every one of those caches registers through RegisterTexture.
- private readonly Dictionary _worldTextureSlotsByHandle = new();
-
- ///
- /// Interns an already-resident world texture handle into the device's table
- /// and returns its slot. Idempotent: the same handle always resolves to the
- /// same slot until retires it, which
- /// is what lets a cache call this per draw rather than tracking the slot.
- ///
- internal GpuTextureSlot RegisterWorldTextureHandle(ulong residentHandle)
- {
- ThrowIfDisposed();
- if (residentHandle == 0)
- return GpuTextureSlot.Unassigned;
- if (_worldTextureSlotsByHandle.TryGetValue(residentHandle, out GpuTextureSlot existing))
- return existing;
-
- uint slotIndex = _textureSlotAllocator.Allocate();
- WriteHandle(slotIndex, residentHandle);
- var slot = new GpuTextureSlot(slotIndex);
- _worldTextureSlotsByHandle.Add(residentHandle, slot);
- return slot;
- }
-
- ///
- /// Retires the table entry for a world handle the caller is about to make
- /// non-resident. The slot itself returns to the free list only once the
- /// retirement queue confirms no submitted frame can still read it, exactly
- /// as for . A handle that was never
- /// registered is a no-op, so a cache may call this unconditionally on its
- /// teardown path.
- ///
- internal void ReleaseWorldTextureHandle(ulong residentHandle)
- {
- if (residentHandle == 0)
- return;
- if (!_worldTextureSlotsByHandle.Remove(residentHandle, out GpuTextureSlot slot))
- return;
- ReleaseTextureSlot(slot);
- }
-
- /// Live world-handle registrations. Diagnostics and tests only.
- internal int WorldTextureSlotCount => _worldTextureSlotsByHandle.Count;
-
- internal void ApplyRenderState(GlRenderStateSnapshot desired)
- {
- GlRenderStateChanges changes = _renderState.Apply(desired);
- CurrentRenderState = desired;
- if (!changes.AnyChange)
- return;
-
- if (changes.Program)
- _gl.UseProgram(desired.Program);
- if (changes.Blend)
- {
- if (desired.Blend == GpuBlendMode.None)
- {
- _gl.Disable(EnableCap.Blend);
- }
- else
- {
- _gl.Enable(EnableCap.Blend);
- (BlendingFactor source, BlendingFactor destination) = GlEnumMapping.BlendFactorsOf(desired.Blend);
- _gl.BlendFunc(source, destination);
- }
- }
- if (changes.DepthTest)
- {
- if (desired.DepthTest)
- _gl.Enable(EnableCap.DepthTest);
- else
- _gl.Disable(EnableCap.DepthTest);
- }
- if (changes.DepthWrite)
- _gl.DepthMask(desired.DepthWrite);
- if (changes.DepthCompare)
- _gl.DepthFunc(GlEnumMapping.DepthFunctionOf(desired.DepthCompare));
- if (changes.Cull)
- {
- if (desired.Cull == GpuCullMode.None)
- {
- _gl.Disable(EnableCap.CullFace);
- }
- else
- {
- _gl.Enable(EnableCap.CullFace);
- _gl.CullFace(GlEnumMapping.CullFaceModeOf(desired.Cull));
- }
- }
- if (changes.FrontFace)
- _gl.FrontFace(GlEnumMapping.FrontFaceDirectionOf(desired.FrontFace));
- if (changes.AlphaToCoverage)
- {
- if (desired.AlphaToCoverage)
- _gl.Enable(EnableCap.SampleAlphaToCoverage);
- else
- _gl.Disable(EnableCap.SampleAlphaToCoverage);
- }
- if (changes.ColorWrite)
- {
- _gl.ColorMask(desired.ColorWrite, desired.ColorWrite, desired.ColorWrite, desired.ColorWrite);
- }
- // Slice V6l. The stencil VALUES are issued whenever the test is on, even
- // if only the enable changed: GL keeps func/op/mask as global state that a
- // raw-GL renderer may have moved since this cache last saw it, and the
- // portal punch's correctness depends on the exact triple it asked for.
- if (changes.StencilTest)
- {
- if (desired.StencilTest)
- _gl.Enable(EnableCap.StencilTest);
- else
- _gl.Disable(EnableCap.StencilTest);
- }
- if (desired.StencilTest && (changes.StencilTest || changes.Stencil))
- {
- GpuStencilState stencil = desired.Stencil;
- _gl.StencilFunc(
- GlEnumMapping.StencilFunctionOf(stencil.Compare),
- (int)stencil.Reference,
- stencil.CompareMask);
- _gl.StencilOp(
- GlEnumMapping.StencilOpOf(stencil.Fail),
- GlEnumMapping.StencilOpOf(stencil.DepthFail),
- GlEnumMapping.StencilOpOf(stencil.Pass));
- _gl.StencilMask(stencil.WriteMask);
- }
- GLHelpers.ThrowOnResourceError(_gl, "apply GL render state");
- }
-
- public void QueueDeviceAction(Action action)
- {
- ArgumentNullException.ThrowIfNull(action);
- _queuedActions.Add(action);
- }
-
- public void ProcessDeviceActions()
- {
- Action[] pending = [.. _queuedActions];
- _queuedActions.Clear();
- foreach (Action action in pending)
- action();
- }
-
- public byte[] CaptureBackbuffer(int width, int height)
- {
- ThrowIfDisposed();
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width);
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height);
-
- // FrameScreenshotController owns the one backbuffer read in the process:
- // it names framebuffer 0 rather than inheriting a binding, and it
- // resolves the multisampled default framebuffer before reading, because
- // glReadPixels against a multisampled read framebuffer is undefined.
- // A second implementation here would be a second instrument to keep
- // sound. FlipRows is the same top-left-origin flip the screenshot gates
- // rely on, so this seam stays byte-for-byte compatible with them.
- byte[] pixels = FrameScreenshotController.ReadDefaultFramebuffer(_gl, width, height);
- GLHelpers.ThrowOnResourceError(_gl, $"capture backbuffer {width}x{height}");
- return FrameScreenshotController.FlipRows(pixels, width, height);
- }
-
- public void WaitIdle()
- {
- ThrowIfDisposed();
- _frameFlights.WaitForSubmittedWork();
- }
-
- public void Dispose()
- {
- if (_disposed)
- return;
- _disposed = true;
-
- // Only resources THIS device created are disposed here.
- // GpuFrameFlightController is constructor-injected and owned by
- // whoever composed this device — disposing it would be a double-
- // dispose from that owner's perspective, and every disposal below
- // routes through it as the retirement queue, so it must still be
- // alive when this method returns.
- foreach (GlGpuSampler sampler in _samplers.Values)
- sampler.Dispose();
- _samplers.Clear();
-
- _defaultTexture.Dispose();
-
- foreach (GlGpuBuffer ringBuffer in _ringBuffers)
- ringBuffer.Dispose();
-
- _textureTableBuffer.Dispose();
- _timerPool.DisposeQueries();
- }
-
- private void WriteHandle(uint slot, ulong handle)
- {
- _textureHandleTable[slot] = handle;
- _textureTableDirtySlots.Mark(slot);
- }
-
- private GpuCapabilityRecord CaptureCapabilities()
- {
- int major = _gl.GetInteger(GetPName.MajorVersion);
- int minor = _gl.GetInteger(GetPName.MinorVersion);
- bool openGl43 = major > 4 || (major == 4 && minor >= 3);
-
- _gl.GetInteger((GetPName)GlMaxShaderStorageBufferBindings, out int maxStorageBindings);
- _gl.GetInteger((GetPName)GlMaxClipDistances, out int maxClipDistances);
- _gl.GetInteger((GetPName)GlMaxSamples, out int maxSamples);
- _gl.GetInteger(GetPName.UniformBufferOffsetAlignment, out int uniformAlignment);
- _gl.GetInteger((GetPName)GlShaderStorageBufferOffsetAlignment, out int storageAlignment);
-
- bool timerQuery = major > 3 || (major == 3 && minor >= 3) || _gl.IsExtensionPresent("GL_ARB_timer_query");
-
- return new GpuCapabilityRecord
- {
- Backend = GpuBackendKind.OpenGl,
- DeviceName = _gl.GetStringS(GLEnum.Renderer),
- DriverInfo = _gl.GetStringS(GLEnum.Vendor),
- ApiVersion = $"OpenGL {_gl.GetStringS(GLEnum.Version)}",
- MaxTextureTableSlots = GpuBindingModel.TextureTableCapacity,
- MaxStorageBufferBindings = (uint)maxStorageBindings,
- MaxPushConstantBytes = (uint)GpuBindingModel.MaxPushConstantBytes,
- MinStorageBufferOffsetAlignment = (uint)storageAlignment,
- MinUniformBufferOffsetAlignment = (uint)uniformAlignment,
- MaxClipDistances = (uint)maxClipDistances,
- MaxSampleCount = (uint)maxSamples,
- SupportsMultiDrawIndirect = openGl43 || _gl.IsExtensionPresent("GL_ARB_multi_draw_indirect"),
- SupportsDrawParameters = _gl.IsExtensionPresent("GL_ARB_shader_draw_parameters"),
- SupportsTextureCompressionBc = _gl.IsExtensionPresent("GL_EXT_texture_compression_s3tc"),
- SupportsTimestampQueries = timerQuery,
- // The ring maps and unmaps per flush; it never holds a persistent
- // mapping a renderer could write into between frames, which is what
- // this capability advertises. Only Vulkan reports true.
- SupportsPersistentlyMappedRings = false,
- };
- }
-
- private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuFrame.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuFrame.cs
deleted file mode 100644
index 49a9f231..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuFrame.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// One frame's recording context on the GL backend. Ring allocations are
-/// forwarded to the device's per-slot ;
-/// passes are single-level (no nesting, matching every acdream renderer
-/// today) and enforced here.
-///
-internal sealed class GlGpuFrame : IGpuFrame
-{
- private readonly GlGpuDevice _device;
- private bool _ended;
- private GlGpuPassEncoder? _openPass;
-
- internal GlGpuFrame(GlGpuDevice device, int slotIndex, long serial)
- {
- _device = device;
- SlotIndex = slotIndex;
- Serial = serial;
- }
-
- public int SlotIndex { get; }
- public long Serial { get; }
-
- public GpuRingAllocation AllocateRing(int byteCount, GpuRingUsage usage) =>
- _device.AllocateRing(SlotIndex, byteCount, usage);
-
- public IGpuPassEncoder BeginPass(GpuPassDescription description)
- {
- ArgumentNullException.ThrowIfNull(description);
- if (_openPass is not null)
- {
- throw new InvalidOperationException(
- "A pass is already open on this frame; dispose it before beginning another.");
- }
-
- _device.BeginPass(description);
- var encoder = new GlGpuPassEncoder(_device, this, description);
- _openPass = encoder;
- return encoder;
- }
-
- internal void ClosePass(GlGpuPassEncoder encoder)
- {
- if (ReferenceEquals(_openPass, encoder))
- _openPass = null;
- }
-
- public void End()
- {
- if (_ended)
- return;
- _ended = true;
- _device.EndFrame();
- }
-
- public void Dispose() => End();
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs
deleted file mode 100644
index d2b895c6..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs
+++ /dev/null
@@ -1,316 +0,0 @@
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// Records one pass's draw work. Binding calls translate almost mechanically
-/// to GL (a storage/uniform binding is glBindBufferRange, an indexed
-/// draw is glDrawElementsInstancedBaseVertexBaseInstance, and so on);
-/// the two pieces of real logic are the render-state diff applied on
-/// / the dynamic setters, and the "flush dirty
-/// ring + texture-table bytes immediately before every draw" discipline
-/// described on .
-///
-internal sealed class GlGpuPassEncoder : IGpuPassEncoder
-{
- private readonly GlGpuDevice _device;
- private readonly GlGpuFrame _frame;
- private readonly GL _gl;
- private readonly IGlAmbientStateApi _ambientApi;
- private readonly GlAmbientCapabilityState _ambientOnEntry;
- private bool _closed;
-
- private GlGpuPipeline? _currentPipeline;
- private GpuIndexType _currentIndexType = GpuIndexType.UInt16;
- private uint _currentIndexBufferBaseOffset;
- private GpuPushConstants? _currentPushConstants;
-
- internal GlGpuPassEncoder(GlGpuDevice device, GlGpuFrame frame, GpuPassDescription pass)
- {
- _device = device;
- _frame = frame;
- _gl = device.Gl;
- Pass = pass;
-
- // Campaign V slice V4a (2026-07-27 revert postmortem, plan §7.1 rule 1):
- // capture every ambient capability a bound pipeline can change, so
- // Dispose can put it back. Every acdream renderer is still raw GL
- // until V4c/V4d, so each one assumes whatever capability state the
- // PREVIOUS renderer left behind is still there — GL_MULTISAMPLE and
- // GL_SAMPLE_ALPHA_TO_COVERAGE in particular are set once per frame by
- // quality settings and never re-asserted per draw. The first V4a
- // attempt bound a pipeline that changed this state and never restored
- // it, so the world drew without multisampling from the first UI frame
- // on. Capturing here and restoring on Dispose keeps the GL backend's
- // behaviour-preserving property true at this seam. Deleted at V4h
- // once nothing raw-GL remains.
- _ambientApi = new SilkGlAmbientStateApi(_gl);
- _ambientOnEntry = GlAmbientCapabilityState.Capture(_ambientApi);
-
- // Campaign V slice V6d. GL_MULTISAMPLE is the one piece of pass state
- // with no representation in GpuPipelineDescription, and the pass's own
- // SampleCount is the contract's answer for it: a single-sampled pass
- // does not multisample. Until now the retained UI asserted that with a
- // raw glDisable of its own — exactly the kind of state a
- // backend-neutral renderer cannot own. Quality settings enable
- // GL_MULTISAMPLE once per frame for the world, and if it leaks into the
- // UI pass every glyph's soft alpha edge becomes dithered coverage
- // instead of a clean alpha blend (the "fuzzy text" artifact). The
- // ambient capture above puts it back on Dispose, so the raw-GL world
- // renderers that follow are unaffected.
- _ambientApi.SetCapability(EnableCap.Multisample, pass.SampleCount > 1);
- }
-
- public GpuPassDescription Pass { get; }
-
- public void BindPipeline(IGpuPipeline pipeline)
- {
- ArgumentNullException.ThrowIfNull(pipeline);
- ThrowIfClosed();
- var p = (GlGpuPipeline)pipeline;
- _currentPipeline = p;
-
- GpuPipelineDescription description = p.Description;
- var desired = new GlRenderStateSnapshot(
- p.GlProgram,
- description.Blend,
- description.Depth.Test,
- description.Depth.Write,
- description.Depth.Compare,
- description.Cull,
- description.FrontFace,
- description.AlphaToCoverage,
- description.ColorWrite,
- description.StencilTest,
- description.Stencil);
- _device.ApplyRenderState(desired);
-
- _gl.BindVertexArray(p.GlVertexArray);
- GLHelpers.ThrowOnResourceError(_gl, $"bind pipeline '{description.Name}' VAO");
-
- // Campaign V slice V6d: the device's texture table is bound with the
- // pipeline, the GL analogue of the Vulkan backend binding descriptor
- // set 2 on every draw. It has to happen here rather than once per frame
- // because every raw-GL world renderer binds its OWN private handle
- // table at this same binding before its own draws, with its own slot
- // numbering; an RHI shader that read that instead would sample a
- // plausible but entirely unrelated texture. Removed at V4h with the
- // per-renderer tables.
- _gl.BindBufferBase(
- GLEnum.ShaderStorageBuffer,
- GpuBindingModel.StorageTextureTable,
- _device.TextureTableGlName);
- GLHelpers.ThrowOnResourceError(_gl, $"bind pipeline '{description.Name}' texture table");
-
- // Push constants "survive pipeline changes within a pass" per the
- // IGpuPassEncoder contract. GL uniforms are per-program state, so the
- // GL backend must explicitly re-apply the last value to the newly
- // bound program to honour that — Vulkan gets this for free from a
- // shared pipeline layout.
- if (_currentPushConstants is { } constants)
- _device.PushConstants.Apply(p.GlProgram, in constants);
- }
-
- public void BindStorageBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
- {
- ThrowIfClosed();
- var b = RequireGlBuffer(buffer);
- _gl.BindBufferRange(GLEnum.ShaderStorageBuffer, binding, b.GlName, (nint)offsetBytes, sizeBytes);
- GLHelpers.ThrowOnResourceError(_gl, $"bind storage buffer '{buffer.Name}' at binding {binding}");
- }
-
- public void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
- {
- ThrowIfClosed();
- var b = RequireGlBuffer(buffer);
- _gl.BindBufferRange(GLEnum.UniformBuffer, binding, b.GlName, (nint)offsetBytes, sizeBytes);
- GLHelpers.ThrowOnResourceError(_gl, $"bind uniform buffer '{buffer.Name}' at binding {binding}");
- }
-
- public unsafe void BindVertexBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes)
- {
- ThrowIfClosed();
- if (_currentPipeline is not { } pipeline)
- throw new InvalidOperationException("BindPipeline must be called before BindVertexBuffer.");
- var b = RequireGlBuffer(buffer);
-
- _gl.BindBuffer(GLEnum.ArrayBuffer, b.GlName);
- GpuVertexLayout layout = pipeline.Description.VertexLayout;
- // Slice V6l: only the attributes this binding actually supplies. GL has
- // no binding indirection of its own — glVertexAttribPointer records the
- // currently bound ARRAY_BUFFER per attribute — so the binding index is
- // resolved here, by filtering, rather than by the driver.
- uint stride = layout.StrideOf(binding);
- foreach (GpuVertexAttribute attribute in layout.Attributes)
- {
- if (attribute.Binding != binding)
- continue;
-
- GlVertexAttributeShape shape = GlEnumMapping.VertexShapeOf(attribute.Format);
- nint attributeOffset = (nint)(offsetBytes + attribute.OffsetBytes);
- if (shape.Integer)
- {
- // An integer shader input (uvec4) must come through the I-form.
- // Supplying it via glVertexAttribPointer leaves the value undefined.
- _gl.VertexAttribIPointer(
- attribute.Location,
- shape.ComponentCount,
- (VertexAttribIType)shape.Type,
- stride,
- (void*)attributeOffset);
- }
- else
- {
- _gl.VertexAttribPointer(
- attribute.Location,
- shape.ComponentCount,
- shape.Type,
- shape.Normalized,
- stride,
- (void*)attributeOffset);
- }
- }
- GLHelpers.ThrowOnResourceError(_gl, $"bind vertex buffer '{buffer.Name}'");
- _gl.BindBuffer(GLEnum.ArrayBuffer, 0);
- }
-
- public void BindIndexBuffer(IGpuBuffer buffer, uint offsetBytes, GpuIndexType indexType)
- {
- ThrowIfClosed();
- var b = RequireGlBuffer(buffer);
- _currentIndexType = indexType;
- _currentIndexBufferBaseOffset = offsetBytes;
- _gl.BindBuffer(GLEnum.ElementArrayBuffer, b.GlName);
- GLHelpers.ThrowOnResourceError(_gl, $"bind index buffer '{buffer.Name}'");
- }
-
- public void SetPushConstants(in GpuPushConstants constants)
- {
- ThrowIfClosed();
- _currentPushConstants = constants;
- if (_currentPipeline is { } pipeline)
- _device.PushConstants.Apply(pipeline.GlProgram, in constants);
- }
-
- public void SetViewport(int x, int y, int width, int height)
- {
- ThrowIfClosed();
- _gl.Viewport(x, y, (uint)width, (uint)height);
- }
-
- public void SetScissor(int x, int y, int width, int height)
- {
- ThrowIfClosed();
- _gl.Enable(EnableCap.ScissorTest);
- _gl.Scissor(x, y, (uint)width, (uint)height);
- }
-
- public void SetCullMode(GpuCullMode cullMode)
- {
- ThrowIfClosed();
- _device.ApplyRenderState(_device.CurrentRenderState with { Cull = cullMode });
- }
-
- public void SetFrontFace(GpuFrontFace frontFace)
- {
- ThrowIfClosed();
- _device.ApplyRenderState(_device.CurrentRenderState with { FrontFace = frontFace });
- }
-
- public void SetDepthWrite(bool enabled)
- {
- ThrowIfClosed();
- _device.ApplyRenderState(_device.CurrentRenderState with { DepthWrite = enabled });
- }
-
- public void SetStencil(in GpuStencilState stencil)
- {
- ThrowIfClosed();
- _device.ApplyRenderState(_device.CurrentRenderState with { Stencil = stencil });
- }
-
- public unsafe void DrawIndexed(uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance)
- {
- ThrowIfClosed();
- _device.FlushBeforeDraw(_frame.SlotIndex);
- int indexSize = GlEnumMapping.IndexSizeBytesOf(_currentIndexType);
- nint indexOffset = (nint)(_currentIndexBufferBaseOffset + firstIndex * (uint)indexSize);
- _gl.DrawElementsInstancedBaseVertexBaseInstance(
- GlEnumMapping.PrimitiveTypeOf(RequirePipeline().Description.Topology),
- indexCount,
- GlEnumMapping.DrawElementsTypeOf(_currentIndexType),
- (void*)indexOffset,
- instanceCount,
- vertexOffset,
- firstInstance);
- GLHelpers.ThrowOnResourceError(_gl, "DrawIndexed");
- }
-
- public void Draw(uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance)
- {
- ThrowIfClosed();
- _device.FlushBeforeDraw(_frame.SlotIndex);
- _gl.DrawArraysInstancedBaseInstance(
- (GLEnum)GlEnumMapping.PrimitiveTypeOf(RequirePipeline().Description.Topology),
- (int)firstVertex,
- vertexCount,
- instanceCount,
- firstInstance);
- GLHelpers.ThrowOnResourceError(_gl, "Draw");
- }
-
- public unsafe void MultiDrawIndexedIndirect(IGpuBuffer commands, uint offsetBytes, uint drawCount, uint strideBytes)
- {
- ThrowIfClosed();
- var indirect = RequireGlBuffer(commands);
- _device.FlushBeforeDraw(_frame.SlotIndex);
- _gl.BindBuffer(GLEnum.DrawIndirectBuffer, indirect.GlName);
- _gl.MultiDrawElementsIndirect(
- GlEnumMapping.PrimitiveTypeOf(RequirePipeline().Description.Topology),
- GlEnumMapping.DrawElementsTypeOf(_currentIndexType),
- (void*)(nint)offsetBytes,
- drawCount,
- strideBytes);
- GLHelpers.ThrowOnResourceError(_gl, "MultiDrawIndexedIndirect");
- _gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
- }
-
- public IDisposable BeginTimerScope(string scopeName) => _device.TimerPool.BeginScope(scopeName);
-
- public void Dispose()
- {
- if (_closed)
- return;
- _closed = true;
- // GL has no store-op work to do here: GpuStoreOp.Resolve was already
- // rejected at BeginPass (V1 targets are single-sampled), and
- // Store/DontCare need no explicit action — the framebuffer's contents
- // simply persist until the next pass rebinds a target.
- //
- // Restore whatever capability state was ambient before this pass
- // opened (see the constructor's comment) so a still-raw-GL renderer
- // running immediately after this pass sees exactly what it would have
- // seen had this pass never bound a pipeline.
- _ambientOnEntry.Restore(_ambientApi);
- _frame.ClosePass(this);
- }
-
- private GlGpuPipeline RequirePipeline() =>
- _currentPipeline ?? throw new InvalidOperationException("BindPipeline must be called before drawing.");
-
- private static GlGpuBuffer RequireGlBuffer(IGpuBuffer buffer)
- {
- ArgumentNullException.ThrowIfNull(buffer);
- if (buffer is not GlGpuBuffer glBuffer)
- throw new ArgumentException("The GL backend can only bind GL buffers.", nameof(buffer));
- return glBuffer;
- }
-
- private void ThrowIfClosed()
- {
- if (_closed)
- throw new ObjectDisposedException(nameof(GlGpuPassEncoder));
- }
-}
-
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPipeline.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPipeline.cs
deleted file mode 100644
index 60106a0e..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPipeline.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using AcDream.App.Rendering;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// Compiles through the existing
-/// (the same compiler every other GL
-/// shader in the codebase uses — this is not a second compiler) and owns one
-/// VAO shaped by .
-///
-/// The VAO only records which attribute locations are enabled and their
-/// component shape; it does NOT bind a vertex buffer at creation time. Vertex
-/// attribute pointers are re-issued by
-/// every time a buffer/offset is bound, because a ring allocation's offset
-/// changes every frame — GL has no notion of "rebase this VAO's buffer at a
-/// new offset" independent of re-issuing glVertexAttribPointer.
-///
-internal sealed class GlGpuPipeline : IGpuPipeline
-{
- private readonly GL _gl;
- private readonly IGpuResourceRetirementQueue _retirement;
- private uint _program;
- private uint _vertexArray;
-
- public GlGpuPipeline(GL gl, IGpuResourceRetirementQueue retirement, GpuPipelineDescription description, string vertexSource, string fragmentSource)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
- Description = description ?? throw new ArgumentNullException(nameof(description));
-
- _program = ShaderProgramConstruction.Build(new GlShaderProgramBuildApi(_gl), vertexSource, fragmentSource);
- try
- {
- _vertexArray = GlResourceCommand.CreateName(
- _gl,
- $"pipeline '{description.Name}' VAO",
- _gl.GenVertexArray,
- _gl.DeleteVertexArray);
- _gl.BindVertexArray(_vertexArray);
- GpuVertexLayout layout = description.VertexLayout;
- foreach (GpuVertexAttribute attribute in layout.Attributes)
- {
- _gl.EnableVertexAttribArray(attribute.Location);
- // Slice V6l: the divisor is VAO state and survives every later
- // glVertexAttribPointer, so it belongs here with the enables
- // rather than in the per-frame rebind. Zero is the GL default and
- // is restated explicitly — a VAO name can be recycled by the
- // driver, and inheriting a stale divisor draws one instance's
- // data across every vertex.
- _gl.VertexAttribDivisor(
- attribute.Location,
- layout.InputRateOf(attribute.Binding) == GpuVertexInputRate.Instance ? 1u : 0u);
- }
- GLHelpers.ThrowOnResourceError(_gl, $"configure pipeline '{description.Name}' VAO");
- _gl.BindVertexArray(0);
- }
- catch
- {
- _gl.DeleteProgram(_program);
- _program = 0;
- throw;
- }
- }
-
- public GpuPipelineDescription Description { get; }
-
- internal uint GlProgram => _program;
- internal uint GlVertexArray => _vertexArray;
-
- public void Dispose()
- {
- uint program = _program;
- uint vertexArray = _vertexArray;
- if (program == 0 && vertexArray == 0)
- return;
- _program = 0;
- _vertexArray = 0;
-
- GL gl = _gl;
- string label = Description.Name;
- _retirement.Retire(() =>
- {
- if (vertexArray != 0)
- {
- gl.DeleteVertexArray(vertexArray);
- GLHelpers.ThrowOnResourceError(gl, $"delete pipeline '{label}' VAO");
- }
- if (program != 0)
- {
- gl.DeleteProgram(program);
- GLHelpers.ThrowOnResourceError(gl, $"delete pipeline '{label}' program");
- }
- });
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPushConstantBinder.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPushConstantBinder.cs
deleted file mode 100644
index 74713463..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPushConstantBinder.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System.Numerics;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// Applies a value to the currently bound GL
-/// program by uniform name (),
-/// caching each program's resolved locations so the name lookup only happens
-/// once per program. A location of -1 (the program does not declare that
-/// uniform) is silently skipped, exactly as the contract's XML docs specify.
-///
-internal sealed class GlGpuPushConstantBinder
-{
- private readonly GL _gl;
- private readonly Dictionary _locationsByProgram = new();
-
- public GlGpuPushConstantBinder(GL gl) => _gl = gl ?? throw new ArgumentNullException(nameof(gl));
-
- public unsafe void Apply(uint program, in GpuPushConstants constants)
- {
- ProgramLocations locations = GetOrResolveLocations(program);
-
- if (locations.ViewProjection >= 0)
- {
- Matrix4x4 m = constants.ViewProjection;
- _gl.UniformMatrix4(locations.ViewProjection, 1, false, (float*)&m);
- }
- if (locations.DrawIdOffset >= 0)
- _gl.Uniform1(locations.DrawIdOffset, constants.DrawIdOffset);
- if (locations.LightingMode >= 0)
- _gl.Uniform1(locations.LightingMode, constants.LightingMode);
- if (locations.RenderPass >= 0)
- _gl.Uniform1(locations.RenderPass, constants.RenderPass);
- if (locations.LightDebug >= 0)
- _gl.Uniform1(locations.LightDebug, constants.LightDebug);
- if (locations.TextureIndexA >= 0)
- _gl.Uniform1(locations.TextureIndexA, constants.TextureIndexA);
- if (locations.TextureIndexB >= 0)
- _gl.Uniform1(locations.TextureIndexB, constants.TextureIndexB);
- if (locations.ParamA >= 0)
- _gl.Uniform1(locations.ParamA, constants.ParamA);
- if (locations.ParamB >= 0)
- _gl.Uniform1(locations.ParamB, constants.ParamB);
- GLHelpers.ThrowOnResourceError(_gl, $"apply push constants to program {program}");
- }
-
- /// Drops cached locations for a deleted program. Call before its GL name is reused.
- public void Forget(uint program) => _locationsByProgram.Remove(program);
-
- private ProgramLocations GetOrResolveLocations(uint program)
- {
- if (_locationsByProgram.TryGetValue(program, out ProgramLocations existing))
- return existing;
-
- ProgramLocations locations = new(
- _gl.GetUniformLocation(program, GlPushConstantUniformNames.ViewProjection),
- _gl.GetUniformLocation(program, GlPushConstantUniformNames.DrawIdOffset),
- _gl.GetUniformLocation(program, GlPushConstantUniformNames.LightingMode),
- _gl.GetUniformLocation(program, GlPushConstantUniformNames.RenderPass),
- _gl.GetUniformLocation(program, GlPushConstantUniformNames.LightDebug),
- _gl.GetUniformLocation(program, GlPushConstantUniformNames.TextureIndexA),
- _gl.GetUniformLocation(program, GlPushConstantUniformNames.TextureIndexB),
- _gl.GetUniformLocation(program, GlPushConstantUniformNames.ParamA),
- _gl.GetUniformLocation(program, GlPushConstantUniformNames.ParamB));
- _locationsByProgram[program] = locations;
- return locations;
- }
-
- private readonly record struct ProgramLocations(
- int ViewProjection,
- int DrawIdOffset,
- int LightingMode,
- int RenderPass,
- int LightDebug,
- int TextureIndexA,
- int TextureIndexB,
- int ParamA,
- int ParamB);
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuRenderTarget.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuRenderTarget.cs
deleted file mode 100644
index 329374d3..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuRenderTarget.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-using AcDream.App.Rendering;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// An offscreen colour(+depth) FBO. The colour attachment is a
-/// so it can be registered into the texture table
-/// after the pass; the depth/stencil attachment (when requested) is a plain
-/// renderbuffer, mirroring ManagedGLFramebuffer's approach — nothing
-/// ever samples depth for the targets this slice's contract describes
-/// (paperdoll, creature appraisal, portal masking).
-///
-internal sealed class GlGpuRenderTarget : IGpuRenderTarget
-{
- private readonly GL _gl;
- private readonly IGpuResourceRetirementQueue _retirement;
- private uint _framebuffer;
- private uint _depthStencilRenderbuffer;
- private GlGpuTexture? _color;
-
- public GlGpuRenderTarget(GL gl, IGpuResourceRetirementQueue retirement, GpuRenderTargetDescription description)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
- Description = description;
- if (description.SampleCount != 1)
- {
- throw new NotSupportedException(
- "GL render targets are single-sampled in Campaign V slice V1; " +
- "MSAA offscreen targets are not part of this contract.");
- }
-
- _color = new GlGpuTexture(
- _gl,
- _retirement,
- new GpuTextureDescription(
- $"{description.Name}-color",
- GpuTextureKind.Texture2D,
- description.ColorFormat,
- description.Width,
- description.Height,
- LayerCount: 1,
- MipLevelCount: 1));
-
- _framebuffer = GlResourceCommand.CreateName(_gl, $"framebuffer '{description.Name}'", _gl.GenFramebuffer, _gl.DeleteFramebuffer);
- _gl.BindFramebuffer(GLEnum.Framebuffer, _framebuffer);
- _gl.FramebufferTexture2D(
- GLEnum.Framebuffer,
- GLEnum.ColorAttachment0,
- GLEnum.Texture2D,
- _color.GlName,
- 0);
-
- if (description.DepthFormat is not null)
- {
- _depthStencilRenderbuffer = GlResourceCommand.CreateName(
- _gl,
- $"depth renderbuffer '{description.Name}'",
- _gl.GenRenderbuffer,
- _gl.DeleteRenderbuffer);
- _gl.BindRenderbuffer(GLEnum.Renderbuffer, _depthStencilRenderbuffer);
- _gl.RenderbufferStorage(GLEnum.Renderbuffer, GLEnum.Depth24Stencil8, (uint)description.Width, (uint)description.Height);
- _gl.FramebufferRenderbuffer(
- GLEnum.Framebuffer,
- GLEnum.DepthStencilAttachment,
- GLEnum.Renderbuffer,
- _depthStencilRenderbuffer);
- }
-
- GLEnum status = _gl.CheckFramebufferStatus(GLEnum.Framebuffer);
- _gl.BindFramebuffer(GLEnum.Framebuffer, 0);
- if (status != GLEnum.FramebufferComplete)
- {
- // Roll back the two names this constructor already owns before
- // surfacing the failure — nothing has been published yet.
- _gl.DeleteFramebuffer(_framebuffer);
- if (_depthStencilRenderbuffer != 0)
- _gl.DeleteRenderbuffer(_depthStencilRenderbuffer);
- _color.Dispose();
- throw new InvalidOperationException(
- $"Render target '{description.Name}' framebuffer is incomplete: {status}.");
- }
- }
-
- public GpuRenderTargetDescription Description { get; }
-
- public IGpuTexture ColorTexture => _color ?? throw new ObjectDisposedException(Description.Name);
-
- internal uint GlFramebufferName => _framebuffer;
-
- public void Dispose()
- {
- uint framebuffer = _framebuffer;
- uint renderbuffer = _depthStencilRenderbuffer;
- GlGpuTexture? color = _color;
- if (framebuffer == 0 && color is null)
- return;
- _framebuffer = 0;
- _depthStencilRenderbuffer = 0;
- _color = null;
-
- GL gl = _gl;
- string label = Description.Name;
- _retirement.Retire(() =>
- {
- if (framebuffer != 0)
- {
- gl.DeleteFramebuffer(framebuffer);
- GLHelpers.ThrowOnResourceError(gl, $"delete framebuffer '{label}'");
- }
- if (renderbuffer != 0)
- {
- gl.DeleteRenderbuffer(renderbuffer);
- GLHelpers.ThrowOnResourceError(gl, $"delete depth renderbuffer '{label}'");
- }
- });
- color?.Dispose();
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuSampler.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuSampler.cs
deleted file mode 100644
index e2d8421f..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuSampler.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using AcDream.App.Rendering;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// One GL sampler object.
-/// de-duplicates by value, so a given
-/// wrap/filter combination is only ever backed by one physical sampler name —
-/// the same pattern SamplerCache already uses for its two fixed
-/// samplers, generalized to the full description space the RHI exposes.
-///
-internal sealed class GlGpuSampler : IGpuSampler
-{
- private const uint GlTextureMaxAnisotropy = 0x84FE;
-
- private readonly GL _gl;
- private readonly IGpuResourceRetirementQueue _retirement;
- private uint _name;
-
- public GlGpuSampler(GL gl, IGpuResourceRetirementQueue retirement, GpuSamplerDescription description)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
- Description = description;
-
- _name = GlResourceCommand.CreateName(_gl, "sampler", _gl.GenSampler, _gl.DeleteSampler);
- TextureMinFilter minFilter = GlEnumMapping.MinFilterOf(description.MinFilter, description.MipFilter);
- TextureMagFilter magFilter = GlEnumMapping.MagFilterOf(description.MagFilter);
- TextureWrapMode wrapU = GlEnumMapping.WrapModeOf(description.AddressU);
- TextureWrapMode wrapV = GlEnumMapping.WrapModeOf(description.AddressV);
-
- _gl.SamplerParameter(_name, SamplerParameterI.MinFilter, (int)minFilter);
- _gl.SamplerParameter(_name, SamplerParameterI.MagFilter, (int)magFilter);
- _gl.SamplerParameter(_name, SamplerParameterI.WrapS, (int)wrapU);
- _gl.SamplerParameter(_name, SamplerParameterI.WrapT, (int)wrapV);
- if (description.MaxAnisotropy > 1f)
- _gl.SamplerParameter(_name, (SamplerParameterF)GlTextureMaxAnisotropy, description.MaxAnisotropy);
- GLHelpers.ThrowOnResourceError(_gl, $"configure sampler {_name}");
- }
-
- public GpuSamplerDescription Description { get; }
-
- internal uint GlName => _name;
-
- public void Dispose()
- {
- uint name = _name;
- if (name == 0)
- return;
- _name = 0;
-
- GL gl = _gl;
- _retirement.Retire(() =>
- {
- gl.DeleteSampler(name);
- GLHelpers.ThrowOnResourceError(gl, $"delete sampler {name}");
- });
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTexture.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTexture.cs
deleted file mode 100644
index f4f96e3d..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTexture.cs
+++ /dev/null
@@ -1,202 +0,0 @@
-using AcDream.App.Rendering;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// A GL texture (2D or 2D array) allocated with immutable storage
-/// (glTexStorage2D/3D) so every mip level is defined the moment the
-/// object is created, regardless of upload order — mirroring
-/// ManagedGLTextureArray's allocation strategy without inheriting its
-/// Chorizite/ coupling.
-///
-/// The texture's own min/mag filter is set at creation (not left at GL
-/// defaults) purely so the texture is "complete" for sampling the instant it
-/// exists — completeness is judged from the texture object's own filter
-/// state, independent of whatever a draw later
-/// binds. The bound sampler object overrides actual filtering at draw time
-/// (same override rule SamplerCache already documents), so this
-/// default never affects the rendered image.
-///
-internal sealed class GlGpuTexture : IGpuTexture
-{
- private readonly GL _gl;
- private readonly IGpuResourceRetirementQueue _retirement;
- private readonly GlTextureFormatInfo _formatInfo;
- private readonly GLEnum _target;
- private uint _name;
-
- public GlGpuTexture(GL gl, IGpuResourceRetirementQueue retirement, GpuTextureDescription description)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
- Name = description.Name;
- Kind = description.Kind;
- Format = description.Format;
- Width = description.Width;
- Height = description.Height;
- LayerCount = description.LayerCount;
- MipLevelCount = description.MipLevelCount;
- _formatInfo = GlGpuTextureFormatMapping.Resolve(Format);
- _target = Kind == GpuTextureKind.Texture2D ? GLEnum.Texture2D : GLEnum.Texture2DArray;
-
- _name = GlResourceCommand.CreateName(_gl, $"texture '{Name}'", _gl.GenTexture, _gl.DeleteTexture);
- _gl.BindTexture(_target, _name);
- if (Kind == GpuTextureKind.Texture2D)
- {
- _gl.TexStorage2D(_target, (uint)MipLevelCount, _formatInfo.SizedInternalFormat, (uint)Width, (uint)Height);
- }
- else
- {
- _gl.TexStorage3D(
- _target,
- (uint)MipLevelCount,
- _formatInfo.SizedInternalFormat,
- (uint)Width,
- (uint)Height,
- (uint)LayerCount);
- }
- GLHelpers.ThrowOnResourceError(
- _gl,
- $"allocate texture '{Name}' {Format} {Width}x{Height}x{LayerCount} ({MipLevelCount} mips)");
-
- TextureMinFilter minFilter = MipLevelCount > 1 ? TextureMinFilter.LinearMipmapLinear : TextureMinFilter.Linear;
- _gl.TexParameter(_target, TextureParameterName.TextureMinFilter, (int)minFilter);
- _gl.TexParameter(_target, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- GLHelpers.ThrowOnResourceError(_gl, $"set default filter for texture '{Name}'");
- _gl.BindTexture(_target, 0);
- }
-
- public string Name { get; }
- public GpuTextureKind Kind { get; }
- public GpuTextureFormat Format { get; }
- public int Width { get; }
- public int Height { get; }
- public int LayerCount { get; }
- public int MipLevelCount { get; }
-
- /// The physical GL texture name. Used by to obtain a bindless handle.
- internal uint GlName => _name;
-
- internal GLEnum Target => _target;
-
- public void Upload(int mipLevel, int layer, ReadOnlySpan data)
- {
- ThrowIfDisposed();
- if (Format == GpuTextureFormat.Depth24Stencil8)
- {
- throw new NotSupportedException(
- "Depth24Stencil8 textures are attachment-only; they are never uploaded from the CPU.");
- }
- if ((uint)mipLevel >= (uint)MipLevelCount)
- throw new ArgumentOutOfRangeException(nameof(mipLevel));
- if (Kind == GpuTextureKind.Texture2D && layer != 0)
- throw new ArgumentOutOfRangeException(nameof(layer), "A 2D texture only has layer 0.");
- if (Kind == GpuTextureKind.Texture2DArray && (uint)layer >= (uint)LayerCount)
- throw new ArgumentOutOfRangeException(nameof(layer));
-
- int mipWidth = Math.Max(1, Width >> mipLevel);
- int mipHeight = Math.Max(1, Height >> mipLevel);
- long expected = _formatInfo.LayerByteCount(mipWidth, mipHeight);
- if (data.Length != expected)
- {
- throw new ArgumentException(
- $"Upload for texture '{Name}' mip {mipLevel} has {data.Length} bytes; expected {expected} " +
- $"for {Format} {mipWidth}x{mipHeight}.",
- nameof(data));
- }
-
- _gl.BindTexture(_target, _name);
- _gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
- unsafe
- {
- fixed (byte* pointer = data)
- {
- if (Kind == GpuTextureKind.Texture2D)
- UploadTexture2D(mipLevel, mipWidth, mipHeight, data.Length, pointer);
- else
- UploadTexture2DArray(mipLevel, layer, mipWidth, mipHeight, data.Length, pointer);
- }
- }
- GLHelpers.ThrowOnResourceError(_gl, $"upload texture '{Name}' mip {mipLevel} layer {layer}");
- _gl.BindTexture(_target, 0);
- }
-
- private unsafe void UploadTexture2D(int mipLevel, int mipWidth, int mipHeight, int byteCount, byte* pointer)
- {
- if (_formatInfo.IsCompressed)
- {
- _gl.CompressedTexSubImage2D(
- _target, mipLevel, 0, 0, (uint)mipWidth, (uint)mipHeight,
- (InternalFormat)_formatInfo.SizedInternalFormat, (uint)byteCount, pointer);
- }
- else
- {
- _gl.TexSubImage2D(
- _target, mipLevel, 0, 0, (uint)mipWidth, (uint)mipHeight,
- _formatInfo.UploadPixelFormat, _formatInfo.UploadPixelType, pointer);
- }
- }
-
- private unsafe void UploadTexture2DArray(int mipLevel, int layer, int mipWidth, int mipHeight, int byteCount, byte* pointer)
- {
- if (_formatInfo.IsCompressed)
- {
- _gl.CompressedTexSubImage3D(
- _target, mipLevel, 0, 0, layer, (uint)mipWidth, (uint)mipHeight, 1,
- (InternalFormat)_formatInfo.SizedInternalFormat, (uint)byteCount, pointer);
- }
- else
- {
- _gl.TexSubImage3D(
- _target, mipLevel, 0, 0, layer, (uint)mipWidth, (uint)mipHeight, 1,
- _formatInfo.UploadPixelFormat, _formatInfo.UploadPixelType, pointer);
- }
- }
-
- public void GenerateMipChain()
- {
- ThrowIfDisposed();
- if (MipLevelCount <= 1)
- return;
-
- // Matches ManagedGLTextureArray.ProcessDirtyUpdatesInternal: GL's
- // driver-defined compressed-mip regeneration is the one behaviour this
- // migration deliberately does not carry into the RHI contract (the
- // Vulkan backend cannot blit BC images at all and requires a CPU-built
- // chain instead). Callers that need BC mips must supply every level
- // through Upload directly; this is a documented no-op for them on GL,
- // not a silent gap, since the GL renderer path today already skips
- // glGenerateMipmap for compressed arrays.
- if (_formatInfo.IsCompressed)
- return;
-
- _gl.BindTexture(_target, _name);
- _gl.GenerateMipmap(_target);
- GLHelpers.ThrowOnResourceError(_gl, $"generate mip chain for texture '{Name}'");
- _gl.BindTexture(_target, 0);
- }
-
- public void Dispose()
- {
- uint name = _name;
- if (name == 0)
- return;
- _name = 0;
-
- GL gl = _gl;
- string label = Name;
- _retirement.Retire(() =>
- {
- gl.DeleteTexture(name);
- GLHelpers.ThrowOnResourceError(gl, $"delete texture '{label}' ({name})");
- });
- }
-
- private void ThrowIfDisposed()
- {
- if (_name == 0)
- throw new ObjectDisposedException(Name);
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTextureFormatMapping.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTextureFormatMapping.cs
deleted file mode 100644
index 8c4aeb47..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTextureFormatMapping.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// GL format triple for a : the sized internal
-/// format used at allocation (glTexStorage2D/3D), the upload
-/// format/type for uncompressed uploads, and whether uploads go through
-/// glCompressedTexSubImage2D/3D instead.
-///
-/// is acdream's own enum, not the Chorizite
-/// TextureFormat the existing TextureFormatExtensions targets,
-/// so this is the "otherwise add a private mapper" fallback the campaign spec
-/// calls for rather than an extension of that existing type. Pure data —
-/// safe to unit test without a live GL context, since Silk's GL enums are
-/// plain value types.
-///
-internal readonly record struct GlTextureFormatInfo(
- SizedInternalFormat SizedInternalFormat,
- PixelFormat UploadPixelFormat,
- PixelType UploadPixelType,
- bool IsCompressed,
- int BlockOrTexelBytes,
- int BlockDimension)
-{
- /// Bytes required for one full, uncompressed mip layer at the given dimensions, or one compressed layer rounded up to whole blocks.
- public long LayerByteCount(int width, int height)
- {
- if (!IsCompressed)
- return (long)width * height * BlockOrTexelBytes;
-
- int blocksWide = (width + BlockDimension - 1) / BlockDimension;
- int blocksHigh = (height + BlockDimension - 1) / BlockDimension;
- return (long)blocksWide * blocksHigh * BlockOrTexelBytes;
- }
-}
-
-internal static class GlGpuTextureFormatMapping
-{
- public static GlTextureFormatInfo Resolve(GpuTextureFormat format) => format switch
- {
- GpuTextureFormat.Rgba8Unorm or GpuTextureFormat.Rgba8UnormRenderTarget =>
- new GlTextureFormatInfo(SizedInternalFormat.Rgba8, PixelFormat.Rgba, PixelType.UnsignedByte, false, 4, 1),
- GpuTextureFormat.R8Unorm =>
- new GlTextureFormatInfo(SizedInternalFormat.R8, PixelFormat.Red, PixelType.UnsignedByte, false, 1, 1),
- GpuTextureFormat.Bc1Unorm =>
- new GlTextureFormatInfo(SizedInternalFormat.CompressedRgbaS3TCDxt1Ext, PixelFormat.Rgba, PixelType.UnsignedByte, true, 8, 4),
- GpuTextureFormat.Bc2Unorm =>
- new GlTextureFormatInfo(SizedInternalFormat.CompressedRgbaS3TCDxt3Ext, PixelFormat.Rgba, PixelType.UnsignedByte, true, 16, 4),
- GpuTextureFormat.Bc3Unorm =>
- new GlTextureFormatInfo(SizedInternalFormat.CompressedRgbaS3TCDxt5Ext, PixelFormat.Rgba, PixelType.UnsignedByte, true, 16, 4),
- GpuTextureFormat.Depth24Stencil8 =>
- new GlTextureFormatInfo(SizedInternalFormat.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248, false, 4, 1),
- _ => throw new NotSupportedException($"No GL format mapping for {format}."),
- };
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTimerPool.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTimerPool.cs
deleted file mode 100644
index a1c015c6..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTimerPool.cs
+++ /dev/null
@@ -1,186 +0,0 @@
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// Seam over the four GL calls a TimeElapsed timer scope needs, mirroring
-/// IGpuFenceApi's role for : it lets
-/// 's double-buffering and result-promotion logic run
-/// under a unit test with no live GL context.
-///
-internal interface IGlTimerQueryApi
-{
- uint CreateQuery();
- void DeleteQuery(uint query);
- void Begin(uint query);
- void End();
-
- /// Non-blocking: false when the result for is not yet available (or the query was never begun).
- bool TryGetResult(uint query, out double milliseconds);
-}
-
-internal sealed class SilkGlTimerQueryApi(GL gl) : IGlTimerQueryApi
-{
- private readonly GL _gl = gl ?? throw new ArgumentNullException(nameof(gl));
-
- public uint CreateQuery() => _gl.GenQuery();
-
- public void DeleteQuery(uint query) => _gl.DeleteQuery(query);
-
- public void Begin(uint query) => _gl.BeginQuery(QueryTarget.TimeElapsed, query);
-
- public void End() => _gl.EndQuery(QueryTarget.TimeElapsed);
-
- public bool TryGetResult(uint query, out double milliseconds)
- {
- _gl.GetQueryObject(query, QueryObjectParameterName.ResultAvailable, out int available);
- if (available == 0)
- {
- milliseconds = 0;
- return false;
- }
-
- _gl.GetQueryObject(query, QueryObjectParameterName.Result, out ulong elapsedNanoseconds);
- milliseconds = elapsedNanoseconds / 1_000_000d;
- return true;
- }
-}
-
-///
-/// backed by TimeElapsed queries. Core GL
-/// only allows one GL_TIME_ELAPSED query active at a time (the
-/// restriction is per target, not per query object), so scopes must not
-/// nest — throws if a previous scope in the same
-/// pass hasn't been disposed yet, exactly as WbDrawDispatcher already
-/// requires for its own opaque/transparent query pair.
-///
-/// Each distinct scope name gets its own double-buffered pair of query
-/// objects (per the campaign spec: "double-buffered so results are read from
-/// a retired frame and never block"). A scope's second call reads back the
-/// FIRST call's result non-blockingly — by the time a scope name repeats,
-/// at least one full frame has usually retired, so the result is normally
-/// ready; if it is not, the previous value is kept until it is.
-///
-internal sealed class GlGpuTimerPool : IGpuTimerPool
-{
- private const int BufferDepth = 2;
-
- private sealed class ScopeState
- {
- public readonly uint[] Queries = new uint[BufferDepth];
- public readonly bool[] Began = new bool[BufferDepth];
- public int NextSlot;
- public double LastResolvedMilliseconds;
- public bool HasResolvedValue;
- }
-
- private readonly IGlTimerQueryApi _api;
- private readonly Dictionary _scopes = new(StringComparer.Ordinal);
- private string? _activeScopeName;
-
- public GlGpuTimerPool(IGlTimerQueryApi api, bool isSupported)
- {
- _api = api ?? throw new ArgumentNullException(nameof(api));
- IsSupported = isSupported;
- }
-
- public bool IsSupported { get; }
-
- ///
- /// Begins (or continues) the named scope. Returns a disposable that ends
- /// the query; dispose it before beginning another scope in the same pass.
- ///
- public IDisposable BeginScope(string scopeName)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(scopeName);
- if (!IsSupported)
- return NullTimerScope.Instance;
-
- if (_activeScopeName is not null)
- {
- throw new InvalidOperationException(
- $"GPU timer scope '{_activeScopeName}' is still active; TimeElapsed queries " +
- "cannot nest. Dispose the previous scope before beginning another.");
- }
-
- if (!_scopes.TryGetValue(scopeName, out ScopeState? state))
- {
- state = new ScopeState();
- for (int i = 0; i < BufferDepth; i++)
- state.Queries[i] = _api.CreateQuery();
- _scopes.Add(scopeName, state);
- }
-
- int slot = state.NextSlot;
- state.NextSlot = (slot + 1) % BufferDepth;
-
- // Poll the OTHER slot's query — the one begun on the previous call to
- // this scope name — before beginning this one. That is what makes a
- // scope's Nth call read back the (N-1)th call's result: with only two
- // slots, "the previous slot" and "the other slot" are the same index,
- // and by now it has usually had at least one frame to retire.
- int previousSlot = (slot + BufferDepth - 1) % BufferDepth;
- if (state.Began[previousSlot] && _api.TryGetResult(state.Queries[previousSlot], out double milliseconds))
- {
- state.LastResolvedMilliseconds = milliseconds;
- state.HasResolvedValue = true;
- }
-
- _api.Begin(state.Queries[slot]);
- state.Began[slot] = true;
- _activeScopeName = scopeName;
- return new ActiveTimerScope(this, scopeName);
- }
-
- public bool TryResolve(string scopeName, out double milliseconds)
- {
- if (_scopes.TryGetValue(scopeName, out ScopeState? state) && state.HasResolvedValue)
- {
- milliseconds = state.LastResolvedMilliseconds;
- return true;
- }
-
- milliseconds = 0;
- return false;
- }
-
- private void EndScope(string scopeName)
- {
- if (_activeScopeName != scopeName)
- return;
-
- _api.End();
- _activeScopeName = null;
- }
-
- internal void DisposeQueries()
- {
- foreach (ScopeState state in _scopes.Values)
- {
- foreach (uint query in state.Queries)
- _api.DeleteQuery(query);
- }
- _scopes.Clear();
- }
-
- private sealed class ActiveTimerScope(GlGpuTimerPool pool, string scopeName) : IDisposable
- {
- private bool _disposed;
-
- public void Dispose()
- {
- if (_disposed)
- return;
- _disposed = true;
- pool.EndScope(scopeName);
- }
- }
-
- private sealed class NullTimerScope : IDisposable
- {
- public static NullTimerScope Instance { get; } = new();
- public void Dispose()
- {
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlPushConstantUniformNames.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlPushConstantUniformNames.cs
deleted file mode 100644
index c61637a0..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlPushConstantUniformNames.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using System.Reflection;
-
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// The single source of truth mapping each
-/// field to the GLSL uniform name the GL backend binds it to — the names
-/// documented on the fields themselves.
-/// reads these constants (not string literals of its own) when it calls
-/// GL.GetUniformLocation, and lets a
-/// test prove the table cannot silently drop a field that a later slice adds
-/// to the shared struct.
-///
-internal static class GlPushConstantUniformNames
-{
- public const string ViewProjection = "uViewProjection";
- public const string DrawIdOffset = "uDrawIDOffset";
- public const string LightingMode = "uLightingMode";
- public const string RenderPass = "uRenderPass";
- public const string LightDebug = "uLightDebug";
- public const string TextureIndexA = "uTextureIndexA";
- public const string TextureIndexB = "uTextureIndexB";
- public const string ParamA = "uParamA";
- public const string ParamB = "uParamB";
-
- ///
- /// Field name (as declared on ) to GLSL
- /// uniform name, built from the same constants the binder applies with —
- /// so the binder and this completeness table can never disagree.
- ///
- public static IReadOnlyDictionary ByFieldName { get; } =
- new Dictionary
- {
- [nameof(GpuPushConstants.ViewProjection)] = ViewProjection,
- [nameof(GpuPushConstants.DrawIdOffset)] = DrawIdOffset,
- [nameof(GpuPushConstants.LightingMode)] = LightingMode,
- [nameof(GpuPushConstants.RenderPass)] = RenderPass,
- [nameof(GpuPushConstants.LightDebug)] = LightDebug,
- [nameof(GpuPushConstants.TextureIndexA)] = TextureIndexA,
- [nameof(GpuPushConstants.TextureIndexB)] = TextureIndexB,
- [nameof(GpuPushConstants.ParamA)] = ParamA,
- [nameof(GpuPushConstants.ParamB)] = ParamB,
- };
-
- ///
- /// Throws if declares a public instance
- /// field this table does not map — the drift guard the campaign spec asks
- /// for. Called from a unit test, not from production code.
- ///
- internal static void AssertMapsEveryField()
- {
- FieldInfo[] fields = typeof(GpuPushConstants).GetFields(
- BindingFlags.Public | BindingFlags.Instance);
- foreach (FieldInfo field in fields)
- {
- if (!ByFieldName.ContainsKey(field.Name))
- {
- throw new InvalidOperationException(
- $"GpuPushConstants.{field.Name} has no mapped GLSL uniform name in " +
- $"{nameof(GlPushConstantUniformNames)}.");
- }
- }
-
- if (ByFieldName.Count != fields.Length)
- {
- throw new InvalidOperationException(
- $"{nameof(GlPushConstantUniformNames)} maps {ByFieldName.Count} names but " +
- $"{nameof(GpuPushConstants)} declares {fields.Length} fields — a stale entry " +
- "survives a field rename or removal.");
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs
deleted file mode 100644
index ae458909..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// Everything bakes plus the handful of fields
-/// core Vulkan 1.3 (and this backend) makes dynamic per draw.
-/// is the GL program name, included so a pipeline
-/// switch is itself a tracked dimension.
-///
-internal readonly record struct GlRenderStateSnapshot(
- uint Program,
- GpuBlendMode Blend,
- bool DepthTest,
- bool DepthWrite,
- GpuCompareOp DepthCompare,
- GpuCullMode Cull,
- GpuFrontFace FrontFace,
- bool AlphaToCoverage,
- bool ColorWrite,
- bool StencilTest,
- GpuStencilState Stencil);
-
-/// Which GL state calls are needed to move from the previous snapshot to the new one.
-internal readonly record struct GlRenderStateChanges(
- bool Program,
- bool Blend,
- bool DepthTest,
- bool DepthWrite,
- bool DepthCompare,
- bool Cull,
- bool FrontFace,
- bool AlphaToCoverage,
- bool ColorWrite,
- bool StencilTest,
- bool Stencil)
-{
- public bool AnyChange =>
- Program || Blend || DepthTest || DepthWrite || DepthCompare
- || Cull || FrontFace || AlphaToCoverage || ColorWrite
- || StencilTest || Stencil;
-
- /// Every dimension reported changed — used for the first apply after a reset.
- internal static GlRenderStateChanges All { get; } =
- new(true, true, true, true, true, true, true, true, true, true, true);
-}
-
-///
-/// Pure GL-free state cache: given the previously applied
-/// and a newly desired one, reports which
-/// dimensions actually changed so only issues
-/// the GL calls that matter. Starts with no baseline, so the very first
-/// call always reports every dimension changed — "when in
-/// doubt, set the state" rather than risk stale driver state from before this
-/// cache existed (e.g. from a previous pass, or default GL state that may not
-/// match a pipeline's baked defaults).
-///
-internal sealed class GlRenderStateCache
-{
- private GlRenderStateSnapshot? _last;
-
- public GlRenderStateChanges Apply(GlRenderStateSnapshot desired)
- {
- GlRenderStateSnapshot? previous = _last;
- _last = desired;
-
- if (previous is not { } p)
- return GlRenderStateChanges.All;
-
- return new GlRenderStateChanges(
- p.Program != desired.Program,
- p.Blend != desired.Blend,
- p.DepthTest != desired.DepthTest,
- p.DepthWrite != desired.DepthWrite,
- p.DepthCompare != desired.DepthCompare,
- p.Cull != desired.Cull,
- p.FrontFace != desired.FrontFace,
- p.AlphaToCoverage != desired.AlphaToCoverage,
- p.ColorWrite != desired.ColorWrite,
- p.StencilTest != desired.StencilTest,
- p.Stencil != desired.Stencil);
- }
-
- /// Discards the cached baseline — the next reports every dimension changed.
- public void Reset() => _last = null;
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlRingBufferState.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlRingBufferState.cs
deleted file mode 100644
index 1771013d..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlRingBufferState.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// Pure bookkeeping for one flight slot's upload ring: an allocation cursor
-/// that only grows across a frame, a "dirty" watermark tracking the byte range
-/// written since the last flush, and the flushed high-water mark that makes the
-/// ring's write path safe to issue unsynchronized.
-///
-/// This is deliberately GL-free so it can be unit tested without a live
-/// context. 's device owns one instance per flight slot
-/// and pairs it with a managed staging byte[] and a real GL buffer; the
-/// staging array receives every write, and
-/// immediately before each Draw/DrawIndexed/
-/// MultiDrawIndexedIndirect the device writes exactly the dirty range
-/// into the GL buffer through
-/// and calls
-/// to reset the watermark. The allocation cursor
-/// itself only resets at (once per BeginFrame) —
-/// writes made after a flush simply extend the dirty range again, to be picked
-/// up by the next flush. This is what makes "flush before every draw, not at
-/// bind time" correct: a renderer that writes after binding still gets uploaded
-/// before its draw call runs.
-///
-/// The forward-only invariant is load-bearing, not incidental. The
-/// device's per-flush write is mapped with GL_MAP_UNSYNCHRONIZED_BIT,
-/// which means the driver inserts no wait and the caller is asserting that no
-/// submitted-and-unfinished draw reads the range. Within a frame that holds
-/// because only ever hands out bytes at or above the
-/// cursor, so each flush covers a range strictly above every range already
-/// flushed. Rather than leave that as an emergent property of the arithmetic,
-/// refuses a write below :
-/// a future change that reused ring bytes mid-frame would fail loudly here
-/// instead of producing an undefined read on the GPU. Across frames the
-/// invariant belongs to GpuFrameFlightController, which waits on a
-/// slot's fence in BeginFrame before this state is .
-///
-///
-internal sealed class GlRingBufferState
-{
- private readonly int _capacityBytes;
- private uint _cursor;
- private int _dirtyStart = -1;
- private int _dirtyEnd = -1;
- private int _flushedEnd;
-
- public GlRingBufferState(int capacityBytes)
- {
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacityBytes);
- _capacityBytes = capacityBytes;
- }
-
- public int CapacityBytes => _capacityBytes;
-
- /// Bytes handed out since the last .
- public uint AllocatedBytes => _cursor;
-
- ///
- /// The exclusive end of the bytes already handed to the GPU this frame.
- /// Every subsequent write must start at or above it.
- ///
- public int FlushedEndBytes => _flushedEnd;
-
- ///
- /// Reserves bytes aligned to
- /// , returning the aligned offset.
- /// Throws — rather than truncating — when the request would exceed the
- /// slot's capacity, because a silently shortened allocation would corrupt
- /// the frame invisibly.
- ///
- public uint Allocate(int byteCount, uint alignmentBytes)
- {
- ArgumentOutOfRangeException.ThrowIfNegative(byteCount);
- uint aligned = AlignUp(_cursor, alignmentBytes);
- long end = (long)aligned + byteCount;
- if (end > _capacityBytes)
- {
- throw new InvalidOperationException(
- $"Ring allocation of {byteCount} bytes at aligned offset {aligned} " +
- $"needs {end} bytes; the ring slot is {_capacityBytes} bytes. " +
- "Increase the per-slot ring capacity (GlGpuDevice's ringCapacityBytesPerSlot).");
- }
-
- _cursor = (uint)end;
- if (byteCount > 0)
- MarkDirty((int)aligned, (int)end);
- return aligned;
- }
-
- /// Starts a new frame: rewinds the allocation cursor. Dirty state is untouched — a flush always runs before this is called.
- public void Reset()
- {
- _cursor = 0;
- _dirtyStart = -1;
- _dirtyEnd = -1;
- _flushedEnd = 0;
- }
-
- ///
- /// Returns the byte range written since the last flush (start, length),
- /// or (0, 0) when nothing is dirty, and clears the watermark while
- /// advancing over it.
- ///
- public (int Start, int Length) TakeDirtyRange()
- {
- if (_dirtyStart < 0)
- return (0, 0);
-
- (int start, int length) = (_dirtyStart, _dirtyEnd - _dirtyStart);
- _flushedEnd = Math.Max(_flushedEnd, _dirtyEnd);
- _dirtyStart = -1;
- _dirtyEnd = -1;
- return (start, length);
- }
-
- public bool HasDirtyBytes => _dirtyStart >= 0;
-
- ///
- /// Records that [start, end) was written. Internal rather than
- /// private for the same reason is: the forward-only
- /// guard below is unreachable through by
- /// construction, so proving it fires at all needs a direct call.
- ///
- internal void MarkDirty(int start, int end)
- {
- if (start < _flushedEnd)
- {
- throw new InvalidOperationException(
- $"Ring write [{start}, {end}) reaches below the {_flushedEnd} bytes already " +
- "uploaded this frame. Ring uploads are issued with GL_MAP_UNSYNCHRONIZED_BIT, " +
- "so rewriting bytes a submitted draw may still be reading is undefined — the " +
- "allocation cursor must only move forward until Reset.");
- }
-
- _dirtyStart = _dirtyStart < 0 ? start : Math.Min(_dirtyStart, start);
- _dirtyEnd = Math.Max(_dirtyEnd, end);
- }
-
- internal static uint AlignUp(uint value, uint alignmentBytes) =>
- alignmentBytes <= 1 ? value : (value + alignmentBytes - 1) / alignmentBytes * alignmentBytes;
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlTextureSlotAllocator.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlTextureSlotAllocator.cs
deleted file mode 100644
index 4db8c218..00000000
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlTextureSlotAllocator.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-namespace AcDream.App.Rendering.Gpu.Gl;
-
-///
-/// Pure bump-plus-free-list allocator for the GL texture table (the storage
-/// buffer of bindless handles at ).
-///
-/// GL-free by design: it knows nothing about bindless handles, retirement
-/// queues, or the SSBO itself. calls
-/// to get a slot to write a handle into;
-/// defers the call to
-/// through the device's IGpuResourceRetirementQueue
-/// so a freed slot is never handed back out while a submitted frame could
-/// still be reading the old handle at that index.
-///
-internal sealed class GlTextureSlotAllocator
-{
- private readonly uint _capacity;
- private readonly Stack _freeList = new();
- private uint _nextBumpSlot;
-
- public GlTextureSlotAllocator(uint capacity)
- {
- ArgumentOutOfRangeException.ThrowIfZero(capacity);
- _capacity = capacity;
- }
-
- /// Slots currently handed out (bumped or reused) and not yet released.
- public int LiveCount => (int)_nextBumpSlot - _freeList.Count;
-
- public uint Allocate()
- {
- if (_freeList.Count > 0)
- return _freeList.Pop();
-
- if (_nextBumpSlot >= _capacity)
- {
- throw new InvalidOperationException(
- $"The GL texture table is exhausted: {_capacity} slots are all live. " +
- "Every texture cache/atlas must release slots it no longer needs before " +
- "registering new ones.");
- }
-
- return _nextBumpSlot++;
- }
-
- ///
- /// Returns a slot to the free list. Callers must only call this once the
- /// retirement queue confirms no live frame could still reference the slot.
- ///
- public void Release(uint slot)
- {
- if (slot >= _nextBumpSlot)
- {
- throw new ArgumentOutOfRangeException(
- nameof(slot),
- slot,
- "Cannot release a slot that was never allocated.");
- }
-
- _freeList.Push(slot);
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/GpuEnums.cs b/src/AcDream.App/Rendering/Gpu/GpuEnums.cs
index a645786b..607d9a11 100644
--- a/src/AcDream.App/Rendering/Gpu/GpuEnums.cs
+++ b/src/AcDream.App/Rendering/Gpu/GpuEnums.cs
@@ -6,9 +6,6 @@ internal enum GpuBackendKind
/// Test double — records calls, owns no driver objects.
Recording,
- /// OpenGL 4.3 + bindless/MDI. Deleted at Campaign V slice V11.
- OpenGl,
-
/// Vulkan 1.3 core.
Vulkan,
}
diff --git a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs
index b22e7424..a808e483 100644
--- a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs
+++ b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs
@@ -1,7 +1,6 @@
using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Gpu.Gl;
using AcDream.App.UI;
using AcDream.App.World;
using AcDream.Content;
@@ -46,9 +45,9 @@ internal interface IPaperdollPoseApplicator
}
///
-/// Owns paperdoll dirty/rebuild state and the private FBO presentation edge.
-/// The GL renderer remains a borrowed resource disposed by the existing window
-/// shutdown transaction.
+/// Owns paperdoll dirty/rebuild state and the private render-target
+/// presentation edge. The renderer remains a borrowed resource disposed by
+/// the existing window shutdown transaction.
///
internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
{
diff --git a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs
index 5666bef4..03309a72 100644
--- a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs
+++ b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs
@@ -2,7 +2,6 @@ using AcDream.App.Rendering.Wb;
using AcDream.App.UI;
using AcDream.Core.Lighting;
using AcDream.Core.World;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -18,8 +17,7 @@ public sealed class PaperdollViewportRenderer :
private readonly PrivateEntityViewportRenderer _renderer;
internal PaperdollViewportRenderer(
- GL? gl,
- IWorldPassScope? scope,
+ IWorldPassScope scope,
AcDream.App.Rendering.Gpu.IGpuDevice device,
ICurrentGpuFrameSource frames,
WbDrawDispatcher dispatcher,
@@ -28,7 +26,6 @@ public sealed class PaperdollViewportRenderer :
IWbMeshAdapter meshAdapter)
{
_renderer = new PrivateEntityViewportRenderer(
- gl,
scope,
device,
frames,
diff --git a/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs b/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs
index f7a35ef6..a1ff118e 100644
--- a/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs
+++ b/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs
@@ -185,12 +185,11 @@ public sealed unsafe partial class ParticleRenderer
}
///
- /// True when mesh particles can be submitted at all. The GL arm answers with
- /// its second Shader, which is only built when a shared mesh arena
- /// exists; the RHI arm answers with the pipelines the same condition builds.
+ /// True when mesh particles can be submitted at all. The GL arm's second
+ /// Shader answer was deleted at Campaign V slice V11; only the RHI
+ /// pipelines remain, built exactly when a shared mesh arena exists.
///
- private bool MeshParticlesAvailable =>
- _glContext is null ? _meshAlphaPipeline is not null : _meshShader is not null;
+ private bool MeshParticlesAvailable => _meshAlphaPipeline is not null;
private void CreateRhiResources(IGpuDevice device, int sampleCount)
{
diff --git a/src/AcDream.App/Rendering/ParticleRenderer.cs b/src/AcDream.App/Rendering/ParticleRenderer.cs
index 28c9f38d..a8cdc074 100644
--- a/src/AcDream.App/Rendering/ParticleRenderer.cs
+++ b/src/AcDream.App/Rendering/ParticleRenderer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -97,22 +97,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
}
}
- ///
- /// The GL arm's context, or null on a backend that has none.
- ///
- /// Campaign V slice V6l: particles draw on both arms, so the context
- /// became optional. below is what every GL-arm statement
- /// still reads, unchanged — reaching it without a context is a composition
- /// error and says so rather than dereferencing null.
- ///
- private readonly GL? _glContext;
- private readonly Shader? _shader;
- private readonly Shader? _meshShader;
-
- private GL _gl => _glContext
- ?? throw new InvalidOperationException(
- "ParticleRenderer's GL arm was reached on a backend with no GL context "
- + "(campaign plan §5.5.16; the RHI arm lives in ParticleRenderer.Rhi.cs).");
private readonly TextureCache? _textures;
private readonly IDatReaderWriter? _dats;
private readonly WbMeshAdapter? _meshAdapter;
@@ -129,70 +113,15 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
private bool _disposing;
private bool _disposed;
private readonly HashSet _meshLoadRequestedThisFrame = new();
- // Campaign V slice V6e: particle_mesh.frag's two loose uniforms were renamed
- // onto members of the shared push-constant block (uTextureIndex →
- // uTextureIndexA, uTextureLayer → uParamA), because Vulkan GLSL has no
- // default uniform block to declare them in. Under GL they are still plain
- // program uniforms set exactly as before; only the names moved.
- private readonly int _meshTextureIndexLoc = -1;
- private readonly int _meshTextureLayerLoc = -1;
-
- // Campaign V slice V4t (2026-07-28): the interim per-renderer
- // GlBindlessHandleTable is retired. Both particle texture sources now hand
- // out the device's own GpuTextureSlot — TextureCache.AcquireParticleTexture
- // for billboards, ObjectRenderBatch.TextureSlot for mesh particles — so all
- // that is left here is flushing and binding that one table before each
- // raw-GL draw. There is still no automated pixel-gate coverage for
- // particles (the offline gate's fixed outdoor view has none in frame), so
- // this change is kept strictly mechanical, exactly as V2c's was.
- private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
- (_meshAdapter
- ?? throw new InvalidOperationException(
- "ParticleRenderer was constructed without a mesh adapter: its texture " +
- "slots come from that adapter's GL device table (Campaign V slice V4t)."))
- .WorldTextureTable;
-
- private uint _quadVao;
- private readonly uint _quadVbo;
- private readonly uint _quadEbo;
- private uint _instanceVbo;
- private uint _meshVao;
- private uint _meshInstanceVbo;
- private int _instanceVboCapacityBytes;
- private int _meshInstanceVboCapacityBytes;
-
- private sealed class DynamicBufferSet
- {
- public uint BillboardVao;
- public uint BillboardInstanceVbo;
- public int BillboardCapacityBytes;
- public uint MeshVao;
- public uint MeshInstanceVbo;
- public int MeshCapacityBytes;
- }
-
- private readonly List[] _dynamicBufferSetsByFrame =
- [[], [], []];
- private int _dynamicFrameSlot;
- private int _dynamicBufferSetCursor;
private bool _dynamicFrameStarted;
- private DynamicBufferSet? _activeDynamicBufferSet;
- internal (int SetCount, long CapacityBytes) DynamicBufferDiagnostics
- {
- get
- {
- int count = 0;
- long bytes = 0;
- foreach (List frameSets in _dynamicBufferSetsByFrame)
- {
- count += frameSets.Count;
- foreach (DynamicBufferSet set in frameSets)
- bytes += set.BillboardCapacityBytes + set.MeshCapacityBytes;
- }
- return (count, bytes);
- }
- }
+ ///
+ /// The GL arm's per-flight VAO/VBO pool this used to report on was deleted
+ /// at Campaign V slice V11: the RHI arm draws every particle instance from
+ /// a ring allocation that lives until its frame retires, so there is no
+ /// persistent dynamic-buffer pool left to size.
+ ///
+ internal (int SetCount, long CapacityBytes) DynamicBufferDiagnostics => (0, 0);
private BillboardGpuInstance[] _instanceScratch = new BillboardGpuInstance[256];
private float[] _meshInstanceScratch = new float[256 * 20];
@@ -237,155 +166,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
=> owner.ResetDeferredAlpha();
}
- internal ParticleRenderer(
- GL gl,
- string shadersDir,
- ParticleSystem particles,
- TextureCache? textures = null,
- IDatReaderWriter? dats = null,
- WbMeshAdapter? meshAdapter = null,
- RetailAlphaQueue? alphaQueue = null,
- long? alphaScratchBudgetBytes = null)
- {
- _glContext = gl ?? throw new ArgumentNullException(nameof(gl));
- _textures = textures;
- _dats = dats;
- _meshAdapter = meshAdapter;
- _particles = particles ?? throw new ArgumentNullException(nameof(particles));
- _alphaQueue = alphaQueue;
- _alphaSource = new AlphaDrawSource(this);
- long scratchBudget = alphaScratchBudgetBytes
- ?? AlphaScratchBudgetProfile.Create(
- ResidencyBudgetOptions.Default.AlphaScratchBytes).ParticleBytes;
- _alphaScratchPolicy =
- new RetainedScratchCapacityPolicy(scratchBudget);
- if (_meshAdapter is not null)
- {
- _meshReferences = new ParticleMeshReferenceTracker(
- gfxObjId => _meshAdapter.IncrementRefCount(gfxObjId),
- gfxObjId => _meshAdapter.DecrementRefCount(gfxObjId));
- }
- _emitterRetirements = new ParticleEmitterRetirementTracker(
- handle => _meshReferences?.Release(handle),
- handle => _particleGfxInfoByEmitter.Remove(handle),
- handle => _textures?.ReleaseParticleTextureOwner(handle),
- error => Console.Error.WriteLine($"[particles] {error}"));
- var constructionResources = new ResourceCleanupGroup();
- try
- {
- _shader = new Shader(_gl,
- System.IO.Path.Combine(shadersDir, "particle.vert"),
- System.IO.Path.Combine(shadersDir, "particle.frag"),
- includeCommonPreamble: true);
- constructionResources.Add("particle shader", _shader.Dispose);
- if (_meshAdapter?.MeshManager?.GlobalBuffer is not null)
- {
- _meshShader = new Shader(_gl,
- System.IO.Path.Combine(shadersDir, "particle_mesh.vert"),
- System.IO.Path.Combine(shadersDir, "particle_mesh.frag"),
- includeCommonPreamble: true);
- constructionResources.Add(
- "particle mesh shader",
- _meshShader.Dispose);
- _meshTextureIndexLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureIndexA");
- _meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uParamA");
- }
-
- float[] quadVerts =
- {
- -0.5f, -0.5f, 0f, 0f,
- 0.5f, -0.5f, 1f, 0f,
- 0.5f, 0.5f, 1f, 1f,
- -0.5f, 0.5f, 0f, 1f,
- };
- uint[] quadIdx = { 0, 1, 2, 0, 2, 3 };
-
- bool vboAllocated = false;
- bool eboAllocated = false;
- uint quadVbo = TrackedGlResource.CreateBuffer(
- _gl,
- "creating particle quad VBO");
- RetryableGpuResourceRelease quadVboRelease =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- quadVbo,
- () => vboAllocated ? quadVerts.Length * sizeof(float) : 0,
- "rolling back particle quad VBO");
- constructionResources.Add("particle quad VBO", quadVboRelease.Run);
- fixed (void* p = quadVerts)
- {
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- GLEnum.ArrayBuffer,
- quadVbo,
- 0,
- quadVerts.Length * sizeof(float),
- GLEnum.StaticDraw,
- p,
- "uploading particle quad VBO");
- }
- vboAllocated = true;
-
- uint quadEbo = TrackedGlResource.CreateBuffer(
- _gl,
- "creating particle quad EBO");
- RetryableGpuResourceRelease quadEboRelease =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- quadEbo,
- () => eboAllocated ? quadIdx.Length * sizeof(uint) : 0,
- "rolling back particle quad EBO");
- constructionResources.Add("particle quad EBO", quadEboRelease.Run);
-
- uint uploadVao = TrackedGlResource.CreateVertexArray(
- _gl,
- "creating particle upload VAO");
- RetryableGpuResourceRelease uploadVaoRelease =
- TrackedGlResource.CreateRetryableVertexArrayDeletion(
- _gl,
- uploadVao,
- "rolling back particle upload VAO");
- constructionResources.Add("particle upload VAO", uploadVaoRelease.Run);
-
- GlResourceCommand.Execute(
- _gl,
- "bind particle upload VAO",
- () => _gl.BindVertexArray(uploadVao));
- fixed (void* p = quadIdx)
- {
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- GLEnum.ElementArrayBuffer,
- quadEbo,
- 0,
- quadIdx.Length * sizeof(uint),
- GLEnum.StaticDraw,
- p,
- "uploading particle quad EBO");
- }
- eboAllocated = true;
- GlResourceCommand.Execute(_gl, "finish particle static-buffer upload", () =>
- {
- _gl.BindVertexArray(0);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
- });
- uploadVaoRelease.Run();
-
- _quadVbo = quadVbo;
- _quadEbo = quadEbo;
-
- _particles.EmitterDied += OnEmitterDied;
- constructionResources.TransferAll();
- }
- catch (Exception constructionFailure)
- {
- constructionResources.RollbackConstructionAndThrow(
- "ParticleRenderer construction failed and its shader prefix did not cleanly roll back.",
- constructionFailure);
- throw new System.Diagnostics.UnreachableException();
- }
- }
-
///
/// Starts one render frame. Wb point-of-use recovery is limited to one
/// request per missing GfxObj even though portal slicing may invoke Draw
@@ -393,13 +173,13 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
///
public void BeginFrame(int frameSlot)
{
- if ((uint)frameSlot >= (uint)_dynamicBufferSetsByFrame.Length)
- throw new ArgumentOutOfRangeException(nameof(frameSlot));
+ // The GL arm's per-flight VAO/VBO pool this used to index into was
+ // deleted at Campaign V slice V11; the RHI arm takes every instance
+ // from a ring allocation, so frameSlot is validated but otherwise
+ // unused here.
+ ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
- _dynamicFrameSlot = frameSlot;
- _dynamicBufferSetCursor = 0;
_dynamicFrameStarted = true;
- _activeDynamicBufferSet = null;
_meshLoadRequestedThisFrame.Clear();
_emitterRetirements.RetryPending();
_textures?.TickParticleTextureCache();
@@ -498,176 +278,14 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
private void DrawOrdered(ICamera camera)
{
- if (_glContext is null)
- {
- DrawOrderedRhi(camera);
- return;
- }
-
- ParticleSubmissionOrdering.Sort(_submissionScratch);
- GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer;
- Matrix4x4 viewProjection = camera.View * camera.Projection;
-
- _gl.Enable(EnableCap.DepthTest);
- _gl.Enable(EnableCap.Blend);
- _gl.DepthMask(false);
-
- for (int i = 0; i < _submissionScratch.Count;)
- {
- ParticleSubmission submission = _submissionScratch[i];
- if (submission.Kind == ParticleSubmissionKind.Billboard)
- {
- ParticleDraw draw = _drawListScratch[submission.DrawIndex];
- BatchKey key = draw.Key;
- _runScratch.Clear();
- do
- {
- _runScratch.Add(_drawListScratch[submission.DrawIndex].Instance);
- i++;
- if (i >= _submissionScratch.Count)
- break;
- submission = _submissionScratch[i];
- }
- while (submission.Kind == ParticleSubmissionKind.Billboard
- && _drawListScratch[submission.DrawIndex].Key == key);
-
- _gl.BlendFunc(
- BlendingFactor.SrcAlpha,
- key.Additive ? BlendingFactor.One : BlendingFactor.OneMinusSrcAlpha);
- DrawInstances(_runScratch, viewProjection);
- continue;
- }
-
- if (_meshShader is null || global is null)
- {
- i++;
- continue;
- }
-
- MeshParticleDraw meshDraw = _meshDrawListScratch[submission.DrawIndex];
- MeshBatchKey meshKey = meshDraw.Key;
- ObjectRenderBatch batch = meshDraw.Batch;
- _meshRunScratch.Clear();
- do
- {
- _meshRunScratch.Add(_meshDrawListScratch[submission.DrawIndex].Instance);
- i++;
- if (i >= _submissionScratch.Count)
- break;
- submission = _submissionScratch[i];
- }
- while (submission.Kind == ParticleSubmissionKind.Mesh
- && _meshDrawListScratch[submission.DrawIndex].Key == meshKey);
-
- ApplyMeshCullMode(batch.CullMode);
- TranslucencyKind blend = ResolveMeshBlend(batch);
- _gl.BlendFunc(
- blend == TranslucencyKind.InvAlpha
- ? BlendingFactor.OneMinusSrcAlpha
- : BlendingFactor.SrcAlpha,
- blend switch
- {
- TranslucencyKind.Additive => BlendingFactor.One,
- TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha,
- _ => BlendingFactor.OneMinusSrcAlpha,
- });
-
- _gl.ProgramUniform1(
- _meshShader.Program,
- _meshTextureIndexLoc,
- batch.TextureSlot.Index);
- // Slice V6e: uParamA is a float, so the layer is widened here rather
- // than in the shader's float(uTextureLayer). Layers are small
- // integers; the sampled value is bit-identical.
- _gl.ProgramUniform1(_meshShader.Program, _meshTextureLayerLoc, (float)batch.TextureIndex);
-
- UploadMeshInstances(_meshRunScratch);
- PrepareMeshPipeline(viewProjection, global);
- FlushAndBindTextureTable(); // Campaign V slice V2c (binding=9)
- _gl.DrawElementsInstancedBaseVertex(
- PrimitiveType.Triangles,
- (uint)batch.IndexCount,
- DrawElementsType.UnsignedShort,
- (void*)(batch.FirstIndex * sizeof(ushort)),
- (uint)_meshRunScratch.Count,
- (int)batch.BaseVertex);
- }
-
- _gl.BindVertexArray(0);
- _gl.DepthMask(true);
- _gl.Disable(EnableCap.Blend);
- _gl.Disable(EnableCap.CullFace);
+ DrawOrderedRhi(camera);
}
private void PrepareDeferredAlphaDraws(ReadOnlySpan tokens)
{
if (tokens.Length == 0)
return;
-
- if (_glContext is null)
- {
- PrepareDeferredAlphaDrawsRhi(tokens);
- return;
- }
-
- ActivateNextDynamicBufferSet();
-
- int count = tokens.Length;
- if (_preparedAlpha.Length < count)
- Array.Resize(ref _preparedAlpha, count + 256);
- if (_preparedInstanceOffsets.Length < count)
- Array.Resize(ref _preparedInstanceOffsets, count + 256);
- if (_instanceScratch.Length < count)
- Array.Resize(ref _instanceScratch, count + 256);
- int neededMeshFloats = count * 20;
- if (_meshInstanceScratch.Length < neededMeshFloats)
- Array.Resize(ref _meshInstanceScratch, neededMeshFloats + 256 * 20);
-
- int billboardCount = 0;
- int meshCount = 0;
- for (int i = 0; i < count; i++)
- {
- DeferredParticleDraw deferred = _deferredAlpha[tokens[i]];
- _preparedAlpha[i] = deferred;
- if (deferred.Kind == ParticleSubmissionKind.Billboard)
- {
- _preparedInstanceOffsets[i] = (uint)billboardCount;
- WriteBillboardGpuInstance(
- ref _instanceScratch[billboardCount++],
- deferred.Billboard.Instance);
- }
- else
- {
- _preparedInstanceOffsets[i] = (uint)meshCount;
- WriteMeshGpuInstance(
- _meshInstanceScratch,
- meshCount++ * 20,
- deferred.Mesh.Instance);
- }
- }
-
- if (billboardCount > 0)
- {
- fixed (void* bp = _instanceScratch)
- UploadDynamicArrayBuffer(
- _instanceVbo,
- ref _instanceVboCapacityBytes,
- bp,
- billboardCount * sizeof(BillboardGpuInstance));
- }
-
- if (meshCount > 0)
- {
- fixed (void* mp = _meshInstanceScratch)
- UploadDynamicArrayBuffer(
- _meshInstanceVbo,
- ref _meshInstanceVboCapacityBytes,
- mp,
- meshCount * 20 * sizeof(float));
- }
-
- PersistActiveDynamicBufferCapacities();
- _preparedAlphaCount = count;
+ PrepareDeferredAlphaDrawsRhi(tokens);
}
private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
@@ -677,133 +295,7 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
if (firstPreparedDraw < 0
|| firstPreparedDraw > _preparedAlphaCount - drawCount)
throw new ArgumentOutOfRangeException(nameof(firstPreparedDraw));
-
- if (_glContext is null)
- {
- DrawPreparedAlphaBatchRhi(firstPreparedDraw, drawCount);
- return;
- }
-
- GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer;
- _gl.Enable(EnableCap.DepthTest);
- _gl.Enable(EnableCap.Blend);
- _gl.DepthMask(false);
-
- ParticleSubmissionKind? activeKind = null;
- Matrix4x4 activeViewProjection = default;
- int i = firstPreparedDraw;
- int preparedEnd = firstPreparedDraw + drawCount;
- while (i < preparedEnd)
- {
- DeferredParticleDraw deferred = _preparedAlpha[i];
- if (deferred.Kind == ParticleSubmissionKind.Billboard)
- {
- ParticleDraw draw = deferred.Billboard;
- BatchKey key = draw.Key;
- if (activeKind != ParticleSubmissionKind.Billboard
- || activeViewProjection != deferred.ViewProjection)
- {
- PrepareBillboardPipeline(deferred.ViewProjection);
- activeKind = ParticleSubmissionKind.Billboard;
- activeViewProjection = deferred.ViewProjection;
- }
-
- uint baseInstance = _preparedInstanceOffsets[i];
- int runStart = i;
- do
- {
- i++;
- if (i >= preparedEnd)
- break;
- deferred = _preparedAlpha[i];
- }
- while (deferred.Kind == ParticleSubmissionKind.Billboard
- && deferred.Billboard.Key == key
- && deferred.ViewProjection == activeViewProjection);
-
- _gl.BlendFunc(
- BlendingFactor.SrcAlpha,
- key.Additive ? BlendingFactor.One : BlendingFactor.OneMinusSrcAlpha);
- _gl.BindVertexArray(_quadVao);
- FlushAndBindTextureTable(); // Campaign V slice V2c (binding=9)
- _gl.DrawElementsInstancedBaseInstance(
- PrimitiveType.Triangles,
- 6,
- DrawElementsType.UnsignedInt,
- (void*)0,
- (uint)(i - runStart),
- baseInstance);
- continue;
- }
-
- if (_meshShader is null || global is null)
- {
- i++;
- continue;
- }
-
- MeshParticleDraw meshDraw = deferred.Mesh;
- MeshBatchKey meshKey = meshDraw.Key;
- ObjectRenderBatch batch = meshDraw.Batch;
- if (activeKind != ParticleSubmissionKind.Mesh
- || activeViewProjection != deferred.ViewProjection)
- {
- PrepareMeshPipeline(deferred.ViewProjection, global);
- activeKind = ParticleSubmissionKind.Mesh;
- activeViewProjection = deferred.ViewProjection;
- }
-
- uint meshBaseInstance = _preparedInstanceOffsets[i];
- int meshRunStart = i;
- do
- {
- i++;
- if (i >= preparedEnd)
- break;
- deferred = _preparedAlpha[i];
- }
- while (deferred.Kind == ParticleSubmissionKind.Mesh
- && deferred.Mesh.Key == meshKey
- && deferred.ViewProjection == activeViewProjection);
-
- ApplyMeshCullMode(batch.CullMode);
- TranslucencyKind blend = ResolveMeshBlend(batch);
- _gl.BlendFunc(
- blend == TranslucencyKind.InvAlpha
- ? BlendingFactor.OneMinusSrcAlpha
- : BlendingFactor.SrcAlpha,
- blend switch
- {
- TranslucencyKind.Additive => BlendingFactor.One,
- TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha,
- _ => BlendingFactor.OneMinusSrcAlpha,
- });
-
- _gl.ProgramUniform1(
- _meshShader.Program,
- _meshTextureIndexLoc,
- batch.TextureSlot.Index);
- // Slice V6e: uParamA is a float, so the layer is widened here rather
- // than in the shader's float(uTextureLayer). Layers are small
- // integers; the sampled value is bit-identical.
- _gl.ProgramUniform1(_meshShader.Program, _meshTextureLayerLoc, (float)batch.TextureIndex);
-
- _gl.BindVertexArray(_meshVao);
- FlushAndBindTextureTable(); // Campaign V slice V2c (binding=9)
- _gl.DrawElementsInstancedBaseVertexBaseInstance(
- PrimitiveType.Triangles,
- (uint)batch.IndexCount,
- DrawElementsType.UnsignedShort,
- (void*)(batch.FirstIndex * sizeof(ushort)),
- (uint)(i - meshRunStart),
- (int)batch.BaseVertex,
- meshBaseInstance);
- }
-
- _gl.BindVertexArray(0);
- _gl.DepthMask(true);
- _gl.Disable(EnableCap.Blend);
- _gl.Disable(EnableCap.CullFace);
+ DrawPreparedAlphaBatchRhi(firstPreparedDraw, drawCount);
}
private void ResetDeferredAlpha()
@@ -834,54 +326,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
Array.Resize(ref _preparedInstanceOffsets, targetCapacity);
}
- private void PrepareBillboardPipeline(Matrix4x4 viewProjection)
- {
- _shader!.Use();
- _shader.SetMatrix4("uViewProjection", viewProjection);
- _gl.Disable(EnableCap.CullFace);
- _gl.BindVertexArray(_quadVao);
- }
-
- private void PrepareMeshPipeline(Matrix4x4 viewProjection, GlobalMeshBuffer global)
- {
- _meshShader!.Use();
- _meshShader.SetMatrix4("uViewProjection", viewProjection);
- _gl.FrontFace(FrontFaceDirection.CW);
- _gl.BindVertexArray(_meshVao);
-
- // GlobalMeshBuffer may grow and replace either backing buffer. Bind
- // today's buffer names on every pipeline switch instead of caching
- // them in this VAO at construction time.
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, global.VBO);
- int vertexStride = VertexPositionNormalTexture.Size;
- _gl.EnableVertexAttribArray(0);
- _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)0);
- _gl.EnableVertexAttribArray(1);
- _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)(3 * sizeof(float)));
- _gl.EnableVertexAttribArray(2);
- _gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)(6 * sizeof(float)));
- _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, global.IBO);
-
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _meshInstanceVbo);
- const int instanceStride = 20 * sizeof(float);
- for (uint column = 0; column < 4; column++)
- {
- uint location = 3 + column;
- _gl.EnableVertexAttribArray(location);
- _gl.VertexAttribPointer(
- location,
- 4,
- VertexAttribPointerType.Float,
- false,
- instanceStride,
- (void*)(column * 4 * sizeof(float)));
- _gl.VertexAttribDivisor(location, 1);
- }
- _gl.EnableVertexAttribArray(7);
- _gl.VertexAttribPointer(7, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(16 * sizeof(float)));
- _gl.VertexAttribDivisor(7, 1);
- }
-
private void BuildDrawLists(
Vector3 cameraWorldPos,
ParticleRenderPass renderPass,
@@ -1042,51 +486,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
return true;
}
- private void DrawInstances(List instances, Matrix4x4 viewProjection)
- {
- if (instances.Count == 0)
- return;
-
- ActivateNextDynamicBufferSet();
-
- if (_instanceScratch.Length < instances.Count)
- _instanceScratch = new BillboardGpuInstance[instances.Count + 256];
-
- for (int i = 0; i < instances.Count; i++)
- WriteBillboardGpuInstance(ref _instanceScratch[i], instances[i]);
-
- fixed (void* bp = _instanceScratch)
- UploadDynamicArrayBuffer(
- _instanceVbo,
- ref _instanceVboCapacityBytes,
- bp,
- instances.Count * sizeof(BillboardGpuInstance));
-
- PersistActiveDynamicBufferCapacities();
- PrepareBillboardPipeline(viewProjection);
- FlushAndBindTextureTable(); // Campaign V slice V2c (binding=9)
- _gl.DrawElementsInstanced(PrimitiveType.Triangles, 6, DrawElementsType.UnsignedInt, (void*)0, (uint)instances.Count);
- }
-
- private void UploadMeshInstances(List instances)
- {
- ActivateNextDynamicBufferSet();
- int needed = instances.Count * 20;
- if (_meshInstanceScratch.Length < needed)
- _meshInstanceScratch = new float[needed + 256 * 20];
-
- for (int i = 0; i < instances.Count; i++)
- WriteMeshGpuInstance(_meshInstanceScratch, i * 20, instances[i]);
-
- fixed (void* bp = _meshInstanceScratch)
- UploadDynamicArrayBuffer(
- _meshInstanceVbo,
- ref _meshInstanceVboCapacityBytes,
- bp,
- instances.Count * 20 * sizeof(float));
- PersistActiveDynamicBufferCapacities();
- }
-
///
/// Campaign V slice V6e: the shader-side spelling of "this particle has no
/// texture, draw the procedural blob". It must agree with
@@ -1156,192 +555,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
destination[offset + 19] = ((instance.ColorArgb >> 24) & 0xFF) / 255f;
}
- private void ActivateNextDynamicBufferSet()
- {
- if (!_dynamicFrameStarted)
- throw new InvalidOperationException("BeginFrame must be called before drawing particles.");
-
- List slotSets = _dynamicBufferSetsByFrame[_dynamicFrameSlot];
- if (_dynamicBufferSetCursor == slotSets.Count)
- slotSets.Add(CreateDynamicBufferSet());
-
- DynamicBufferSet set = slotSets[_dynamicBufferSetCursor++];
- _activeDynamicBufferSet = set;
- _quadVao = set.BillboardVao;
- _instanceVbo = set.BillboardInstanceVbo;
- _instanceVboCapacityBytes = set.BillboardCapacityBytes;
- _meshVao = set.MeshVao;
- _meshInstanceVbo = set.MeshInstanceVbo;
- _meshInstanceVboCapacityBytes = set.MeshCapacityBytes;
- }
-
- private DynamicBufferSet CreateDynamicBufferSet()
- {
- var set = new DynamicBufferSet();
- try
- {
- set.BillboardVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle billboard VAO");
- set.BillboardInstanceVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle billboard instance VBO");
- set.MeshVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle mesh VAO");
- set.MeshInstanceVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle mesh instance VBO");
-
- _gl.BindVertexArray(set.BillboardVao);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _quadVbo);
- _gl.EnableVertexAttribArray(0);
- _gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)0);
- _gl.EnableVertexAttribArray(1);
- _gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)(2 * sizeof(float)));
- _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _quadEbo);
-
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.BillboardInstanceVbo);
- uint instanceStride = (uint)sizeof(BillboardGpuInstance);
- _gl.EnableVertexAttribArray(2);
- _gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)0);
- _gl.VertexAttribDivisor(2, 1);
- _gl.EnableVertexAttribArray(3);
- _gl.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(4 * sizeof(float)));
- _gl.VertexAttribDivisor(3, 1);
- _gl.EnableVertexAttribArray(4);
- _gl.VertexAttribPointer(4, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(8 * sizeof(float)));
- _gl.VertexAttribDivisor(4, 1);
- _gl.EnableVertexAttribArray(5);
- _gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(12 * sizeof(float)));
- _gl.VertexAttribDivisor(5, 1);
- // Campaign V slice V2c: one uint table slot (was uvec2 low/high
- // handle halves) — BillboardGpuInstance shrank by 4 bytes.
- _gl.EnableVertexAttribArray(6);
- _gl.VertexAttribIPointer(
- 6,
- 1,
- VertexAttribIType.UnsignedInt,
- instanceStride,
- (void*)(16 * sizeof(float)));
- _gl.VertexAttribDivisor(6, 1);
-
- _gl.BindVertexArray(0);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
- GLHelpers.ThrowOnResourceError(_gl, "configuring particle dynamic VAOs");
- return set;
- }
- catch (Exception creationFailure)
- {
- try { DeleteDynamicBufferSet(set); }
- catch (Exception cleanupFailure)
- {
- throw new AggregateException(
- "Particle dynamic-buffer creation and rollback failed.",
- creationFailure,
- cleanupFailure);
- }
- throw;
- }
- }
-
- private void DeleteDynamicBufferSet(DynamicBufferSet set)
- {
- List? failures = null;
- void Attempt(Action action)
- {
- try { action(); }
- catch (Exception ex) { (failures ??= []).Add(ex); }
- }
-
- Attempt(() => TrackedGlResource.DeleteBuffer(
- _gl,
- set.BillboardInstanceVbo,
- set.BillboardCapacityBytes,
- "deleting particle billboard instance VBO"));
- Attempt(() => TrackedGlResource.DeleteBuffer(
- _gl,
- set.MeshInstanceVbo,
- set.MeshCapacityBytes,
- "deleting particle mesh instance VBO"));
- Attempt(() => TrackedGlResource.DeleteVertexArray(
- _gl,
- set.BillboardVao,
- "deleting particle billboard VAO"));
- Attempt(() => TrackedGlResource.DeleteVertexArray(
- _gl,
- set.MeshVao,
- "deleting particle mesh VAO"));
- if (failures is not null)
- throw new AggregateException("One or more particle dynamic resources failed to delete.", failures);
- }
-
- private void PersistActiveDynamicBufferCapacities()
- {
- DynamicBufferSet set = _activeDynamicBufferSet
- ?? throw new InvalidOperationException("No dynamic particle buffer set is active.");
- set.BillboardCapacityBytes = _instanceVboCapacityBytes;
- set.MeshCapacityBytes = _meshInstanceVboCapacityBytes;
- }
-
- private void UploadDynamicArrayBuffer(
- uint buffer,
- ref int capacityBytes,
- void* data,
- int byteCount)
- {
- if (byteCount <= 0)
- throw new ArgumentOutOfRangeException(nameof(byteCount));
-
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, buffer);
- if (capacityBytes < byteCount)
- {
- int grownCapacity = DynamicBufferCapacity.Grow(capacityBytes, byteCount);
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- GLEnum.ArrayBuffer,
- buffer,
- capacityBytes,
- grownCapacity,
- GLEnum.DynamicDraw,
- $"growing particle dynamic buffer {buffer} to {grownCapacity} bytes");
- capacityBytes = grownCapacity;
- }
-
- _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)byteCount, data);
- }
-
- ///
- /// Campaign V slice V4t: drains the device texture table's dirty runs and
- /// (re)binds it at
- /// .
- /// Still called immediately before every draw call rather than once per
- /// pipeline switch, for the reason V2c gave: a run of consecutive
- /// mesh-particle sub-batches can pull in a texture whose slot was registered
- /// this frame, and the table must be current for each one.
- ///
- private void FlushAndBindTextureTable()
- {
- AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable;
- device.FlushTextureTable();
- _gl.BindBufferBase(
- BufferTargetARB.ShaderStorageBuffer,
- AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
- device.TextureTableGlName);
- }
-
- private void ApplyMeshCullMode(CullMode mode)
- {
- _gl.FrontFace(FrontFaceDirection.CW);
- 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;
- }
- }
-
private TranslucencyKind ResolveMeshBlend(ObjectRenderBatch batch)
{
uint surfaceId = batch.Key.SurfaceId;
@@ -1652,38 +865,12 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
if (_meshReferences is not null)
releases.Add(("mesh-references", _meshReferences.Dispose));
- // Campaign V slice V6l: the RHI arm owns pipelines and two static quad
- // buffers and no GL names at all, so it releases through the same
- // retryable ledger and then there is nothing else to do.
- if (_glContext is null)
- {
- releases.Add(("rhi-resources", DisposeRhiResources));
- return;
- }
-
- AddTrackedBufferRelease(
- releases,
- _quadVbo,
- 16L * sizeof(float),
- "quad-vbo",
- "deleting particle quad VBO");
- AddTrackedBufferRelease(
- releases,
- _quadEbo,
- 6L * sizeof(uint),
- "quad-ebo",
- "deleting particle quad EBO");
- for (int frame = 0; frame < _dynamicBufferSetsByFrame.Length; frame++)
- {
- List frameSets = _dynamicBufferSetsByFrame[frame];
- for (int index = 0; index < frameSets.Count; index++)
- AddDynamicBufferSetReleases(releases, frameSets[index], frame, index);
- }
-
- if (_shader is not null)
- releases.Add(("billboard-shader", _shader.Dispose));
- if (_meshShader is not null)
- releases.Add(("mesh-shader", _meshShader.Dispose));
+ // The RHI arm owns pipelines and two static quad buffers and no GL
+ // names at all, so it releases through the same retryable ledger and
+ // then there is nothing else to do. The raw-GL release path (tracked
+ // quad/dynamic-buffer/shader deletions) was deleted at Campaign V
+ // slice V11.
+ releases.Add(("rhi-resources", DisposeRhiResources));
}
private void RetireEveryResolvedEmitter()
@@ -1694,82 +881,9 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
_emitterRetirements.CompleteOrThrow();
}
- private void AddDynamicBufferSetReleases(
- List<(string Name, Action Release)> releases,
- DynamicBufferSet set,
- int frame,
- int index)
- {
- AddTrackedBufferRelease(
- releases,
- set.BillboardInstanceVbo,
- set.BillboardCapacityBytes,
- $"dynamic-{frame}-{index}-billboard-vbo",
- "deleting particle billboard instance VBO");
- AddTrackedBufferRelease(
- releases,
- set.MeshInstanceVbo,
- set.MeshCapacityBytes,
- $"dynamic-{frame}-{index}-mesh-vbo",
- "deleting particle mesh instance VBO");
- AddTrackedVertexArrayRelease(
- releases,
- set.BillboardVao,
- $"dynamic-{frame}-{index}-billboard-vao",
- "deleting particle billboard VAO");
- AddTrackedVertexArrayRelease(
- releases,
- set.MeshVao,
- $"dynamic-{frame}-{index}-mesh-vao",
- "deleting particle mesh VAO");
- }
-
- 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));
- }
-
- private void AddTrackedVertexArrayRelease(
- List<(string Name, Action Release)> releases,
- uint vertexArray,
- string name,
- string context)
- {
- if (vertexArray == 0)
- return;
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableVertexArrayDeletion(
- _gl,
- vertexArray,
- context);
- releases.Add((name, release.Run));
- }
-
private void CompleteDispose()
{
- foreach (List frameSets in _dynamicBufferSetsByFrame)
- frameSets.Clear();
- _activeDynamicBufferSet = null;
_dynamicFrameStarted = false;
- _quadVao = 0;
- _instanceVbo = 0;
- _meshVao = 0;
- _meshInstanceVbo = 0;
- _instanceVboCapacityBytes = 0;
- _meshInstanceVboCapacityBytes = 0;
_particleGfxInfoByEmitter.Clear();
_particleGfxInfoByGfxObj.Clear();
_geometryKindByGfxObj.Clear();
diff --git a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
index 7f3e0889..a20f286f 100644
--- a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
+++ b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
@@ -1,8 +1,5 @@
using System;
-using System.Linq;
using System.Numerics;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -42,196 +39,42 @@ namespace AcDream.App.Rendering;
/// depth write lands only inside the slice region, matching retail's clipped
/// fan.
///
-/// Self-contained GL state (feedback_render_self_contained_gl_state):
-/// sets everything it depends on, restores the frame-global convention on
-/// exit, no early-outs between set and restore.
+/// The GL arm's inline program (rehomed clip planes, raw uniform
+/// locations) and this class's own dynamic VAO/VBO ring were deleted at
+/// Campaign V slice V11; the RHI arm compiles
+/// Rendering/Shaders/portal_depth.{vert,frag} from committed SPIR-V,
+/// and its per-frame fan vertices come from the frame ring instead.
///
public sealed partial class PortalDepthMaskRenderer : IDisposable
{
- ///
- /// The GL arm's inline program. Campaign V slice V6l added a SECOND arm that
- /// compiles Rendering/Shaders/portal_depth.{vert,frag} — the same body,
- /// with its loose uniforms rehomed onto the shared push block and the clip
- /// planes onto the TerrainClip UBO — because a Vulkan pipeline needs a
- /// named, SPIR-V-compiled pair. This string stays because the GL arm keeps
- /// its raw path through to V10 (plan §5.5.6) and because rehoming its clip
- /// planes onto UBO binding 2 would clobber the terrain clip block that
- /// ClipFrame binds there globally on GL. The two are kept in step by
- /// PortalDepthShaderParityTests, and this one is deleted at V11.
- ///
- internal const string VertSrc = @"#version 430 core
-layout(location = 0) in vec3 aPos;
-uniform mat4 uViewProjection;
-uniform int uPlaneCount;
-uniform vec4 uPlanes[8];
-uniform int uForceFarZ;
-uniform float uDepthBias; // NDC bias toward the viewer (mark pass only)
-uniform float uDepthBiasEyeCapN; // eye-span cap x near plane (#129; see MarkBiasNdc)
-out float gl_ClipDistance[8];
-void main()
-{
- vec4 clipPos = uViewProjection * vec4(aPos, 1.0);
- for (int i = 0; i < 8; i++)
- gl_ClipDistance[i] = (i < uPlaneCount) ? dot(uPlanes[i], clipPos) : 1.0;
- if (uForceFarZ == 1)
- clipPos.z = clipPos.w * 0.99999988; // retail far-z punch constant (0x0059bc90 tail)
- else if (uDepthBias > 0.0)
- {
- // #117 mark-pass bias, #129 eye-space cap. clipPos.w = eye depth d;
- // an NDC bias b spans ~b*d*d/near meters of eye depth, so the
- // constant-NDC form alone reached METERS at distance (door-shaped
- // leaks through hills/houses). Keep in sync with MarkBiasNdc.
- float biasNdc = min(uDepthBias, uDepthBiasEyeCapN / max(clipPos.w * clipPos.w, 1e-6));
- clipPos.z -= biasNdc * clipPos.w;
- }
- gl_Position = clipPos;
-}";
-
- internal const string FragSrc = @"#version 430 core
-void main() { } // depth-only: color writes are masked off by the caller state
-";
-
- private readonly GL? _glContext;
- private GL _gl => _glContext
- ?? throw new InvalidOperationException(
- "PortalDepthMaskRenderer's GL arm was reached on a backend with no GL context "
- + "(campaign plan slice V6l; the RHI arm lives in PortalDepthMaskRenderer.Rhi.cs).");
-
- private readonly uint _program;
- private readonly int _locViewProjection;
- private readonly int _locPlaneCount;
- private readonly int _locPlanes;
- private readonly int _locForceFarZ;
- private readonly int _locDepthBias;
- private readonly int _locDepthBiasEyeCapN;
private readonly ResourceCleanupGroup _resources;
+ /// Shared by both arms: the largest fan accepts.
private const int MaxFanVerts = 32;
- private readonly float[] _scratch = new float[MaxFanVerts * 3];
- private sealed class FrameBufferSet
- {
- public uint Vao;
- public uint Vbo;
- public int CapacityBytes;
- public int UsedVertices;
- }
-
- private readonly FrameBufferSet?[] _frameBuffers = new FrameBufferSet[3];
- private FrameBufferSet? _activeFrameBuffer;
-
- internal long DynamicBufferCapacityBytes =>
- _frameBuffers.Sum(set => (long)(set?.CapacityBytes ?? 0));
-
- public PortalDepthMaskRenderer(GL gl)
- {
- _glContext = gl ?? throw new ArgumentNullException(nameof(gl));
- var resources = new ResourceCleanupGroup();
- try
- {
- _program = ShaderProgramConstruction.Build(
- new GlShaderProgramBuildApi(gl),
- VertSrc,
- FragSrc);
- resources.Add(
- "portal-depth program",
- () => GlResourceCommand.DeleteProgram(
- gl,
- _program,
- $"delete PortalDepthMask program {_program}"));
-
- (
- _locViewProjection,
- _locPlaneCount,
- _locPlanes,
- _locForceFarZ,
- _locDepthBias,
- _locDepthBiasEyeCapN) = GlResourceCommand.Execute(
- _gl,
- "resolve PortalDepthMask uniforms",
- () => (
- _gl.GetUniformLocation(_program, "uViewProjection"),
- _gl.GetUniformLocation(_program, "uPlaneCount"),
- _gl.GetUniformLocation(_program, "uPlanes"),
- _gl.GetUniformLocation(_program, "uForceFarZ"),
- _gl.GetUniformLocation(_program, "uDepthBias"),
- _gl.GetUniformLocation(_program, "uDepthBiasEyeCapN")));
-
- for (int i = 0; i < _frameBuffers.Length; i++)
- {
- FrameBufferSet set = CreateFrameBufferSet(resources, i);
- _frameBuffers[i] = set;
- }
- }
- catch (Exception constructionFailure)
- {
- resources.RollbackConstructionAndThrow(
- "PortalDepthMaskRenderer construction failed and its GL prefix did not cleanly roll back.",
- constructionFailure);
- throw new System.Diagnostics.UnreachableException();
- }
-
- _resources = resources;
- }
+ ///
+ /// The GL arm's per-flight VAO/VBO ring this used to report on was deleted
+ /// at Campaign V slice V11: the RHI arm draws every fan from a ring
+ /// allocation that lives until its frame retires, so there is no
+ /// persistent dynamic-buffer pool left to size.
+ ///
+ internal long DynamicBufferCapacityBytes => 0;
///
/// Selects the GPU-fenced frame slot and resets its append cursor. Portal
/// fans written during one frame occupy distinct ranges, so later portal
/// slices never overwrite vertices still referenced by earlier draws.
+ ///
+ /// The GL arm's per-flight VAO/VBO ring this used to activate was
+ /// deleted at Campaign V slice V11 — the RHI arm owns no per-flight ring
+ /// at all, because every frame ring allocation is already distinct memory
+ /// that lives until the frame retires — so all this edge carries now is
+ /// the started latch.
///
public void BeginFrame(int frameSlot)
{
- if ((uint)frameSlot >= (uint)_frameBuffers.Length)
- throw new ArgumentOutOfRangeException(nameof(frameSlot));
- // Slice V6l: the RHI arm owns no per-flight VBO ring — every frame ring
- // allocation is already distinct memory that lives until the frame
- // retires — so all this edge carries there is the started latch.
+ ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
_rhiFrameStarted = true;
- if (_glContext is null)
- return;
- _activeFrameBuffer = _frameBuffers[frameSlot];
- _activeFrameBuffer!.UsedVertices = 0;
- }
-
- private FrameBufferSet CreateFrameBufferSet(
- ResourceCleanupGroup resources,
- int frameIndex)
- {
- uint vao = TrackedGlResource.CreateVertexArray(
- _gl,
- "PortalDepthMask frame VAO creation");
- RetryableGpuResourceRelease vaoRelease =
- TrackedGlResource.CreateRetryableVertexArrayDeletion(
- _gl,
- vao,
- "PortalDepthMask frame VAO disposal");
- resources.Add($"portal-depth frame {frameIndex} VAO", vaoRelease.Run);
-
- uint vbo = TrackedGlResource.CreateBuffer(
- _gl,
- "PortalDepthMask frame VBO creation");
- var set = new FrameBufferSet { Vao = vao, Vbo = vbo };
- RetryableGpuResourceRelease vboRelease =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- vbo,
- () => set.CapacityBytes,
- "PortalDepthMask frame VBO disposal");
- resources.Add($"portal-depth frame {frameIndex} VBO", vboRelease.Run);
-
- GlResourceCommand.Execute(_gl, "configure PortalDepthMask frame buffers", () =>
- {
- _gl.BindVertexArray(set.Vao);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo);
- unsafe
- {
- _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), (void*)0);
- }
- _gl.EnableVertexAttribArray(0);
- _gl.BindVertexArray(0);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
- });
- return set;
}
///
@@ -303,143 +146,8 @@ void main() { } // depth-only: color writes are masked off by the caller state
{
if (worldVerts.Length < 3)
return;
- if (_glContext is null)
- {
- DrawDepthFanRhi(worldVerts, in viewProjection, planes, forceFarZ);
- return;
- }
-
- FrameBufferSet frameBuffer = _activeFrameBuffer
- ?? throw new InvalidOperationException("BeginFrame must be called before drawing portal depth masks.");
- int n = Math.Min(worldVerts.Length, MaxFanVerts);
- int planeCount = Math.Min(planes.Length, 8);
- int firstVertex = frameBuffer.UsedVertices;
- int requiredBytes = checked((firstVertex + n) * 3 * sizeof(float));
-
- for (int i = 0; i < n; i++)
- {
- _scratch[i * 3 + 0] = worldVerts[i].X;
- _scratch[i * 3 + 1] = worldVerts[i].Y;
- _scratch[i * 3 + 2] = worldVerts[i].Z;
- }
-
- // ---- set state (everything this draw depends on) ----
- _gl.UseProgram(_program);
- _gl.Disable(EnableCap.Blend);
- _gl.Disable(EnableCap.CullFace); // portal fans face either way
- _gl.Disable(EnableCap.ScissorTest);
- _gl.Enable(EnableCap.DepthTest);
- _gl.ColorMask(false, false, false, false); // alpha-0 fan ≙ no color
- for (int i = 0; i < planeCount; i++)
- _gl.Enable(EnableCap.ClipDistance0 + i);
-
- unsafe
- {
- var m = viewProjection;
- _gl.UniformMatrix4(_locViewProjection, 1, false, (float*)&m);
- _gl.Uniform1(_locPlaneCount, planeCount);
- if (planeCount > 0)
- {
- Span p = stackalloc float[planeCount * 4];
- for (int i = 0; i < planeCount; i++)
- {
- p[i * 4 + 0] = planes[i].X;
- p[i * 4 + 1] = planes[i].Y;
- p[i * 4 + 2] = planes[i].Z;
- p[i * 4 + 3] = planes[i].W;
- }
- fixed (float* pp = p)
- _gl.Uniform4(_locPlanes, (uint)planeCount, pp);
- }
-
- _gl.BindVertexArray(frameBuffer.Vao);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, frameBuffer.Vbo);
- if (frameBuffer.CapacityBytes < requiredBytes)
- {
- int newCapacity = DynamicBufferCapacity.Grow(
- frameBuffer.CapacityBytes,
- requiredBytes);
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- GLEnum.ArrayBuffer,
- frameBuffer.Vbo,
- frameBuffer.CapacityBytes,
- newCapacity,
- GLEnum.DynamicDraw,
- "PortalDepthMask frame VBO growth");
- frameBuffer.CapacityBytes = newCapacity;
- }
- fixed (float* v = _scratch)
- _gl.BufferSubData(
- BufferTargetARB.ArrayBuffer,
- (nint)(firstVertex * 3 * sizeof(float)),
- (nuint)(n * 3 * sizeof(float)),
- v);
- frameBuffer.UsedVertices += n;
-
- if (!forceFarZ)
- {
- // ── SEAL: retail-verbatim single pass ──
- _gl.DepthFunc(DepthFunction.Always);
- _gl.DepthMask(true);
- _gl.Uniform1(_locForceFarZ, 0);
- _gl.Uniform1(_locDepthBias, 0f);
- _gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n);
- }
- else
- {
- // ── PUNCH pass A: stencil-mark visible aperture pixels ──
- _gl.Enable(EnableCap.StencilTest);
- _gl.StencilFunc(StencilFunction.Always, 1, 0xFF);
- _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
- _gl.StencilMask(0xFF);
- _gl.DepthFunc(DepthFunction.Lequal);
- _gl.DepthMask(false);
- _gl.Uniform1(_locForceFarZ, 0);
- _gl.Uniform1(_locDepthBias, PunchMarkDepthBias);
- _gl.Uniform1(_locDepthBiasEyeCapN,
- PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters);
- _gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n);
-
- // ── PUNCH pass B: far-Z write on marked pixels only;
- // zero the stencil as we go (self-cleaning) ──
- _gl.StencilFunc(StencilFunction.Equal, 1, 0xFF);
- _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Zero);
- _gl.DepthFunc(DepthFunction.Always);
- _gl.DepthMask(true);
- _gl.Uniform1(_locForceFarZ, 1);
- _gl.Uniform1(_locDepthBias, 0f);
- _gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n);
-
- _gl.Disable(EnableCap.StencilTest);
- }
-
- _gl.BindVertexArray(0);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
- }
-
- // ---- restore the frame-global convention ----
- for (int i = 0; i < planeCount; i++)
- _gl.Disable(EnableCap.ClipDistance0 + i);
- _gl.ColorMask(true, true, true, true);
- _gl.DepthMask(true);
- _gl.DepthFunc(DepthFunction.Less);
- // Renderers opt into culling locally; the shared frame convention is
- // cull-off. Keep this success exit identical to frame-abort recovery.
- _gl.Disable(EnableCap.CullFace);
- _gl.CullFace(TriangleFace.Back);
- _gl.FrontFace(FrontFaceDirection.CW);
- _gl.UseProgram(0);
+ DrawDepthFanRhi(worldVerts, in viewProjection, planes, forceFarZ);
}
- public void Dispose()
- {
- if (_glContext is null)
- {
- DisposeRhiResources();
- return;
- }
-
- _resources.RetryCleanup();
- }
+ public void Dispose() => DisposeRhiResources();
}
diff --git a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs
index 9ded8cd7..28d43065 100644
--- a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs
+++ b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs
@@ -12,7 +12,6 @@ using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -24,14 +23,12 @@ namespace AcDream.App.Rendering;
/// retained gameplay UI.
///
/// Campaign V slice V6m. This was the last raw-GL world-adjacent
-/// renderer; it now draws on both arms. Nothing about the scene changed — the
-/// same synthetic Setup, the same 40 fps sequence, the same rotation and the
-/// same distant light, through the same (already dual-arm)
-/// . What forked is where the draw is recorded:
-/// GL sets ambient capability state under a and draws
-/// into whatever framebuffer is bound, while the RHI arm opens a pass of its own
-/// against the backbuffer and publishes it on for
-/// the span of the draw, exactly as the two offscreen viewports do
+/// renderer, and its GL arm was deleted at slice V11. Nothing about the scene
+/// changed — the same synthetic Setup, the same 40 fps sequence, the same
+/// rotation and the same distant light, through the same
+/// . The RHI arm opens a pass of its own against
+/// the backbuffer and publishes it on for the
+/// span of the draw, exactly as the two offscreen viewports do
/// (, slice V6l).
///
public sealed class PortalTunnelPresentation : IDisposable
@@ -40,13 +37,6 @@ public sealed class PortalTunnelPresentation : IDisposable
public const uint AnimationClientEnum = 0x10000002u;
public const uint ClientEnumCategory = 7u;
- // UIViewportObject::DrawContent @ 0x006950A5 calls
- // RenderDeviceD3D::Clear(4, RGBAColor_Black, 1). Clear @ 0x0059FD30
- // maps retail flag 4 to D3DCLEAR_ZBUFFER only: portal space deliberately
- // preserves the black color target established by SceneTool::BeginScene's
- // whole-frame Clear(7) while replacing the hidden world viewport.
- internal const ClearBufferMask RetailViewportClearMask = ClearBufferMask.DepthBufferBit;
-
///
/// The colour the RHI arm's pass loads with, and the one value that makes
/// its Clear load-op equivalent to GL's depth-only clear.
@@ -74,22 +64,17 @@ public sealed class PortalTunnelPresentation : IDisposable
private static readonly HashSet AnimatedIds = new() { SyntheticEntityId };
- /// The GL arm's context, or null on a backend that has none.
- private readonly GL? _glContext;
-
///
- /// The world pass scope, or null on the GL arm.
- ///
- /// 's RHI arm borrows its pass from the
- /// scope rather than opening one, so a presentation that opens a pass of its
- /// own has to publish it there for the duration of the draw. On the GL arm
- /// the dispatcher records against whatever framebuffer is bound, so nothing
- /// is published.
+ /// The world pass scope this presentation's own pass publishes into for
+ /// the span of the draw. borrows its pass
+ /// from the scope rather than opening one, so a presentation that opens a
+ /// pass of its own has to publish it there. The raw-GL arm this used to be
+ /// optional for was deleted at Campaign V slice V11.
///
- private readonly IWorldPassScope? _scope;
+ private readonly IWorldPassScope _scope;
- /// The frame the RHI arm's pass is opened on; null on the GL arm.
- private readonly ICurrentGpuFrameSource? _frames;
+ /// The frame this presentation's own pass is opened on.
+ private readonly ICurrentGpuFrameSource _frames;
private readonly WbDrawDispatcher _dispatcher;
private readonly SceneLightingUboBinding _lightUbo;
@@ -117,9 +102,8 @@ public sealed class PortalTunnelPresentation : IDisposable
private bool _disposed;
private PortalTunnelPresentation(
- GL? gl,
- IWorldPassScope? scope,
- ICurrentGpuFrameSource? frames,
+ IWorldPassScope scope,
+ ICurrentGpuFrameSource frames,
WbDrawDispatcher dispatcher,
SceneLightingUboBinding lightUbo,
IWbMeshAdapter meshAdapter,
@@ -132,7 +116,6 @@ public sealed class PortalTunnelPresentation : IDisposable
Action? displayNotice,
IDisposable? displayNoticeLifetime)
{
- _glContext = gl;
_scope = scope;
_frames = frames;
_dispatcher = dispatcher;
@@ -173,9 +156,8 @@ public sealed class PortalTunnelPresentation : IDisposable
/// Composition is the only caller.
///
internal static PortalTunnelPresentation CreateRequired(
- GL? gl,
- IWorldPassScope? scope,
- ICurrentGpuFrameSource? frames,
+ IWorldPassScope scope,
+ ICurrentGpuFrameSource frames,
IDatReaderWriter dats,
IAnimationLoader animationLoader,
IAnimationHookSink hookSink,
@@ -186,14 +168,8 @@ public sealed class PortalTunnelPresentation : IDisposable
IDisposable? displayNoticeLifetime = null,
Random? random = null)
{
- if (gl is null && (scope is null || frames is null))
- {
- throw new ArgumentNullException(
- nameof(scope),
- "A backend without a GL context must supply a world pass scope and a frame source "
- + "for portal space to open its pass on.");
- }
-
+ ArgumentNullException.ThrowIfNull(scope);
+ ArgumentNullException.ThrowIfNull(frames);
ArgumentNullException.ThrowIfNull(dats);
ArgumentNullException.ThrowIfNull(animationLoader);
ArgumentNullException.ThrowIfNull(hookSink);
@@ -215,7 +191,6 @@ public sealed class PortalTunnelPresentation : IDisposable
animation is not null);
return new PortalTunnelPresentation(
- gl,
scope,
frames,
dispatcher,
@@ -335,51 +310,21 @@ public sealed class PortalTunnelPresentation : IDisposable
_camera.Aspect = width / (float)height;
_camera.UseSmartBoxFov(smartBoxProjection);
- if (_glContext is { } gl)
- {
- DrawGl(gl, width, height);
- return;
- }
-
DrawRhi();
}
- private void DrawGl(GL gl, int width, int height)
- {
- using var scope = new GLStateScope(gl);
-
- gl.Viewport(0, 0, (uint)width, (uint)height);
- gl.Disable(EnableCap.ScissorTest);
- gl.ClearDepth(1.0);
- gl.DepthMask(true);
- gl.Clear(RetailViewportClearMask);
-
- gl.Enable(EnableCap.DepthTest);
- gl.DepthFunc(DepthFunction.Less);
- gl.Enable(EnableCap.CullFace);
- gl.CullFace(TriangleFace.Back);
- gl.FrontFace(FrontFaceDirection.Ccw);
- gl.Disable(EnableCap.Blend);
-
- DrawScene();
- }
-
///
- /// The RHI arm. Every capability the GL arm sets by hand is baked into the
- /// dispatcher's pipelines, and the pass sets its own full-attachment
- /// viewport, so what remains is the pass itself: a backbuffer pass at the
- /// world's sample count that clears colour and depth (see
- /// for why re-clearing colour is
- /// exact) and is published as the scope's for the span of the draw.
+ /// Every capability the raw-GL arm used to set by hand (deleted at
+ /// Campaign V slice V11) is baked into the dispatcher's pipelines, and the
+ /// pass sets its own full-attachment viewport, so what remains is the
+ /// pass itself: a backbuffer pass at the world's sample count that clears
+ /// colour and depth (see for why
+ /// re-clearing colour is exact) and is published as the scope's for the
+ /// span of the draw.
///
private void DrawRhi()
{
- IWorldPassScope scope = _scope
- ?? throw new InvalidOperationException(
- "Portal space reached its RHI arm with no world pass scope.");
- IGpuFrame frame = (_frames ?? throw new InvalidOperationException(
- "Portal space reached its RHI arm with no frame source."))
- .CurrentFrame
+ IGpuFrame frame = _frames.CurrentFrame
?? throw new InvalidOperationException(
"Portal space requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
@@ -387,11 +332,11 @@ public sealed class PortalTunnelPresentation : IDisposable
GpuPassDescription.BackbufferClear(
"portal-space",
RetailPortalSpaceClearColor,
- scope.SampleCount));
+ _scope.SampleCount));
// Published AFTER the pass opens and BEFORE the light upload: publishing
// resets the frame-global sections, and the distant light installed below
// is the one this scene wants rather than the world's.
- using IDisposable publication = scope.Publish(encoder);
+ using IDisposable publication = _scope.Publish(encoder);
DrawScene();
}
diff --git a/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs b/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs
index 7670d7aa..97db5e00 100644
--- a/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs
+++ b/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs
@@ -4,7 +4,6 @@ using AcDream.App.Rendering.Wb;
using AcDream.App.UI;
using AcDream.Core.Lighting;
using AcDream.Core.World;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -41,41 +40,31 @@ internal interface IPrivateEntityViewportCamera : ICamera
/// nothing depends on inheritance and the GL and Vulkan backends agree about
/// what Target: null means.
///
-/// The DRAW inside the pass is still raw GL: WbDrawDispatcher keeps
-/// its GL arm through to V10 (§5.5.6), so this renderer opens an RHI pass and
-/// then lets a raw-GL renderer record into the framebuffer that pass bound. The
-/// surrounding is what restores the previous
-/// framebuffer, viewport and capability state afterwards, exactly as before.
+/// The GL arm this renderer used to open an RHI pass and delegate a
+/// raw-GL WbDrawDispatcher into (through V10, §5.5.6) was deleted at
+/// Campaign V slice V11: WbDrawDispatcher now records into the pass
+/// this renderer publishes on both call sites the same way.
///
-internal sealed unsafe class PrivateEntityViewportRenderer :
+internal sealed class PrivateEntityViewportRenderer :
IUiViewportRenderer,
IDisposable
{
private const uint PrivateLandblockId = 0u;
- ///
- /// The GL arm's context, or null on a backend that has none. Campaign V
- /// slice V6l: both retained-UI viewports render on either arm now.
- ///
- private readonly GL? _glContext;
-
- private GL RequireGl => _glContext
- ?? throw new InvalidOperationException(
- $"The {_diagnosticName} reached its GL arm on a backend with no GL context.");
-
private readonly IGpuDevice _device;
private readonly ICurrentGpuFrameSource _frames;
///
- /// The world pass scope, or null on the GL arm.
+ /// The world pass scope this renderer's own pass publishes into for the
+ /// span of the draw. WbDrawDispatcher borrows its pass from the
+ /// scope rather than opening one, so this renderer — which opens a pass of
+ /// its own to draw into an offscreen target — has to publish it there.
///
- /// Slice V6l. WbDrawDispatcher's RHI arm borrows its pass from
- /// the scope rather than opening one, so a viewport that opens a pass of its
- /// own has to publish it there for the duration of the draw. On the GL arm
- /// the dispatcher records against whatever framebuffer is bound, which is
- /// what the pass bound, so nothing is published.
+ /// The raw-GL arm this used to be optional for (the GL dispatcher
+ /// recorded against whatever framebuffer was already bound, needing no
+ /// publication) was deleted at Campaign V slice V11.
///
- private readonly IWorldPassScope? _scope;
+ private readonly IWorldPassScope _scope;
private readonly WbDrawDispatcher _dispatcher;
private readonly SceneLightingUboBinding _lightUbo;
@@ -96,8 +85,7 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
private SyntheticEntityMeshReferenceOwner? _meshReferences;
public PrivateEntityViewportRenderer(
- GL? gl,
- IWorldPassScope? scope,
+ IWorldPassScope scope,
IGpuDevice device,
ICurrentGpuFrameSource frames,
WbDrawDispatcher dispatcher,
@@ -110,15 +98,10 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
{
if (renderId == 0u)
throw new ArgumentOutOfRangeException(nameof(renderId));
- if (gl is null && scope is null)
- {
- throw new ArgumentNullException(
- nameof(scope),
- "A backend without a GL context must publish a world pass scope for the viewport to draw into.");
- }
- _glContext = gl;
- _scope = scope;
+ _scope = scope ?? throw new ArgumentNullException(
+ nameof(scope),
+ "The viewport must publish a world pass scope to draw into.");
_device = device ?? throw new ArgumentNullException(nameof(device));
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
@@ -136,10 +119,11 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
}
///
- /// A GL framebuffer's colour texture samples bottom-up; a Vulkan render
- /// target's does not. See .
+ /// A GL framebuffer's colour texture used to sample bottom-up; that arm was
+ /// deleted at Campaign V slice V11, and a Vulkan render target's does not.
+ /// See .
///
- public bool TextureIsBottomUp => _glContext is not null;
+ public bool TextureIsBottomUp => false;
public void SetEntity(WorldEntity? entity)
{
@@ -222,14 +206,6 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
?? throw new InvalidOperationException(
$"The {_diagnosticName} requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
- // The pass's clear is not scissored on either backend, but GL's glClear
- // is: a doorway slice earlier in the frame can have left the scissor on,
- // and this target is not confined to it.
- using GLStateScope? glState = _glContext is { } scissorGl
- ? new GLStateScope(scissorGl)
- : null;
- _glContext?.Disable(EnableCap.ScissorTest);
-
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
{
Name = _diagnosticName,
@@ -246,31 +222,13 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
SampleCount = 1,
});
- // GL's BeginPass deliberately does not touch viewport or scissor, and
- // the renderer that draws inside this pass is still raw GL, so the
- // remaining state is set here exactly as it always was. On the RHI arm
- // every one of these is baked into the dispatcher's pipelines and the
- // pass sets its own full-attachment viewport, so there is nothing to do.
- if (_glContext is { } stateGl)
- {
- stateGl.Viewport(0, 0, (uint)width, (uint)height);
- stateGl.Enable(EnableCap.DepthTest);
- stateGl.DepthFunc(DepthFunction.Less);
- stateGl.Enable(EnableCap.CullFace);
- stateGl.CullFace(TriangleFace.Back);
- stateGl.FrontFace(FrontFaceDirection.Ccw);
- stateGl.Disable(EnableCap.Blend);
- }
-
- // Slice V6l: the dispatcher's RHI arm borrows its pass from the scope,
- // so this pass has to BE the scope's for the span of the draw. Published
- // after the world phase has closed its own, which is why it does not
- // nest; the sections it resets are republished by UploadCreatureLight
+ // The dispatcher borrows its pass from the scope, so this pass has to
+ // BE the scope's for the span of the draw. Published after the world
+ // phase has closed its own, which is why it does not nest; the
+ // sections it resets are republished by UploadCreatureLight
// immediately below, which is the private lighting this view wants
// rather than the world's.
- using IDisposable? publication = _glContext is null
- ? _scope!.Publish(encoder)
- : null;
+ using IDisposable publication = _scope.Publish(encoder);
UploadCreatureLight();
diff --git a/src/AcDream.App/Rendering/RenderBootstrap.cs b/src/AcDream.App/Rendering/RenderBootstrap.cs
deleted file mode 100644
index 6636a4f5..00000000
--- a/src/AcDream.App/Rendering/RenderBootstrap.cs
+++ /dev/null
@@ -1,310 +0,0 @@
-using System.Collections.Concurrent;
-using AcDream.Content;
-using DatReaderWriter;
-using Microsoft.Extensions.Logging.Abstractions;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering;
-
-///
-/// The subset of the production render stack that the UI Studio needs:
-/// GL + dats + UiHost + the WB mesh pipeline. Constructed from the same
-/// classes and the same order as , minus
-/// terrain / sky / physics / streaming.
-///
-public sealed record RenderStack(
- GL Gl,
- IDatReaderWriter Dats,
- string ShaderDir,
- Wb.BindlessSupport Bindless,
- TextureCache TextureCache,
- Shader MeshShader,
- Wb.WbMeshAdapter MeshAdapter,
- Wb.EntitySpawnAdapter EntitySpawnAdapter,
- Wb.WbDrawDispatcher DrawDispatcher,
- SceneLightingUboBinding LightingUbo,
- AcDream.App.UI.UiHost UiHost,
- AcDream.App.UI.UiDatFont? VitalsDatFont,
- AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable
-{
- internal GpuFrameFlightController FrameFlights { get; init; } = null!;
-
- ///
- /// Campaign V slice V4a: the studio's own RHI device (mirrors
- /// 's production one — the studio
- /// composes its own render stack independently of GameWindow).
- ///
- internal AcDream.App.Rendering.Gpu.IGpuDevice GpuDevice { get; init; } = null!;
-
- ///
- /// Drives /IGpuFrame.End
- /// once per / pair and
- /// exposes the open frame to 's .
- ///
- internal GpuDeviceFrameLifetime FrameLifetime { get; init; } = null!;
-
- private ResourceShutdownTransaction? _shutdown;
-
- internal void BeginFrame() => FrameLifetime.BeginFrame();
-
- internal void EndFrame() => FrameLifetime.EndFrame();
-
- /// Dispose the GL pieces this stack OWNS (everything created in
- /// ). + are caller-owned
- /// and NOT disposed here. Called once at studio teardown.
- public void Dispose()
- {
- _shutdown ??= new ResourceShutdownTransaction(
- new ResourceShutdownStage("submitted GPU work",
- [
- new("frame flight drain", FrameFlights.WaitForSubmittedWork),
- ]),
- new ResourceShutdownStage("draw frontend",
- [
- new("draw dispatcher", DrawDispatcher.Dispose),
- ]),
- new ResourceShutdownStage("mesh adapter",
- [
- new("mesh adapter", MeshAdapter.Dispose),
- ]),
- new ResourceShutdownStage("remaining render stack",
- [
- new("texture cache", TextureCache.Dispose),
- new("mesh shader", MeshShader.Dispose),
- new("lighting UBO", LightingUbo.Dispose),
- new("UI host", UiHost.Dispose),
- // GpuDevice's own Dispose routes every resource release through
- // FrameFlights as its retirement queue, so it must be disposed
- // before FrameFlights below (see GlGpuDevice.Dispose's comment).
- new("GPU device (RHI)", GpuDevice.Dispose),
- ]),
- new ResourceShutdownStage("frame flight owner",
- [
- new("frame flights", FrameFlights.Dispose),
- ]));
- _shutdown.CompleteOrThrow();
- }
-
- ///
- /// Resolves a sprite id (0x06xxxxxx) to a (GL handle, width, height) triple.
- /// Copied verbatim from GameWindow's ResolveChrome closure — it calls
- /// TextureCache.GetOrUploadRenderSurface(id, out w, out h).
- ///
- public (uint handle, int width, int height) ResolveChrome(uint spriteId)
- {
- uint t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h);
- return (t, w, h);
- }
-
- // ── Font cache (per-stack, keyed by FontDid) ─────────────────────────────
-
- ///
- /// Cache of loaded dat fonts keyed by FontDid (0x40000000-range).
- /// Populated lazily by . Thread-safe for
- /// concurrent reads from the studio render loop; writes happen only
- /// during the first load of each distinct FontDid.
- ///
- private readonly ConcurrentDictionary _fontCache = new();
-
- ///
- /// Lazily load and cache a dat font by its FontDid. Returns null (and
- /// caches null) when the Font DBObj is absent or has no foreground surface —
- /// callers fall back to the global font in that case.
- ///
- /// Pre-seeds (0x40000000) and
- /// (0x40000001) from the already-loaded instances
- /// to avoid a redundant upload on those two ids.
- ///
- public AcDream.App.UI.UiDatFont? ResolveDatFont(uint fontDid)
- {
- return _fontCache.GetOrAdd(fontDid, id =>
- AcDream.App.UI.UiDatFont.Load(Dats, TextureCache, id));
- }
-
- ///
- /// Pre-seeds the font cache from the two already-loaded font instances
- /// (VitalsDatFont = 0x40000000, LargeDatFont = 0x40000001) so that
- /// returns them without a redundant GL upload.
- /// Called once by after the stack is
- /// fully constructed.
- ///
- internal void SeedFontCache()
- {
- if (VitalsDatFont is not null)
- _fontCache.TryAdd(AcDream.App.UI.UiDatFont.DefaultFontId, VitalsDatFont);
- if (LargeDatFont is not null)
- _fontCache.TryAdd(0x40000001u, LargeDatFont);
- }
-}
-
-/// Options for .
-public sealed record RenderBootstrapOptions(
- AcDream.UI.Abstractions.Settings.QualitySettings Quality,
- string DiagnosticsDirectory);
-
-///
-/// Constructs the UI Studio's render stack from the production classes,
-/// in the same order as .
-///
-public static class RenderBootstrap
-{
- ///
- /// Build the studio's render stack. Throws
- /// (same message as GameWindow) if GL_ARB_bindless_texture or
- /// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory.
- ///
- public static RenderStack Create(
- GL gl,
- IDatReaderWriter dats,
- RenderBootstrapOptions opts)
- {
- // --- Bindless detection (GameWindow ~1701-1723) ---
- if (!Wb.BindlessSupport.TryCreate(gl, out var bindless)
- || bindless is null
- || !bindless.HasShaderDrawParameters(gl))
- {
- throw new NotSupportedException(
- "acdream requires GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters " +
- "(GL 4.3+ with bindless support). Your GPU/driver does not expose these extensions. " +
- "If this is unexpected, please file a bug report with your GPU vendor + driver version.");
- }
-
- // --- Shared infra (GameWindow ~1198, ~1211) ---
- string shaderDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
- var lightingUbo = new SceneLightingUboBinding(gl);
-
- // --- Mesh shader (GameWindow ~1769-1771) ---
- // Campaign V slice V6e: mesh_modern has needed common.glsl since V2 —
- // ACDREAM_UBO_SET appears in a layout qualifier and ACDREAM_SAMPLE_ARRAY
- // at the sample site, and without the preamble both are undeclared
- // identifiers, so this program has failed to link on the Studio path
- // since that slice. The world composition (WorldRenderComposition) has
- // always passed true; this is the same pair loaded the same way.
- var meshShader = new Shader(gl,
- Path.Combine(shaderDir, "mesh_modern.vert"),
- Path.Combine(shaderDir, "mesh_modern.frag"),
- includeCommonPreamble: true);
-
- // --- TextureCache (GameWindow ~1774) ---
- var frameFlights = new GpuFrameFlightController(gl);
- // Campaign V slice V4a: the studio composes its own RHI device
- // independently of GameWindow/HostInputCameraComposition, mirroring
- // that composition's construction (gl + frame flights + shaders dir).
- Gpu.IGpuDevice gpuDevice = new Gpu.Gl.GlGpuDevice(gl, frameFlights, shaderDir);
- var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice);
- var textureCache = new TextureCache(
- gl,
- gpuDevice,
- dats,
- bindless,
- frameFlights,
- opts.DiagnosticsDirectory);
-
- // --- AnimLoader (GameWindow ~1240) ---
- var animLoader = new AcDream.Content.Vfx.RetailAnimationLoader(dats);
-
- // --- WbMeshAdapter (GameWindow ~2286-2287) ---
- var wbLogger = NullLogger.Instance;
- var meshAdapter = Wb.WbMeshAdapter.CreateWithLiveDatPreparedAssets(
- gl,
- gpuDevice,
- dats,
- wbLogger,
- frameFlights);
-
- // --- SequencerFactory (GameWindow ~2306-2334) ---
- var capturedDats = dats;
- var capturedAnimLoader = animLoader;
- AcDream.Core.Physics.AnimationSequencer SequencerFactory(AcDream.Core.World.WorldEntity e)
- {
- if (capturedDats is not null && capturedAnimLoader is not null)
- {
- var setup = capturedDats.Get(e.SourceGfxObjOrSetupId);
- if (setup is not null)
- {
- uint mtableId = (uint)setup.DefaultMotionTable;
- if (mtableId != 0)
- {
- var mtable = capturedDats.Get(mtableId);
- if (mtable is not null)
- return new AcDream.Core.Physics.AnimationSequencer(
- setup, mtable, capturedAnimLoader);
- }
- // Setup exists but no motion table — no-op sequencer.
- return new AcDream.Core.Physics.AnimationSequencer(
- setup,
- new DatReaderWriter.DBObjs.MotionTable(),
- capturedAnimLoader);
- }
- }
- // Complete fallback: empty setup + empty motion table + null loader.
- return new AcDream.Core.Physics.AnimationSequencer(
- new DatReaderWriter.DBObjs.Setup(),
- new DatReaderWriter.DBObjs.MotionTable(),
- new NullAnimLoader());
- }
-
- // --- EntitySpawnAdapter (GameWindow ~2335-2336) ---
- var entitySpawnAdapter = new Wb.EntitySpawnAdapter(
- textureCache, SequencerFactory, meshAdapter);
-
- // --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) ---
- var classificationCache = new Wb.EntityClassificationCache();
-
- // --- TranslucencyFadeManager (GameWindow — field initializer, new()) ---
- var translucencyFades = new AcDream.Core.Rendering.TranslucencyFadeManager();
-
- // --- WbDrawDispatcher (GameWindow ~2377-2381) ---
- var drawDispatcher = new Wb.WbDrawDispatcher(
- gl, meshShader, textureCache, meshAdapter, entitySpawnAdapter,
- bindless, classificationCache, translucencyFades);
- drawDispatcher.AlphaToCoverage = opts.Quality.AlphaToCoverage;
-
- // --- Vitals dat font (GameWindow ~1820-1822) ---
- var vitalsDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache);
-
- // --- Larger retail font (0x40000001, MaxCharHeight=18) for attribute row text.
- // The default font (0x40000000, 16px) renders the row names too small; the 18px
- // variant (confirmed in client_portal.dat 2026-06-26) matches the retail character
- // window list more closely (≈ icon height ≈ 24px target, 18px is best available).
- var largeDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache, 0x40000001u);
-
- // --- UiHost (GameWindow ~1790); pass null for debugFont (only used as
- // a fallback BitmapFont for the world-space HUD — not needed for the
- // UI Studio, and BitmapFont requires a system font byte array) ---
- var uiHost = new AcDream.App.UI.UiHost(gpuDevice, gpuFrameLifetime, shaderDir, defaultFont: null);
-
- var stack = new RenderStack(
- Gl: gl,
- Dats: dats,
- ShaderDir: shaderDir,
- Bindless: bindless,
- TextureCache: textureCache,
- MeshShader: meshShader,
- MeshAdapter: meshAdapter,
- EntitySpawnAdapter: entitySpawnAdapter,
- DrawDispatcher: drawDispatcher,
- LightingUbo: lightingUbo,
- UiHost: uiHost,
- VitalsDatFont: vitalsDatFont,
- LargeDatFont: largeDatFont)
- {
- FrameFlights = frameFlights,
- GpuDevice = gpuDevice,
- FrameLifetime = gpuFrameLifetime,
- };
-
- // Pre-seed the font cache with the two already-uploaded atlas instances
- // so ResolveDatFont(0x40000000) and ResolveDatFont(0x40000001) hit the cache
- // rather than re-uploading the same GL texture a second time.
- stack.SeedFontCache();
-
- return stack;
- }
-
- // NullAnimLoader mirrors GameWindow's private NullAnimLoader (GameWindow ~13327-13330).
- private sealed class NullAnimLoader : AcDream.Core.Physics.IAnimationLoader
- {
- public DatReaderWriter.DBObjs.Animation? LoadAnimation(uint id) => null;
- }
-}
diff --git a/src/AcDream.App/Rendering/RenderFrameGlStateController.cs b/src/AcDream.App/Rendering/RenderFrameGlStateController.cs
deleted file mode 100644
index b8a68c4e..00000000
--- a/src/AcDream.App/Rendering/RenderFrameGlStateController.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering;
-
-internal interface IRenderFrameGlState
-{
- void RestoreFrameDefaults();
-}
-
-internal interface IRenderFrameGlStateApi
-{
- void SetCapability(EnableCap capability, bool enabled);
-
- void ColorMask(bool red, bool green, bool blue, bool alpha);
-
- void StencilMask(uint mask);
-
- void DepthMask(bool enabled);
-
- void DepthFunc(DepthFunction function);
-
- void CullFace(TriangleFace face);
-
- void FrontFace(FrontFaceDirection direction);
-
- void UseProgram(uint program);
-
- void BindVertexArray(uint vertexArray);
-
- void BindBuffer(BufferTargetARB target, uint buffer);
-}
-
-internal sealed class SilkRenderFrameGlStateApi : IRenderFrameGlStateApi
-{
- private readonly GL _gl;
-
- public SilkRenderFrameGlStateApi(GL gl)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- }
-
- public void SetCapability(EnableCap capability, bool enabled)
- {
- if (enabled)
- _gl.Enable(capability);
- else
- _gl.Disable(capability);
- }
-
- public void ColorMask(bool red, bool green, bool blue, bool alpha) =>
- _gl.ColorMask(red, green, blue, alpha);
-
- public void StencilMask(uint mask) => _gl.StencilMask(mask);
-
- public void DepthMask(bool enabled) => _gl.DepthMask(enabled);
-
- public void DepthFunc(DepthFunction function) => _gl.DepthFunc(function);
-
- public void CullFace(TriangleFace face) => _gl.CullFace(face);
-
- public void FrontFace(FrontFaceDirection direction) => _gl.FrontFace(direction);
-
- public void UseProgram(uint program) => _gl.UseProgram(program);
-
- public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray);
-
- public void BindBuffer(BufferTargetARB target, uint buffer) =>
- _gl.BindBuffer(target, buffer);
-}
-
-///
-/// Restores the frame-global OpenGL convention shared by frame clear and
-/// exceptional world-pass rollback. This prevents a failed doorway scissor or
-/// portal depth mask from clipping/color-masking the next frame's clear.
-///
-internal sealed class RenderFrameGlStateController : IRenderFrameGlState
-{
- private readonly IRenderFrameGlStateApi _gl;
-
- public RenderFrameGlStateController(IRenderFrameGlStateApi gl)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- }
-
- public void RestoreFrameDefaults()
- {
- _gl.SetCapability(EnableCap.ScissorTest, enabled: false);
- _gl.SetCapability(EnableCap.StencilTest, enabled: false);
- _gl.SetCapability(EnableCap.Blend, enabled: false);
- _gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: false);
- for (int index = 0; index < ClipFrame.MaxPlanes; index++)
- {
- _gl.SetCapability(
- EnableCap.ClipDistance0 + index,
- enabled: false);
- }
-
- _gl.ColorMask(true, true, true, true);
- _gl.StencilMask(0xFF);
- _gl.DepthMask(true);
- _gl.DepthFunc(DepthFunction.Less);
- _gl.SetCapability(EnableCap.DepthTest, enabled: true);
- // Renderers that need face culling establish it locally. The shared
- // frame convention is culling off; SkyRenderer restores the state it
- // observed, so enabling it here would leak culling into later passes.
- _gl.SetCapability(EnableCap.CullFace, enabled: false);
- _gl.CullFace(TriangleFace.Back);
- _gl.FrontFace(FrontFaceDirection.CW);
- _gl.UseProgram(0);
- _gl.BindVertexArray(0);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
- }
-}
diff --git a/src/AcDream.App/Rendering/ResourceCleanupGroup.cs b/src/AcDream.App/Rendering/ResourceCleanupGroup.cs
index 1a3b6c5d..0153d719 100644
--- a/src/AcDream.App/Rendering/ResourceCleanupGroup.cs
+++ b/src/AcDream.App/Rendering/ResourceCleanupGroup.cs
@@ -2,6 +2,38 @@ namespace AcDream.App.Rendering;
using System.Runtime.ExceptionServices;
+internal interface IRetryableResourceCleanup
+{
+ bool IsCleanupComplete { get; }
+ void RetryCleanup();
+}
+
+///
+/// Thrown when a resource construction failed and the partial-construction
+/// rollback it triggered could not fully complete either. Backend-neutral —
+/// and ShaderProgramConstruction
+/// throw it on both the (deleted, Campaign V slice V11) GL construction path
+/// and the Vulkan one.
+///
+internal sealed class ResourceConstructionException : AggregateException,
+ IRetryableResourceCleanup
+{
+ private readonly IRetryableResourceCleanup _cleanup;
+
+ public ResourceConstructionException(
+ string message,
+ IRetryableResourceCleanup cleanup,
+ IEnumerable failures)
+ : base(message, failures)
+ {
+ _cleanup = cleanup ?? throw new ArgumentNullException(nameof(cleanup));
+ }
+
+ public bool IsCleanupComplete => _cleanup.IsCleanupComplete;
+
+ public void RetryCleanup() => _cleanup.RetryCleanup();
+}
+
///
/// Reverse-order, all-attempted cleanup owner used while a composite resource
/// is still under construction and after it becomes the aggregate owner.
@@ -84,7 +116,7 @@ internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup
}
catch (Exception cleanupFailure)
{
- throw new GlResourceConstructionException(
+ throw new ResourceConstructionException(
message,
this,
[constructionFailure, cleanupFailure]);
@@ -94,3 +126,88 @@ internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup
throw new InvalidOperationException("Unreachable construction rollback path.");
}
}
+
+///
+/// Lifetime root for cleanup work that could not finish before a throwing
+/// composition factory returned control. The original exception remains the
+/// retry owner; this ledger prevents it and its exact pending names from
+/// becoming local-only.
+///
+/// Backend-neutral, despite its former name
+/// (GlConstructionCleanupLedger, deleted at Campaign V slice V11): it
+/// walks any exception chain for —
+/// which implements on both
+/// backends — and does not itself touch GL.
+///
+internal sealed class ResourceConstructionCleanupLedger : IDisposable
+{
+ private readonly List _pending = [];
+ private bool _disposing;
+
+ public bool IsComplete => _pending.Count == 0;
+
+ public bool RetainFrom(Exception failure)
+ {
+ ArgumentNullException.ThrowIfNull(failure);
+ bool retained = false;
+ Visit(failure);
+ return retained;
+
+ void Visit(Exception current)
+ {
+ if (current is IRetryableResourceCleanup cleanup)
+ {
+ if (!cleanup.IsCleanupComplete && !_pending.Contains(cleanup))
+ _pending.Add(cleanup);
+ retained = true;
+ }
+
+ if (current is AggregateException aggregate)
+ {
+ foreach (Exception inner in aggregate.InnerExceptions)
+ Visit(inner);
+ }
+ else if (current.InnerException is { } inner)
+ {
+ Visit(inner);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposing || _pending.Count == 0)
+ return;
+
+ _disposing = true;
+ List? failures = null;
+ try
+ {
+ for (int i = _pending.Count - 1; i >= 0; i--)
+ {
+ IRetryableResourceCleanup cleanup = _pending[i];
+ try
+ {
+ cleanup.RetryCleanup();
+ if (cleanup.IsCleanupComplete)
+ _pending.RemoveAt(i);
+ }
+ catch (Exception failure)
+ {
+ (failures ??= []).Add(failure);
+ }
+ }
+ }
+ finally
+ {
+ _disposing = false;
+ }
+
+ if (failures is not null)
+ {
+ throw new AggregateException(
+ "One or more failed resource construction transactions remain pending.",
+ failures);
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
index 0a9966fc..f329013c 100644
--- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
+++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
@@ -5,7 +5,6 @@ using AcDream.App.Rendering.Wb;
using AcDream.Core.Rendering;
using AcDream.Core.Vfx;
using AcDream.Core.World;
-using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace AcDream.App.Rendering;
diff --git a/src/AcDream.App/Rendering/SamplerCache.cs b/src/AcDream.App/Rendering/SamplerCache.cs
deleted file mode 100644
index 91647bab..00000000
--- a/src/AcDream.App/Rendering/SamplerCache.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using System;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering;
-
-///
-/// Two persistent GL sampler objects (Repeat + ClampToEdge) created once
-/// per GL context. Renderers the appropriate
-/// one to a texture unit instead of mutating per-texture
-/// GL_TEXTURE_WRAP_S/T state — sampler state overrides the
-/// texture's own wrap parameters, so two renderers can share the same
-/// texture handle but sample it with different wrap modes safely.
-///
-///
-/// Ported from
-/// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132.
-/// Filter modes match 's upload defaults
-/// (Linear / Linear, no mipmaps) so binding either sampler doesn't
-/// change the visual filtering behavior — only the wrap behavior at
-/// UVs outside [0, 1].
-///
-///
-///
-/// Lifetime: created once at GL init, disposed with the GL context.
-/// Anything that binds a sampler MUST unbind it (BindSampler(unit, 0))
-/// before yielding to a renderer that doesn't use samplers, otherwise
-/// the bound sampler's wrap mode will silently override that renderer's
-/// per-texture wrap state.
-///
-///
-public sealed class SamplerCache : IDisposable
-{
- private readonly GL _gl;
- private readonly ResourceCleanupGroup _resources;
-
- /// Sampler with WrapS = WrapT = Repeat. The default for textures uploaded by .
- public uint Wrap { get; }
-
- /// Sampler with WrapS = WrapT = ClampToEdge. Used by sky meshes whose authored UVs are strictly in [0, 1] to avoid bilinear-filter bleed at seam edges.
- public uint Clamp { get; }
-
- public SamplerCache(GL gl)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- var resources = new ResourceCleanupGroup();
- uint wrap = 0;
- uint clamp = 0;
- try
- {
- wrap = GlResourceCommand.CreateName(
- _gl,
- "repeat sampler",
- _gl.GenSampler,
- _gl.DeleteSampler);
- uint ownedWrap = wrap;
- resources.Add(
- "repeat sampler",
- () => GlResourceCommand.Execute(
- _gl,
- $"delete repeat sampler {ownedWrap}",
- () => _gl.DeleteSampler(ownedWrap)));
- Configure(wrap, TextureWrapMode.Repeat, "repeat sampler");
-
- clamp = GlResourceCommand.CreateName(
- _gl,
- "clamp sampler",
- _gl.GenSampler,
- _gl.DeleteSampler);
- uint ownedClamp = clamp;
- resources.Add(
- "clamp sampler",
- () => GlResourceCommand.Execute(
- _gl,
- $"delete clamp sampler {ownedClamp}",
- () => _gl.DeleteSampler(ownedClamp)));
- Configure(clamp, TextureWrapMode.ClampToEdge, "clamp sampler");
- }
- catch (Exception constructionFailure)
- {
- resources.RollbackConstructionAndThrow(
- "SamplerCache construction failed and its sampler prefix did not cleanly roll back.",
- constructionFailure);
- }
-
- Wrap = wrap;
- Clamp = clamp;
- _resources = resources;
- }
-
- private void Configure(uint sampler, TextureWrapMode wrap, string name) =>
- GlResourceCommand.Execute(_gl, $"configure {name}", () =>
- {
- _gl.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrap);
- _gl.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrap);
- _gl.SamplerParameter(sampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear);
- _gl.SamplerParameter(sampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
- });
-
- public void Dispose()
- {
- _resources.RetryCleanup();
- }
-}
diff --git a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs
index 57e20d1e..61602a6a 100644
--- a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs
+++ b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs
@@ -1,61 +1,39 @@
using System;
-using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Wb;
using AcDream.Core.Lighting;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
///
-/// GL wrapper that owns the SceneLighting UBO buffer, updates its
-/// contents each frame, and keeps it bound at binding=1 so every
-/// shader sampling uLights[] / uFogColor / etc reads
-/// consistent data without per-shader re-upload.
+/// Publishes the SceneLighting UBO each frame so every shader sampling
+/// uLights[] / uFogColor / etc reads consistent data without
+/// per-shader re-upload.
///
-///
-/// Usage (r12 §13.2 + r13 §12.3):
-///
-/// - Instantiate once at startup, after the GL context exists.
-/// - Each frame, after , call with a freshly-built .
-/// - Shader programs that declare layout(std140, binding = 1) uniform SceneLighting { ... } automatically pick up the data.
-///
-///
+/// The raw-GL arm this used to have alongside it — a global uniform
+/// binding point kept live by a per-flight-slot buffer pool — was deleted at
+/// Campaign V slice V11. Vulkan has no such global binding point: a
+/// descriptor set is bound per draw, so the upload is a frame ring slice
+/// PUBLISHED on , and each world
+/// renderer binds it inside the pass after its own binds (plan §5.5.14 item
+/// 2). Every allocation within a frame is already distinct memory that lives
+/// until the frame retires, which is the property the deleted buffer pool
+/// existed to provide when the world, portal-space and paperdoll draws each
+/// upload different lighting.
///
public sealed unsafe class SceneLightingUboBinding : IDisposable
{
- private readonly GL? _gl;
- private readonly ICurrentGpuFrameSource? _frames;
- private readonly WorldFrameSections? _sections;
- private uint _ubo;
- private readonly List[] _buffersByFrame = [[], [], []];
- private int _frameSlot;
- private int _bufferCursor;
+ private readonly ICurrentGpuFrameSource _frames;
+ private readonly WorldFrameSections _sections;
private bool _frameStarted;
- private bool _disposed;
-
- internal int DynamicBufferCount => _buffersByFrame.Sum(buffers => buffers.Count);
-
- public SceneLightingUboBinding(GL gl)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- }
///
- /// Campaign V slice V6j: the RHI arm.
- ///
- /// GL keeps the block bound at a global uniform binding point and every
- /// shader inherits it. Vulkan has no such point — a descriptor set is bound
- /// per draw and the renderer's own binds select the scope this block has to
- /// land in — so the upload becomes a frame ring slice PUBLISHED on
- /// , and each world renderer
- /// binds it inside the pass after its own binds (plan §5.5.14 item 2).
- ///
- /// The per-flight-slot buffer pool disappears with it: every allocation
- /// within a frame is already distinct memory that lives until the frame
- /// retires, which is the property the pool existed to provide when the world,
- /// portal-space and paperdoll draws each upload different lighting.
+ /// The GL arm's per-flight-slot buffer pool this used to report on was
+ /// deleted at Campaign V slice V11: every allocation now comes from a ring
+ /// that lives until its frame retires, so there is no persistent
+ /// dynamic-buffer pool left to size.
///
+ internal int DynamicBufferCount => 0;
+
internal SceneLightingUboBinding(
ICurrentGpuFrameSource frames,
WorldFrameSections sections)
@@ -71,80 +49,20 @@ public sealed unsafe class SceneLightingUboBinding : IDisposable
///
public void BeginFrame(int frameSlot)
{
- if ((uint)frameSlot >= (uint)_buffersByFrame.Length)
- throw new ArgumentOutOfRangeException(nameof(frameSlot));
-
- _frameSlot = frameSlot;
- _bufferCursor = 0;
+ ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
_frameStarted = true;
}
- private void ActivateNextBuffer()
- {
- if (!_frameStarted)
- throw new InvalidOperationException("BeginFrame must be called before uploading scene lighting.");
- if (_gl is null)
- throw new InvalidOperationException("The RHI arm publishes a ring section rather than a buffer.");
-
- List buffers = _buffersByFrame[_frameSlot];
- if (_bufferCursor == buffers.Count)
- {
- uint buffer = TrackedGlResource.CreateBuffer(
- _gl,
- "SceneLighting frame UBO creation");
- try
- {
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- GLEnum.UniformBuffer,
- buffer,
- 0,
- SceneLightingUbo.SizeInBytes,
- GLEnum.DynamicDraw,
- "SceneLighting frame UBO allocation");
- buffers.Add(buffer);
- }
- catch
- {
- TrackedGlResource.DeleteBuffer(
- _gl,
- buffer,
- 0,
- "SceneLighting frame UBO rollback");
- throw;
- }
- }
-
- _ubo = buffers[_bufferCursor++];
- }
-
///
/// Push the current frame's UBO contents to the GPU. Cheap (576 bytes)
/// so fine to call unconditionally every frame.
///
public void Upload(SceneLightingUbo data)
- {
- if (_gl is null)
- {
- PublishSection(data);
- return;
- }
-
- ActivateNextBuffer();
- _gl.BindBuffer(BufferTargetARB.UniformBuffer, _ubo);
- _gl.BufferSubData(BufferTargetARB.UniformBuffer,
- (nint)0, (nuint)SceneLightingUbo.SizeInBytes, &data);
- _gl.BindBufferBase(BufferTargetARB.UniformBuffer,
- SceneLightingUbo.BindingPoint, _ubo);
- _gl.BindBuffer(BufferTargetARB.UniformBuffer, 0);
- }
-
- private void PublishSection(SceneLightingUbo data)
{
if (!_frameStarted)
throw new InvalidOperationException("BeginFrame must be called before uploading scene lighting.");
- IGpuFrame frame = _frames!.CurrentFrame
+ IGpuFrame frame = _frames.CurrentFrame
?? throw new InvalidOperationException(
"Scene lighting requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
GpuRingAllocation allocation = frame.AllocateRing(
@@ -152,30 +70,19 @@ public sealed unsafe class SceneLightingUboBinding : IDisposable
GpuRingUsage.Uniform);
new ReadOnlySpan(&data, SceneLightingUbo.SizeInBytes)
.CopyTo(allocation.Data);
- _sections!.SceneLighting = new GpuBufferSection(
+ _sections.SceneLighting = new GpuBufferSection(
allocation.Buffer,
allocation.OffsetBytes,
(uint)SceneLightingUbo.SizeInBytes);
}
+ ///
+ /// No-op: this binding owns no GPU resource of its own on the RHI arm — the
+ /// deleted GL arm's per-flight buffer pool was the only thing to release.
+ /// Kept as a method so callers that hold this behind an
+ /// reference (composition's acquisition scope) don't need a special case.
+ ///
public void Dispose()
{
- if (_disposed) return;
- if (_gl is not null)
- {
- foreach (List buffers in _buffersByFrame)
- {
- foreach (uint buffer in buffers)
- {
- TrackedGlResource.DeleteBuffer(
- _gl,
- buffer,
- SceneLightingUbo.SizeInBytes,
- "SceneLighting frame UBO disposal");
- }
- buffers.Clear();
- }
- }
- _disposed = true;
}
}
diff --git a/src/AcDream.App/Rendering/Shader.cs b/src/AcDream.App/Rendering/Shader.cs
deleted file mode 100644
index 2d3be01c..00000000
--- a/src/AcDream.App/Rendering/Shader.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System.Numerics;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering;
-
-public sealed class Shader : IDisposable
-{
- private readonly GL _gl;
- private readonly Dictionary _uniformLocations = new(StringComparer.Ordinal);
- public uint Program { get; private set; }
-
- public Shader(GL gl, string vertexPath, string fragmentPath)
- : this(gl, vertexPath, fragmentPath, includeCommonPreamble: false)
- {
- }
-
- ///
- /// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when
- /// is true, the text of
- /// Shaders/common.glsl — sitting alongside
- /// — is spliced into both sources right after their leading
- /// #version/#extension block. GL has no #include, so this
- /// is plain string concatenation at load time rather than a GLSL-level
- /// mechanism. Every existing two-argument-path caller is unaffected: the
- /// convenience constructor above always passes false.
- ///
- public Shader(GL gl, string vertexPath, string fragmentPath, bool includeCommonPreamble)
- {
- _gl = gl;
- string vertexSource = File.ReadAllText(vertexPath);
- string fragmentSource = File.ReadAllText(fragmentPath);
- if (includeCommonPreamble)
- {
- string? directory = Path.GetDirectoryName(vertexPath);
- string commonPath = directory is null
- ? "common.glsl"
- : Path.Combine(directory, "common.glsl");
- string commonSource = File.ReadAllText(commonPath);
- vertexSource = InjectPreamble(vertexSource, commonSource);
- fragmentSource = InjectPreamble(fragmentSource, commonSource);
- }
- Program = ShaderProgramConstruction.Build(
- new GlShaderProgramBuildApi(gl),
- vertexSource,
- fragmentSource);
- }
-
- ///
- /// Inserts right after the shader's leading
- /// #version/#extension/blank-line block. GLSL requires
- /// #version to be the very first statement in the source, so the
- /// preamble cannot simply be prepended — it has to land after that block,
- /// before the first real declaration.
- ///
- /// Internal rather than private since slice V6d: GlGpuDevice.CreatePipeline
- /// loads RHI shader pairs itself and has to splice the same preamble the
- /// same way. Two implementations of "where does the preamble go" is exactly
- /// the kind of drift that shows up as one shader silently missing a binding.
- ///
- internal static string InjectPreamble(string source, string preamble)
- {
- int insertAt = 0;
- int lineStart = 0;
- while (lineStart < source.Length)
- {
- int lineEnd = source.IndexOf('\n', lineStart);
- if (lineEnd < 0)
- lineEnd = source.Length;
- string trimmed = source[lineStart..lineEnd].TrimStart();
- bool isLeadingLine =
- trimmed.Length == 0
- || trimmed.StartsWith("#version", StringComparison.Ordinal)
- || trimmed.StartsWith("#extension", StringComparison.Ordinal);
- if (!isLeadingLine)
- break;
-
- insertAt = lineEnd < source.Length ? lineEnd + 1 : lineEnd;
- lineStart = lineEnd + 1;
- }
-
- return string.Concat(source.AsSpan(0, insertAt), preamble, "\n", source.AsSpan(insertAt));
- }
-
- public void Use() => _gl.UseProgram(Program);
-
- public unsafe void SetMatrix4(string name, Matrix4x4 m)
- {
- int loc = GetUniformLocation(name);
- _gl.UniformMatrix4(loc, 1, false, (float*)&m);
- }
-
- public void SetInt(string name, int value)
- {
- int loc = GetUniformLocation(name);
- _gl.Uniform1(loc, value);
- }
-
- public void SetFloat(string name, float value)
- {
- int loc = GetUniformLocation(name);
- _gl.Uniform1(loc, value);
- }
-
- public void SetVec3(string name, Vector3 v)
- {
- int loc = GetUniformLocation(name);
- _gl.Uniform3(loc, v.X, v.Y, v.Z);
- }
-
- public void SetVec2(string name, Vector2 v)
- {
- int loc = GetUniformLocation(name);
- _gl.Uniform2(loc, v.X, v.Y);
- }
-
- public void SetVec4(string name, Vector4 v)
- {
- int loc = GetUniformLocation(name);
- _gl.Uniform4(loc, v.X, v.Y, v.Z, v.W);
- }
-
- private int GetUniformLocation(string name)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(name);
- if (_uniformLocations.TryGetValue(name, out int location))
- return location;
-
- location = _gl.GetUniformLocation(Program, name);
- _uniformLocations.Add(name, location);
- return location;
- }
-
- public void Dispose()
- {
- uint program = Program;
- if (program == 0)
- return;
-
- _uniformLocations.Clear();
- GlResourceCommand.DeleteProgram(_gl, program, $"delete Shader program {program}");
- Program = 0;
- }
-}
diff --git a/src/AcDream.App/Rendering/ShaderProgramConstruction.cs b/src/AcDream.App/Rendering/ShaderProgramConstruction.cs
deleted file mode 100644
index a01f81ab..00000000
--- a/src/AcDream.App/Rendering/ShaderProgramConstruction.cs
+++ /dev/null
@@ -1,325 +0,0 @@
-using System.Runtime.ExceptionServices;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering;
-
-internal interface IShaderProgramBuildApi
-{
- uint CreateShader(ShaderType type);
- void ShaderSource(uint shader, string source);
- void CompileShader(uint shader);
- int GetShaderCompileStatus(uint shader);
- string GetShaderInfoLog(uint shader);
- uint CreateProgram();
- void AttachShader(uint program, uint shader);
- void LinkProgram(uint program);
- int GetProgramLinkStatus(uint program);
- string GetProgramInfoLog(uint program);
- void DetachShader(uint program, uint shader);
- void DeleteShader(uint shader);
- void DeleteProgram(uint program);
-}
-
-internal sealed class GlShaderProgramBuildApi(GL gl) : IShaderProgramBuildApi
-{
- public uint CreateShader(ShaderType type) =>
- GlResourceCommand.CreateName(
- gl,
- $"{type} shader",
- () => gl.CreateShader(type),
- gl.DeleteShader);
-
- public void ShaderSource(uint shader, string source) =>
- GlResourceCommand.Execute(
- gl,
- $"upload shader source {shader}",
- () => gl.ShaderSource(shader, source));
-
- public void CompileShader(uint shader) =>
- GlResourceCommand.Execute(
- gl,
- $"compile shader {shader}",
- () => gl.CompileShader(shader));
-
- public void GetShader(uint shader, ShaderParameterName name, out int value) =>
- gl.GetShader(shader, name, out value);
-
- public int GetShaderCompileStatus(uint shader)
- {
- return GlResourceCommand.Execute(
- gl,
- $"read shader {shader} compile status",
- () =>
- {
- gl.GetShader(shader, ShaderParameterName.CompileStatus, out int value);
- return value;
- });
- }
-
- public string GetShaderInfoLog(uint shader) =>
- GlResourceCommand.Execute(
- gl,
- $"read shader {shader} info log",
- () => gl.GetShaderInfoLog(shader));
-
- public uint CreateProgram() =>
- GlResourceCommand.CreateName(
- gl,
- "shader program",
- gl.CreateProgram,
- gl.DeleteProgram);
-
- public void AttachShader(uint program, uint shader) =>
- GlResourceCommand.Execute(
- gl,
- $"attach shader {shader} to program {program}",
- () => gl.AttachShader(program, shader));
-
- public void LinkProgram(uint program) =>
- GlResourceCommand.Execute(
- gl,
- $"link shader program {program}",
- () => gl.LinkProgram(program));
-
- public int GetProgramLinkStatus(uint program)
- {
- return GlResourceCommand.Execute(
- gl,
- $"read shader program {program} link status",
- () =>
- {
- gl.GetProgram(program, ProgramPropertyARB.LinkStatus, out int value);
- return value;
- });
- }
-
- public string GetProgramInfoLog(uint program) =>
- GlResourceCommand.Execute(
- gl,
- $"read shader program {program} info log",
- () => gl.GetProgramInfoLog(program));
-
- public void DetachShader(uint program, uint shader) =>
- GlResourceCommand.Execute(
- gl,
- $"detach shader {shader} from program {program}",
- () => gl.DetachShader(program, shader));
-
- public void DeleteShader(uint shader) =>
- GlResourceCommand.DeleteShader(gl, shader, $"delete shader {shader}");
-
- public void DeleteProgram(uint program) =>
- GlResourceCommand.DeleteProgram(gl, program, $"delete shader program {program}");
-}
-
-internal sealed class ShaderProgramCleanup(IShaderProgramBuildApi api)
- : IRetryableResourceCleanup
-{
- public uint Vertex { get; set; }
- public uint Fragment { get; set; }
- public uint Program { get; set; }
- public bool VertexAttached { get; set; }
- public bool FragmentAttached { get; set; }
-
- public bool IsCleanupComplete => Vertex == 0 && Fragment == 0 && Program == 0;
-
- public uint TransferProgram()
- {
- if (Vertex != 0 || Fragment != 0 || Program == 0)
- throw new InvalidOperationException("The linked program is not ready for ownership transfer.");
- uint program = Program;
- Program = 0;
- return program;
- }
-
- public List Release(bool includeProgram)
- {
- var failures = new List();
- Try(
- () =>
- {
- if (Program != 0 && Vertex != 0 && VertexAttached)
- {
- api.DetachShader(Program, Vertex);
- VertexAttached = false;
- }
- },
- failures);
- Try(
- () =>
- {
- if (Program != 0 && Fragment != 0 && FragmentAttached)
- {
- api.DetachShader(Program, Fragment);
- FragmentAttached = false;
- }
- },
- failures);
- Try(
- () =>
- {
- if (Vertex != 0)
- {
- api.DeleteShader(Vertex);
- Vertex = 0;
- VertexAttached = false;
- }
- },
- failures);
- Try(
- () =>
- {
- if (Fragment != 0)
- {
- api.DeleteShader(Fragment);
- Fragment = 0;
- FragmentAttached = false;
- }
- },
- failures);
- if (includeProgram)
- {
- Try(
- () =>
- {
- if (Program != 0)
- {
- api.DeleteProgram(Program);
- Program = 0;
- VertexAttached = false;
- FragmentAttached = false;
- }
- },
- failures);
- }
-
- return failures;
- }
-
- public List ReleaseProgramOnly()
- {
- var failures = new List();
- Try(
- () =>
- {
- if (Program != 0)
- {
- api.DeleteProgram(Program);
- Program = 0;
- VertexAttached = false;
- FragmentAttached = false;
- }
- },
- failures);
- return failures;
- }
-
- public void RetryCleanup()
- {
- List failures = Release(includeProgram: true);
- if (!IsCleanupComplete)
- {
- throw new AggregateException(
- "Shader program cleanup remains incomplete.",
- failures.Count == 0
- ? [new InvalidOperationException("Shader cleanup retained names without reporting an operation failure.")]
- : failures);
- }
- }
-
- private static void Try(Action operation, List failures)
- {
- try
- {
- operation();
- }
- catch (Exception failure)
- {
- failures.Add(failure);
- }
- }
-}
-
-internal static class ShaderProgramConstruction
-{
- public static uint Build(
- IShaderProgramBuildApi api,
- string vertexSource,
- string fragmentSource)
- {
- ArgumentNullException.ThrowIfNull(api);
- ArgumentNullException.ThrowIfNull(vertexSource);
- ArgumentNullException.ThrowIfNull(fragmentSource);
-
- var cleanup = new ShaderProgramCleanup(api);
- Exception? constructionFailure = null;
-
- try
- {
- cleanup.Vertex = CreateShader(api, ShaderType.VertexShader);
- Compile(api, cleanup.Vertex, ShaderType.VertexShader, vertexSource);
- cleanup.Fragment = CreateShader(api, ShaderType.FragmentShader);
- Compile(api, cleanup.Fragment, ShaderType.FragmentShader, fragmentSource);
- cleanup.Program = api.CreateProgram();
- if (cleanup.Program == 0)
- throw new InvalidOperationException("OpenGL returned no shader program name.");
-
- api.AttachShader(cleanup.Program, cleanup.Vertex);
- cleanup.VertexAttached = true;
- api.AttachShader(cleanup.Program, cleanup.Fragment);
- cleanup.FragmentAttached = true;
- api.LinkProgram(cleanup.Program);
- if (api.GetProgramLinkStatus(cleanup.Program) == 0)
- throw new InvalidOperationException(
- "program link failed: " + api.GetProgramInfoLog(cleanup.Program));
- }
- catch (Exception failure)
- {
- constructionFailure = failure;
- }
-
- List cleanupFailures = cleanup.Release(includeProgram: false);
-
- if (constructionFailure is null && cleanupFailures.Count == 0)
- return cleanup.TransferProgram();
-
- cleanupFailures.AddRange(cleanup.ReleaseProgramOnly());
-
- if (constructionFailure is not null && cleanupFailures.Count == 0)
- ExceptionDispatchInfo.Capture(constructionFailure).Throw();
-
- var failures = new List(cleanupFailures.Count + 1);
- if (constructionFailure is not null)
- failures.Add(constructionFailure);
- failures.AddRange(cleanupFailures);
- const string message =
- "Shader program construction failed and one or more temporary OpenGL names could not be cleanly released.";
- if (!cleanup.IsCleanupComplete)
- throw new GlResourceConstructionException(message, cleanup, failures);
- throw new AggregateException(message, failures);
- }
-
- private static uint CreateShader(
- IShaderProgramBuildApi api,
- ShaderType type)
- {
- uint shader = api.CreateShader(type);
- if (shader == 0)
- throw new InvalidOperationException($"OpenGL returned no {type} name.");
- return shader;
- }
-
- private static void Compile(
- IShaderProgramBuildApi api,
- uint shader,
- ShaderType type,
- string source)
- {
- api.ShaderSource(shader, source);
- api.CompileShader(shader);
- if (api.GetShaderCompileStatus(shader) == 0)
- throw new InvalidOperationException(
- $"{type} compile failed: " + api.GetShaderInfoLog(shader));
- }
-}
diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs
index 058beb7a..38cedc8f 100644
--- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs
+++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs
@@ -9,7 +9,6 @@ using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Sky;
@@ -43,46 +42,11 @@ namespace AcDream.App.Rendering.Sky;
/// measured clockwise from north.
///
///
-public sealed unsafe partial class SkyRenderer : IDisposable
+public sealed partial class SkyRenderer : IDisposable
{
- private readonly GL? _gl;
private readonly IDatReaderWriter _dats;
- private readonly Shader? _shader;
private readonly TextureCache _textures;
- private readonly SamplerCache? _samplers;
- // Campaign V slice V6e. Two GL objects replace what used to be a run of
- // glUniform* calls and a texture/sampler binding on unit 0:
- //
- // _paramsUbo — the std140 SkyParams block both stages declare.
- // _uTextureIndexALoc — the push-constant-named uniform carrying the slot.
- //
- // The wrap mode is what makes the (texture, wrap) pairing necessary. A
- // bindless handle BAKES its sampler state, so the per-submesh
- // Repeat-vs-ClampToEdge choice that used to be a glBindSampler call has to be
- // a different handle — which is also exactly how the Vulkan table works,
- // where an entry is a combined image sampler. ManagedGLTextureArray has
- // interned wrap/clamp handle pairs the same way since the world path went
- // bindless.
- //
- // Campaign V slice V6k retired the interim per-renderer GlBindlessHandleTable
- // — the last one in the tree — for the device's own retirement-gated table,
- // exactly as V4t did for the other four world renderers. The handles are
- // still minted here, because the sky is the one world path whose textures it
- // produces itself; only the slot allocator moved.
- private readonly Wb.BindlessSupport? _bindless;
-
- ///
- /// Campaign V slice V6k: the GL arm's slot allocator. The sky mints its own
- /// resident handles — it is the one world path that does — so it registers
- /// them into the device's table through V4t's world-handle seam instead of
- /// keeping the last private GlBindlessHandleTable in the tree.
- ///
- private readonly AcDream.App.Rendering.Gpu.Gl.GlGpuDevice? _glDevice;
-
- private readonly Dictionary<(uint Texture, bool Repeat), ulong> _handleByTextureAndWrap = new();
- private uint _paramsUbo;
- private int _uTextureIndexALoc = -1;
private SkyParams _params;
// Lazily-built GPU resources per sky-GfxObj.
@@ -125,40 +89,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable
public float Near { get; set; } = 0.1f;
public float Far { get; set; } = 1_000_000f;
- internal SkyRenderer(
- GL gl,
- IDatReaderWriter dats,
- Shader shader,
- TextureCache textures,
- SamplerCache samplers,
- Wb.BindlessSupport bindless,
- AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _dats = dats ?? throw new ArgumentNullException(nameof(dats));
- _shader = shader ?? throw new ArgumentNullException(nameof(shader));
- _textures = textures ?? throw new ArgumentNullException(nameof(textures));
- _samplers = samplers ?? throw new ArgumentNullException(nameof(samplers));
- _bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
- _glDevice = device ?? throw new ArgumentNullException(nameof(device));
-
- _uTextureIndexALoc = _gl.GetUniformLocation(_shader.Program, "uTextureIndexA");
-
- _paramsUbo = Wb.TrackedGlResource.CreateBuffer(_gl, "sky params UBO creation");
- // Fixed size: SkyParams is a 256-byte std140 struct, so the buffer never
- // grows and every write is one whole-struct BufferSubData.
- _gl.BindBuffer(BufferTargetARB.UniformBuffer, _paramsUbo);
- Wb.TrackedGlResource.AllocateBufferStorage(
- _gl,
- BufferTargetARB.UniformBuffer,
- _paramsUbo,
- 0,
- SkyParams.SizeInBytes,
- BufferUsageARB.DynamicDraw,
- "allocating sky params UBO");
- _gl.BindBuffer(BufferTargetARB.UniformBuffer, 0);
- }
-
///
/// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds —
/// every SkyObject with Properties & 0x04 == 0).
@@ -265,7 +195,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable
skyView.M42 = 0f;
skyView.M43 = 0f;
- _shader?.Use();
// Campaign V slice V6e: the values below used to be individual
// glUniform* calls. They now populate the std140 SkyParams block, which
// is uploaded once per submesh right before its draw — the same cadence
@@ -283,34 +212,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable
_params.SunDir =
AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(keyframe);
- bool wasCullFace = false;
- if (_gl is { } gl)
- {
- gl.BindBufferBase(
- BufferTargetARB.UniformBuffer,
- AcDream.App.Rendering.Gpu.GpuBindingModel.UniformSkyParams,
- _paramsUbo);
-
- // Save + override GL state.
- gl.DepthMask(false);
- gl.Disable(EnableCap.DepthTest);
- // Save + disable CullFace for the sky pass; restore at the end.
- // Mirrors TextRenderer.cs's save/restore pattern. Without this the
- // sky pass left CullFace disabled regardless of its prior state,
- // which is benign today (the global convention in this codebase is
- // off and subsequent renderers manage their own CullFace) but
- // would break the moment any future caller assumes back-face
- // culling stays on across the sky pass.
- wasCullFace = gl.IsEnabled(EnableCap.CullFace);
- gl.Disable(EnableCap.CullFace);
- gl.Enable(EnableCap.Blend);
- // Default blend — overridden per-submesh inside the inner loop.
- // Additive surfaces (sun/moon/stars via SurfaceType.Additive =
- // 0x10000) get GL_SRC_ALPHA / GL_ONE; alpha-blended (clouds, dome
- // with Alpha flag) get GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA.
- gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
- }
-
// Look up the keyframe's override list so we can apply
// SkyObjReplace (r12 §2.3): per-keyframe GfxObj swaps + rotation
// override + transparency fade + luminosity cap.
@@ -434,14 +335,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable
// mesh pipeline where Surface flags dictate state. On the RHI
// arm the same two blend functions are two pipelines, because
// Vulkan bakes blend rather than making it dynamic.
- if (_gl is { } blendGl)
- {
- if (sub.IsAdditive)
- blendGl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
- else
- blendGl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
- }
-
// Emissive source picks the surface's authored Luminosity by
// default; the per-keyframe replace data can OVERRIDE
// (rep.Luminosity > 0) or CAP (rep.MaxBright). This matches
@@ -532,46 +425,9 @@ public sealed unsafe partial class SkyRenderer : IDisposable
|| obj.TexVelocityX != 0f
|| obj.TexVelocityY != 0f;
uint slot = TextureTableSlot(sub.SurfaceId, needsRepeat);
-
- if (_gl is { } drawGl)
- {
- drawGl.ProgramUniform1(
- _shader!.Program,
- _uTextureIndexALoc,
- slot);
-
- UploadParams();
- FlushAndBindTextureTable();
-
- drawGl.BindVertexArray(sub.Vao);
- drawGl.DrawElements(PrimitiveType.Triangles,
- (uint)sub.IndexCount,
- DrawElementsType.UnsignedInt,
- (void*)0);
- }
- else
- {
- DrawSubMeshRhi(sub, slot);
- }
+ DrawSubMeshRhi(sub, slot);
}
}
-
- // Restore GL state expected by the rest of the pipeline.
- //
- // Slice V6e removed the glBindSampler(0, …) that used to happen here
- // and its matching unbind. The sky no longer touches texture unit 0 at
- // all — its wrap mode rides inside the bindless handle — so there is
- // nothing left to leak onto the next renderer's unit 0. That unbind was
- // load-bearing precisely because the binding was global state; a table
- // slot is not.
- if (_gl is { } restoreGl)
- {
- restoreGl.Disable(EnableCap.Blend);
- restoreGl.DepthMask(true);
- restoreGl.Enable(EnableCap.DepthTest);
- if (wasCullFace) restoreGl.Enable(EnableCap.CullFace);
- restoreGl.BindVertexArray(0);
- }
}
///
@@ -588,57 +444,8 @@ public sealed unsafe partial class SkyRenderer : IDisposable
/// handle table in the codebase — the sky's texture set is a fixed handful
/// per day group and does not churn.
///
- private uint TextureTableSlot(uint surfaceId, bool repeat)
- {
- if (_gl is null)
- return RhiTextureTableSlot(surfaceId, repeat);
-
- uint textureName = _textures.GetOrUpload(surfaceId);
- var key = (textureName, repeat);
- if (!_handleByTextureAndWrap.TryGetValue(key, out ulong handle))
- {
- handle = _bindless!.GetResidentHandle(
- textureName,
- repeat ? _samplers!.Wrap : _samplers!.Clamp);
- _handleByTextureAndWrap.Add(key, handle);
- }
-
- return _glDevice!.RegisterWorldTextureHandle(handle).Index;
- }
-
- ///
- /// Writes the current to its uniform buffer. Called
- /// immediately before each draw, which is the cadence the per-submesh
- /// glUniform* calls it replaces already had.
- ///
- private void UploadParams()
- {
- GL gl = _gl!;
- fixed (void* p = &_params)
- {
- gl.BindBuffer(BufferTargetARB.UniformBuffer, _paramsUbo);
- gl.BufferSubData(
- BufferTargetARB.UniformBuffer, 0, (nuint)SkyParams.SizeInBytes, p);
- }
- }
-
- ///
- /// Campaign V slice V6k: drains the device texture table's dirty runs and
- /// (re)binds it at
- /// .
- /// Mirrors ParticleRenderer.FlushAndBindTextureTable, including its
- /// reason for running per draw rather than per pass: a submesh partway
- /// through the sky stack can be the first to ask for a wrap mode.
- ///
- private void FlushAndBindTextureTable()
- {
- AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = _glDevice!;
- device.FlushTextureTable();
- _gl!.BindBufferBase(
- BufferTargetARB.ShaderStorageBuffer,
- AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
- device.TextureTableGlName);
- }
+ private uint TextureTableSlot(uint surfaceId, bool repeat) =>
+ RhiTextureTableSlot(surfaceId, repeat);
///
/// Find the entries for the
@@ -865,106 +672,9 @@ public sealed unsafe partial class SkyRenderer : IDisposable
}
}
- private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm)
- {
- if (_gl is null)
- return UploadSubMeshRhi(sm);
+ private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm) => UploadSubMeshRhi(sm);
- uint vao = _gl.GenVertexArray();
- _gl.BindVertexArray(vao);
-
- uint vbo = _gl.GenBuffer();
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
- fixed (void* p = sm.Vertices)
- _gl.BufferData(BufferTargetARB.ArrayBuffer,
- (nuint)(sm.Vertices.Length * sizeof(Vertex)), p, BufferUsageARB.StaticDraw);
-
- uint ebo = _gl.GenBuffer();
- _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo);
- fixed (void* p = sm.Indices)
- _gl.BufferData(BufferTargetARB.ElementArrayBuffer,
- (nuint)(sm.Indices.Length * sizeof(uint)), p, BufferUsageARB.StaticDraw);
-
- uint stride = (uint)sizeof(Vertex);
- _gl.EnableVertexAttribArray(0);
- _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
- _gl.EnableVertexAttribArray(1);
- _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
- _gl.EnableVertexAttribArray(2);
- _gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
-
- _gl.BindVertexArray(0);
-
- // Classify blend mode from the Surface's flags. Sun/moon/stars with
- // `SurfaceType.Additive = 0x10000` get GL_ONE / GL_ONE (their texture
- // has a black background and a bright body; additive makes the
- // background contribute nothing and the body glow on top of the sky).
- //
- // NOTE: earlier revision also treated `SurfaceType.Luminous = 0x40`
- // as additive, but that flag is present on the sky DOME itself and
- // on cloud sheets — turning those additive blew the whole sky to
- // white. `Luminous` means "self-illuminated / unshaded" in retail's
- // render pipeline, not "additive blend". Only the Additive bit
- // toggles the blend mode.
- bool isAdditive = sm.Translucency == TranslucencyKind.Additive;
-
- return new SubMeshGpu
- {
- Vao = vao,
- Vbo = vbo,
- Ebo = ebo,
- IndexCount = sm.Indices.Length,
- SurfaceId = sm.SurfaceId,
- IsAdditive = isAdditive,
- SurfLuminosity = sm.Luminosity,
- SurfDiffuse = sm.Diffuse,
- NeedsUvRepeat = sm.NeedsUvRepeat,
- SurfOpacity = sm.SurfOpacity,
- DisableFog = sm.DisableFog,
- };
- }
-
- public void Dispose()
- {
- if (_gl is null)
- {
- DisposeRhi();
- return;
- }
-
- foreach (var subs in _gpuByGfxObj.Values)
- {
- foreach (var sub in subs)
- {
- _gl.DeleteBuffer(sub.Vbo);
- _gl.DeleteBuffer(sub.Ebo);
- _gl.DeleteVertexArray(sub.Vao);
- }
- }
- _gpuByGfxObj.Clear();
-
- // Campaign V slice V6e. Residency first: a handle made resident pins
- // its texture's GPU address, and the textures themselves belong to
- // TextureCache, which outlives this renderer and disposes them itself.
- //
- // Slice V6k: the device's table entry is retired alongside it, in the
- // order V4t established — the table entry goes first, because the slot
- // is only recycled once the frames that could still read it retire,
- // whereas the handle stops being valid the instant it is non-resident.
- foreach (ulong handle in _handleByTextureAndWrap.Values)
- {
- _glDevice!.ReleaseWorldTextureHandle(handle);
- _bindless!.MakeNonResident(handle);
- }
- _handleByTextureAndWrap.Clear();
-
- if (_paramsUbo != 0)
- {
- Wb.TrackedGlResource.DeleteBuffer(
- _gl, _paramsUbo, SkyParams.SizeInBytes, "sky params UBO disposal");
- _paramsUbo = 0;
- }
- }
+ public void Dispose() => DisposeRhi();
///
/// Campaign V slice V6e: the CPU mirror of sky.{vert,frag}'s SkyParams
@@ -999,14 +709,11 @@ public sealed unsafe partial class SkyRenderer : IDisposable
private sealed class SubMeshGpu
{
- public uint Vao;
- public uint Vbo;
- public uint Ebo;
///
/// Campaign V slice V6k: the RHI arm's vertex source. The sky's meshes are
/// built once per GfxObj and never change, so each submesh owns a
/// device-local buffer pair rather than taking a ring slice per frame.
- /// Null on the GL arm, which uses .
+ /// The raw-GL arm's VAO/VBO/EBO names were deleted at slice V11.
///
public AcDream.App.Rendering.Gpu.IGpuBuffer? VertexBuffer;
public AcDream.App.Rendering.Gpu.IGpuBuffer? IndexBuffer;
diff --git a/src/AcDream.App/Rendering/TerrainAtlas.cs b/src/AcDream.App/Rendering/TerrainAtlas.cs
index 1b4286aa..64a73db6 100644
--- a/src/AcDream.App/Rendering/TerrainAtlas.cs
+++ b/src/AcDream.App/Rendering/TerrainAtlas.cs
@@ -1,13 +1,10 @@
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Gpu.Gl;
using AcDream.Core.Textures;
using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
-using Silk.NET.OpenGL;
using DatPixelFormat = DatReaderWriter.Enums.PixelFormat;
-using GLPixelFormat = Silk.NET.OpenGL.PixelFormat;
namespace AcDream.App.Rendering;
@@ -29,12 +26,8 @@ namespace AcDream.App.Rendering;
/// The alpha atlas is built but not yet sampled by any shader — that wiring
/// lands in Phase 3c.4 along with the shader rewrite.
///
-public sealed unsafe class TerrainAtlas : IDisposable
+public sealed class TerrainAtlas : IDisposable
{
- private readonly GL? _gl;
-
- // --- Terrain atlas (unchanged public API from Phase 2b) ---
- public uint GlTexture { get; } // terrain atlas, kept as GlTexture for back-compat with TerrainRenderer
public IReadOnlyDictionary TerrainTypeToLayer { get; }
public int LayerCount { get; }
///
@@ -45,7 +38,6 @@ public sealed unsafe class TerrainAtlas : IDisposable
public IReadOnlyList TilingByLayer { get; }
// --- Alpha atlas (new in Phase 3c.2) ---
- public uint GlAlphaTexture { get; }
public int AlphaLayerCount { get; }
/// Layer indices in the alpha atlas for CornerTerrainMaps (typically 4 entries).
public IReadOnlyList CornerAlphaLayers { get; }
@@ -62,77 +54,12 @@ public sealed unsafe class TerrainAtlas : IDisposable
/// RCode for each RoadMap, parallel to .
public IReadOnlyList RoadAlphaRCodes { get; }
- private readonly Wb.BindlessSupport? _bindless;
- private readonly BindlessTexturePair? _bindlessHandles;
- private readonly BindlessTextureMutationGuard? _bindlessMutation;
- private readonly RestoredTextureBindingMutation _anisotropyBindingMutation = new();
- private ResourceShutdownTransaction? _shutdown;
-
- private ulong _registeredTerrainHandle;
- private ulong _registeredAlphaHandle;
- private GpuTextureSlot _terrainSlot = GpuTextureSlot.Unassigned;
- private GpuTextureSlot _alphaSlot = GpuTextureSlot.Unassigned;
-
///
- /// Campaign V slice V4t: the device texture-table slots for the terrain and
- /// alpha arrays — the backend-neutral replacement for the raw 64-bit
- /// ARB_bindless_texture handles this used to return. Residency still
- /// belongs to this atlas (acquired lazily here, released by
- /// ); the device owns only the two table entries.
- ///
- /// Throws if the atlas was
- /// constructed without a instance.
- ///
- /// makes both textures non-resident and
- /// re-acquires them, which yields new handles. Re-registering is therefore
- /// conditional on the handle actually having changed, and the superseded
- /// table entry is retired in the same step — otherwise a quality-preset
- /// change would leak a slot holding a non-resident handle. Registration is
- /// idempotent, so the common per-draw call is two dictionary lookups.
- ///
- internal (GpuTextureSlot Terrain, GpuTextureSlot Alpha) GetTextureSlots(GlGpuDevice device)
- {
- ArgumentNullException.ThrowIfNull(device);
- if (_rhi is not null)
- {
- throw new InvalidOperationException(
- "This TerrainAtlas owns IGpuTexture arrays; ask it for TextureSlots, not for GL handles.");
- }
-
- if (_bindless is null)
- throw new InvalidOperationException(
- "TerrainAtlas was constructed without BindlessSupport; cannot return texture slots.");
-
- (ulong terrain, ulong alpha) = _bindlessHandles!.Acquire();
- if (terrain != _registeredTerrainHandle)
- {
- device.ReleaseWorldTextureHandle(_registeredTerrainHandle);
- _terrainSlot = device.RegisterWorldTextureHandle(terrain);
- _registeredTerrainHandle = terrain;
- }
- if (alpha != _registeredAlphaHandle)
- {
- device.ReleaseWorldTextureHandle(_registeredAlphaHandle);
- _alphaSlot = device.RegisterWorldTextureHandle(alpha);
- _registeredAlphaHandle = alpha;
- }
- return (_terrainSlot, _alphaSlot);
- }
-
- ///
- /// Campaign V slice V6i-2: the backend-neutral arm. When present, both
- /// arrays are s the device created and both slots
- /// were registered at build time, so is a field
- /// read rather than a residency negotiation. and
- /// are 0 and no GL handle exists at all.
- ///
- /// V4t moved the TABLE ENTRY to the device but left CREATION with this
- /// class, which is exactly the remainder plan §5.5.11 recorded: "Creating
- /// world textures through IGpuTexture is real remaining work and it belongs
- /// with the Vulkan world arm, which is the first thing that cannot use a GL
- /// handle at all." This is that work, expressed as a second construction
- /// path rather than a rewrite, so the GL path's calls are textually
- /// unchanged.
+ /// Campaign V slice V6i-2: both arrays are s the
+ /// device created, and both slots were registered at build time, so
+ /// is a field read rather than a residency
+ /// negotiation. The GL construction path this used to sit alongside is
+ /// deleted as of Campaign V slice V11.
///
private sealed class RhiArrays(
IGpuDevice device,
@@ -149,30 +76,19 @@ public sealed unsafe class TerrainAtlas : IDisposable
public GpuTextureSlot AlphaSlot { get; set; } = GpuTextureSlot.Unassigned;
}
- private readonly RhiArrays? _rhi;
+ private readonly RhiArrays _rhi;
///
- /// True when this atlas owns arrays rather than raw
- /// GL names. The two construction paths are mutually exclusive.
- ///
- internal bool IsBackendNeutral => _rhi is not null;
-
- ///
- /// The device-table slots for the terrain and alpha arrays on the
- /// backend-neutral arm. Registered once at build time and re-registered only
- /// by , which changes the sampler.
+ /// The device-table slots for the terrain and alpha arrays. Registered once
+ /// at build time and re-registered only by ,
+ /// which changes the sampler.
///
internal (GpuTextureSlot Terrain, GpuTextureSlot Alpha) TextureSlots =>
- _rhi is null
- ? throw new InvalidOperationException(
- "This TerrainAtlas owns GL names; ask it for GetTextureSlots(GlGpuDevice).")
- : (_rhi.TerrainSlot, _rhi.AlphaSlot);
+ (_rhi.TerrainSlot, _rhi.AlphaSlot);
///
/// Retail's terrain arrays are trilinear-filtered with the highest anisotropy
- /// the quality preset allows; lowers it. The GL
- /// path sets GL_TEXTURE_MAX_ANISOTROPY to 16 at build time, so the
- /// backend-neutral path starts at the same value.
+ /// the quality preset allows; lowers it.
///
private const float RetailMaxAnisotropy = 16f;
@@ -192,11 +108,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
IReadOnlyList sideTCodes,
IReadOnlyList roadRCodes)
{
- _gl = null;
- _bindless = null;
_rhi = new RhiArrays(device, terrain, alpha, alphaSampler);
- GlTexture = 0;
- GlAlphaTexture = 0;
TerrainTypeToLayer = map;
LayerCount = layerCount;
TilingByLayer = tilingByLayer;
@@ -211,72 +123,6 @@ public sealed unsafe class TerrainAtlas : IDisposable
ApplyAnisotropic(RetailMaxAnisotropy);
}
- private TerrainAtlas(
- GL gl,
- Wb.BindlessSupport? bindless,
- uint glTexture, IReadOnlyDictionary map, int layerCount,
- IReadOnlyList tilingByLayer,
- uint glAlphaTexture, int alphaLayerCount,
- IReadOnlyList cornerLayers, IReadOnlyList sideLayers, IReadOnlyList roadLayers,
- IReadOnlyList cornerTCodes, IReadOnlyList sideTCodes, IReadOnlyList roadRCodes)
- {
- _gl = gl;
- _bindless = bindless;
- GlTexture = glTexture;
- TerrainTypeToLayer = map;
- LayerCount = layerCount;
- TilingByLayer = tilingByLayer;
- GlAlphaTexture = glAlphaTexture;
- AlphaLayerCount = alphaLayerCount;
- CornerAlphaLayers = cornerLayers;
- SideAlphaLayers = sideLayers;
- RoadAlphaLayers = roadLayers;
- CornerAlphaTCodes = cornerTCodes;
- SideAlphaTCodes = sideTCodes;
- RoadAlphaRCodes = roadRCodes;
- if (bindless is not null)
- {
- _bindlessHandles = new BindlessTexturePair(
- glTexture,
- glAlphaTexture,
- bindless.GetResidentHandle,
- bindless.MakeNonResident);
- _bindlessMutation = new BindlessTextureMutationGuard(_bindlessHandles);
- }
- }
-
- ///
- /// Build the atlas by walking Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc
- /// for the mapping from TerrainTextureType to SurfaceTexture id, decoding each
- /// to RGBA8, and uploading as layers in a single GL_TEXTURE_2D_ARRAY.
- ///
- public static TerrainAtlas Build(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
- {
- var textures = new GlTextureConstructionTransaction(new GlTextureNameApi(gl));
- try
- {
- TerrainAtlas atlas = BuildCore(gl, dats, bindless, textures);
- textures.Commit();
- return atlas;
- }
- catch (Exception constructionFailure)
- {
- try
- {
- textures.Rollback();
- }
- catch (Exception cleanupFailure)
- {
- throw new GlResourceConstructionException(
- "TerrainAtlas construction failed and its allocated OpenGL texture names did not cleanly roll back.",
- textures,
- [constructionFailure, cleanupFailure]);
- }
-
- throw;
- }
- }
-
///
/// Campaign V slice V6i-2: the decode both construction paths share.
/// Splitting it out is what keeps the CPU logic single while the upload
@@ -340,108 +186,6 @@ public sealed unsafe class TerrainAtlas : IDisposable
return new TerrainLayerDecode(decodedByType, tilingByType, maxW, maxH);
}
- private static TerrainAtlas BuildCore(
- GL gl,
- IDatReaderWriter dats,
- Wb.BindlessSupport? bindless,
- GlTextureConstructionTransaction textures)
- {
- var region = dats.Get(0x13000000u)
- ?? throw new InvalidOperationException("Region dat id 0x13000000 missing");
-
- var texMerge = region.TerrainInfo?.LandSurfaces?.TexMerge;
- var terrainDesc = texMerge?.TerrainDesc;
- if (terrainDesc is null || terrainDesc.Count == 0)
- {
- Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer");
- return BuildFallback(gl, bindless, textures);
- }
-
- // ---- Terrain atlas (unchanged Phase 2b logic) ----
- TerrainLayerDecode decode = DecodeTerrainLayers(dats, terrainDesc);
- Dictionary decodedByType = decode.DecodedByType;
- Dictionary tilingByType = decode.TilingByType;
- int maxW = decode.MaxWidth, maxH = decode.MaxHeight;
-
- int layerCount = decodedByType.Count;
- var map = new Dictionary();
- uint tex = TrackedTextureConstruction.Create(
- textures,
- gl,
- "upload terrain atlas texture array",
- texture =>
- {
- gl.BindTexture(TextureTarget.Texture2DArray, texture);
- gl.TexImage3D(
- TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8,
- (uint)maxW, (uint)maxH, (uint)layerCount,
- 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
-
- int layerIdx = 0;
- foreach (var kvp in decodedByType)
- {
- byte[] buffer = ResizeRgba8Nearest(kvp.Value, maxW, maxH);
- fixed (byte* p = buffer)
- {
- gl.TexSubImage3D(
- TextureTarget.Texture2DArray, 0,
- 0, 0, layerIdx,
- (uint)maxW, (uint)maxH, 1,
- GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
- }
- map[kvp.Key] = (uint)layerIdx;
- layerIdx++;
- }
-
- // A.5 T19: generate mipmaps + trilinear + 16x anisotropic for distant-LB quality.
- gl.GenerateMipmap(TextureTarget.Texture2DArray);
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
- // GL_TEXTURE_MAX_ANISOTROPY = 0x84FE (GL_EXT_texture_filter_anisotropic / ARB_texture_filter_anisotropic).
- gl.TexParameter(TextureTarget.Texture2DArray, (TextureParameterName)0x84FE, 16.0f);
- gl.BindTexture(TextureTarget.Texture2DArray, 0);
- });
-
- var tilingByLayer = TerrainTextureTilingTable.Build(
- map.Select(entry =>
- (entry.Value, tilingByType.TryGetValue(entry.Key, out uint repeatCount)
- ? repeatCount
- : 1u)));
-
- Console.WriteLine($"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH} (mipmaps+aniso16x)");
-
- // ---- Alpha atlas (new in Phase 3c.2) ----
- // texMerge is guaranteed non-null here: the early return above exited
- // if texMerge?.TerrainDesc was null.
- var alphaBuild = BuildAlphaAtlas(gl, dats, texMerge!, textures);
-
- return new TerrainAtlas(
- gl,
- bindless,
- tex, map, layerCount, tilingByLayer,
- alphaBuild.gl, alphaBuild.layerCount,
- alphaBuild.corner, alphaBuild.side, alphaBuild.road,
- alphaBuild.cornerTCodes, alphaBuild.sideTCodes, alphaBuild.roadRCodes);
- }
-
- ///
- /// Load corner, side, and road alpha maps from the TexMerge into a second
- /// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures;
- /// expands each alpha byte
- /// into all four RGBA channels so the shader can sample from any channel.
- ///
- /// Layers are appended in TexMerge insertion order: corners first, then
- /// sides, then roads. The returned index lists tell
- /// TerrainBlending.BuildSurface which layer to cite for each
- /// corner/side/road alpha source.
- ///
- private readonly record struct AlphaAtlasBuildResult(
- uint gl, int layerCount,
- IReadOnlyList corner, IReadOnlyList side, IReadOnlyList road,
- IReadOnlyList cornerTCodes, IReadOnlyList sideTCodes, IReadOnlyList roadRCodes);
-
///
/// Slice V6i-2: the alpha-map decode, shared by both construction paths for
/// the same reason is.
@@ -530,100 +274,12 @@ public sealed unsafe class TerrainAtlas : IDisposable
decodedMaxH);
}
- private static AlphaAtlasBuildResult BuildAlphaAtlas(
- GL gl,
- IDatReaderWriter dats,
- DatReaderWriter.Types.TexMerge texMerge,
- GlTextureConstructionTransaction textures)
- {
- AlphaLayerDecode decode = DecodeAlphaLayers(dats, texMerge);
- List decoded = decode.Decoded;
- List cornerLayers = decode.CornerLayers;
- List sideLayers = decode.SideLayers;
- List roadLayers = decode.RoadLayers;
- List cornerTCodes = decode.CornerTCodes;
- List sideTCodes = decode.SideTCodes;
- List roadRCodes = decode.RoadRCodes;
-
- if (decoded.Count == 0)
- {
- Console.WriteLine("WARN: no alpha maps loaded; alpha atlas will be a 1x1 white fallback");
- uint fallbackAlpha = TrackedTextureConstruction.Create(
- textures,
- gl,
- "upload fallback terrain alpha texture array",
- texture =>
- {
- gl.BindTexture(TextureTarget.Texture2DArray, texture);
- gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0,
- GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
- var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
- fixed (byte* p = white)
- gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1,
- GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
- gl.BindTexture(TextureTarget.Texture2DArray, 0);
- });
- return new AlphaAtlasBuildResult(
- fallbackAlpha, 1,
- cornerLayers, sideLayers, roadLayers,
- cornerTCodes, sideTCodes, roadRCodes);
- }
-
- int aMaxW = decode.MaxWidth, aMaxH = decode.MaxHeight;
-
- uint glAlpha = TrackedTextureConstruction.Create(
- textures,
- gl,
- "upload terrain alpha texture array",
- texture =>
- {
- gl.BindTexture(TextureTarget.Texture2DArray, texture);
- gl.TexImage3D(
- TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8,
- (uint)aMaxW, (uint)aMaxH, (uint)decoded.Count,
- 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
-
- for (int i = 0; i < decoded.Count; i++)
- {
- var buffer = ResizeRgba8Nearest(decoded[i], aMaxW, aMaxH);
- fixed (byte* p = buffer)
- {
- gl.TexSubImage3D(
- TextureTarget.Texture2DArray, 0,
- 0, 0, i,
- (uint)aMaxW, (uint)aMaxH, 1,
- GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
- }
- }
-
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
- gl.BindTexture(TextureTarget.Texture2DArray, 0);
- });
-
- Console.WriteLine(
- $"AlphaAtlas: {decoded.Count} layers at {aMaxW}x{aMaxH} "
- + $"(corners={cornerLayers.Count}, sides={sideLayers.Count}, roads={roadLayers.Count})");
-
- return new AlphaAtlasBuildResult(
- glAlpha, decoded.Count,
- cornerLayers, sideLayers, roadLayers,
- cornerTCodes, sideTCodes, roadRCodes);
- }
-
///
- /// Campaign V slice V6i-2: build both arrays through
- /// .
- ///
- /// Same DAT reads, same decode, same layer ordering and the same
- /// resize-to-max policy as — only the upload differs,
- /// which is the whole point of splitting the decode out. The terrain array
+ /// Builds both arrays through . The terrain array
/// gets a full mip chain ( blits
/// it, because RGBA8 is a legal blit destination) and a repeat/anisotropic
- /// sampler; the alpha array is single-level and clamped, exactly as the GL
- /// texture parameters say.
+ /// sampler; the alpha array is single-level and clamped, matching what the
+ /// deleted GL path's texture parameters said.
///
internal static TerrainAtlas BuildBackendNeutral(IGpuDevice device, IDatReaderWriter dats)
{
@@ -826,151 +482,27 @@ public sealed unsafe class TerrainAtlas : IDisposable
return dst;
}
- private static TerrainAtlas BuildFallback(
- GL gl,
- Wb.BindlessSupport? bindless,
- GlTextureConstructionTransaction textures)
- {
- var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
- uint tex = TrackedTextureConstruction.Create(
- textures,
- gl,
- "upload fallback terrain texture array",
- texture =>
- {
- gl.BindTexture(TextureTarget.Texture2DArray, texture);
- gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
- fixed (byte* p = white)
- gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
- gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- gl.BindTexture(TextureTarget.Texture2DArray, 0);
- });
-
- // Fallback alpha atlas: 1x1 white, no layers tracked
- uint alphaTex = TrackedTextureConstruction.Create(
- textures,
- gl,
- "upload fallback alpha texture array",
- texture =>
- {
- gl.BindTexture(TextureTarget.Texture2DArray, texture);
- gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
- fixed (byte* p = white)
- gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
- gl.BindTexture(TextureTarget.Texture2DArray, 0);
- });
-
- return new TerrainAtlas(
- gl,
- bindless,
- tex, new Dictionary { [0] = 0u }, 1,
- TerrainTextureTilingTable.Build(Array.Empty<(uint Layer, uint RepeatCount)>()),
- alphaTex, 1,
- Array.Empty(), Array.Empty(), Array.Empty(),
- Array.Empty(), Array.Empty(), Array.Empty());
- }
-
///
- /// A.5 T22.5: update GL_TEXTURE_MAX_ANISOTROPY on the terrain atlas at
- /// runtime (called by
+ /// Update terrain-array anisotropy at runtime (called by
/// when
/// the user changes Quality preset mid-session). Idempotent — calling with
/// the same level as the current setting is safe and produces no visual
- /// change. The texture must not be resident-bindless when its parameters
- /// are mutated; we temporarily make it non-resident if needed.
+ /// change.
///
public void SetAnisotropic(int level)
{
- if (_rhi is not null)
- {
- ApplyAnisotropic(level);
- Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x");
- return;
- }
-
- GL gl = RequireGl();
- void Mutate()
- {
- _anisotropyBindingMutation.Execute(
- () => unchecked((uint)GlResourceCommand.Execute(
- gl,
- "read terrain-array binding before anisotropy mutation",
- () => gl.GetInteger(GetPName.TextureBinding2DArray))),
- binding => GlResourceCommand.Execute(
- gl,
- "set terrain-array binding for anisotropy mutation",
- () => gl.BindTexture(TextureTarget.Texture2DArray, binding)),
- GlTexture,
- () => GlResourceCommand.Execute(
- gl,
- "set terrain atlas anisotropy",
- () =>
- {
- // GL_TEXTURE_MAX_ANISOTROPY = 0x84FE
- gl.TexParameter(
- TextureTarget.Texture2DArray,
- (TextureParameterName)0x84FE,
- (float)level);
- }));
- }
-
- if (_bindlessMutation is not null)
- _bindlessMutation.Execute(Mutate);
- else
- Mutate();
-
+ ApplyAnisotropic(level);
Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x");
}
- ///
- /// Slice V6i-2: the GL arm's context. The two construction paths are
- /// mutually exclusive, so a null here means a backend-neutral atlas reached
- /// GL-only code — a programming error, not a runtime condition.
- ///
- private GL RequireGl() =>
- _gl ?? throw new InvalidOperationException(
- "This TerrainAtlas owns IGpuTexture arrays and has no GL context.");
-
public void Dispose()
{
- if (_rhi is not null)
- {
- // Slice V4t's teardown rule holds on both arms: the device dies with
- // its callers, so the table entries are not released here —
- // deferring through a possibly-disposed retirement queue would turn
- // a clean shutdown into a throw. The images themselves route through
- // the device's retirement queue, which is what IGpuTexture.Dispose
- // does.
- _rhi.Alpha.Dispose();
- _rhi.Terrain.Dispose();
- return;
- }
-
- _shutdown ??= new ResourceShutdownTransaction(
- new ResourceShutdownStage(
- "terrain atlas bindless residency",
- [
- new ResourceShutdownOperation(
- "release terrain and alpha handles",
- () => _bindlessHandles?.Release()),
- ]),
- new ResourceShutdownStage(
- "terrain atlas textures",
- [
- new ResourceShutdownOperation(
- "delete terrain texture",
- () => GlResourceCommand.DeleteTexture(
- RequireGl(),
- GlTexture,
- $"delete terrain atlas texture {GlTexture}")),
- new ResourceShutdownOperation(
- "delete alpha texture",
- () => GlResourceCommand.DeleteTexture(
- RequireGl(),
- GlAlphaTexture,
- $"delete terrain alpha texture {GlAlphaTexture}")),
- ]));
- _shutdown.CompleteOrThrow();
+ // Slice V4t's teardown rule: the device dies with its callers, so the
+ // table entries are not released here — deferring through a
+ // possibly-disposed retirement queue would turn a clean shutdown into a
+ // throw. The images themselves route through the device's retirement
+ // queue, which is what IGpuTexture.Dispose does.
+ _rhi.Alpha.Dispose();
+ _rhi.Terrain.Dispose();
}
}
diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs
index 15cd3f7a..9840788f 100644
--- a/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs
+++ b/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs
@@ -292,7 +292,6 @@ public sealed unsafe partial class TerrainModernRenderer
throw;
}
_tilingBuffer = buffer;
- _textureTilingUploaded = true;
}
encoder.BindUniformBuffer(
diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.cs
index e28c3a2f..a9a91cfe 100644
--- a/src/AcDream.App/Rendering/TerrainModernRenderer.cs
+++ b/src/AcDream.App/Rendering/TerrainModernRenderer.cs
@@ -1,23 +1,23 @@
using System.Numerics;
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Gpu.Gl;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Terrain;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
///
-/// Phase N.5b modern terrain dispatcher. Single global VBO/EBO with a slot
-/// allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 bytes
-/// per slot). Per-frame: build a DrawElementsIndirectCommand array from
-/// visible slots, upload, dispatch via glMultiDrawElementsIndirect. Atlas
-/// textures bound via bindless handles set per-frame as sampler uniforms.
+/// Phase N.5b modern terrain dispatcher. Single global vertex/index arena with
+/// a slot allocator (one slot per landblock, 384 verts × 40 bytes = 15,360
+/// bytes per slot). Per-frame: build a DrawElementsIndirectCommand array from
+/// visible slots and dispatch via one multi-draw-indirect call. Atlas
+/// textures bound via the device's global texture table.
///
-/// Total ~6-8 GL calls per frame for terrain regardless of visible
-/// landblock count.
+/// Campaign V slice V11 deleted the raw-GL submission arm
+/// (TerrainModernRenderer.cs's former GL fields/constructor/Draw/Dispose
+/// bodies); the RHI arm this file now exclusively hosts the shared allocator
+/// and visibility logic for is defined in TerrainModernRenderer.Rhi.cs.
///
-public sealed unsafe partial class TerrainModernRenderer : IDisposable
+public sealed partial class TerrainModernRenderer : IDisposable
{
// VertsPerLandblock MUST stay divisible by 6 — terrain_modern.vert uses
// `gl_VertexID % 6` to pick the cell-corner index (BL/BR/TR/TL), and
@@ -32,12 +32,6 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable
private const int IndexSize = sizeof(uint);
private const float LandblockSize = LandblockMesh.LandblockSize; // 192
- // Campaign V slice V6j: null on the RHI arm, where every statement below that
- // reaches one of these is forked into TerrainModernRenderer.Rhi.cs. The GL arm
- // executes exactly what it did before.
- private readonly GL? _gl;
- private readonly BindlessSupport? _bindless;
- private readonly Shader? _shader;
private readonly TerrainAtlas _atlas;
/// A.5 T22.5: exposes the terrain atlas so callers can update
@@ -46,7 +40,6 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable
private readonly GpuRetiredTerrainSlotAllocator _alloc;
private readonly GpuRetirementLedger _retirementLedger;
- private RetryableResourceReleaseLedger? _disposeResources;
private bool _disposed;
// Per-slot live data (index by slot integer; null entries are unused slots).
@@ -55,58 +48,22 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable
// Reverse map: landblockId -> slot, for RemoveLandblock and replacement.
private readonly Dictionary _idToSlot = new();
- // GPU buffers.
- private uint _globalVao;
- private uint _globalVbo;
- private uint _globalEbo;
+ // Backing-store capacity bookkeeping, shared with the RHI arm's arena.
private long _globalVboCapacityBytes;
private long _globalEboCapacityBytes;
- private uint _indirectBuffer;
- private int _indirectCapacity;
- private sealed class DynamicIndirectBuffer
- {
- public uint Buffer;
- public int Capacity;
- }
-
- private readonly List[] _indirectBuffersByFrame =
- [[], [], []];
+ // Per-GPU-fenced-frame-slot draw bookkeeping, shared with the RHI arm.
private int _dynamicFrameSlot;
- private int _dynamicBufferCursor;
private bool _dynamicFrameStarted;
- internal int DynamicIndirectBufferCount =>
- _indirectBuffersByFrame.Sum(frameBuffers => frameBuffers.Count);
-
- // Phase U.3: terrain clip UBO (binding=2, terrain_modern.vert TerrainClip).
- // The shared one is created + uploaded by the GameWindow-level ClipFrame and
- // handed in via SetClipUbo. When 0, we bind a lazily-created no-clip fallback
- // (count 0 = ungated) so the shader never reads an unbound UBO at binding=2.
- private TerrainClipBufferBinding _sharedClipBinding;
- private uint _fallbackClipUbo;
-
- // Campaign V slice V2b (2026-07-27): uTerrainHandle/uAlphaHandle (uvec2)
- // became uTextureIndexA/uTextureIndexB (uint table slots) — cached
- // uniform locations (matrix uniforms are set by name via Shader.SetMatrix4).
- private int _uTextureIndexALoc;
- private int _uTextureIndexBLoc;
- private bool _textureTilingUploaded;
-
- // Campaign V slice V6f-2: the 36 per-layer tiling factors used to be a loose
- // `uniform float uTexTiling[36]`, which Vulkan GLSL cannot declare at all.
- // They now live in the uniform buffer GpuBindingModel reserved
- // UniformTerrainTiling (binding 3) for — see terrain_modern.frag for the
- // std140 packing and why it is vec4[9] rather than float[36].
- private uint _tilingUbo;
-
- // Campaign V slice V4t: the interim per-renderer GlBindlessHandleTable is
- // retired. TerrainAtlas hands out GpuTextureSlots from the device's one
- // table and this renderer flushes and binds that table at
- // GpuBindingModel.StorageTextureTable itself, because it still submits
- // through raw GL and so never reaches GlGpuDevice.FlushBeforeDraw. Null on
- // a backend with no GL device, where this renderer is never constructed.
- private readonly GlGpuDevice? _gpuDevice;
+ ///
+ /// The dynamic per-frame-slot indirect-command buffer pool this used to
+ /// report was raw-GL-only bookkeeping, deleted with that arm at Campaign V
+ /// slice V11. The RHI arm allocates its indirect-command storage from the
+ /// GPU frame's own upload ring instead, so there is no separate pool to
+ /// count.
+ ///
+ internal int DynamicIndirectBufferCount => 0;
// Reusable per-frame buffers.
private readonly List _visibleSlots = new();
@@ -126,183 +83,18 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable
public void BeginVisibilityFrame() => _visibleCellIds.Clear();
- internal TerrainModernRenderer(
- GL gl,
- BindlessSupport bindless,
- Shader shader,
- TerrainAtlas atlas,
- GlGpuDevice? gpuDevice,
- int initialSlotCapacity = 64)
- : this(
- gl,
- bindless,
- shader,
- atlas,
- gpuDevice,
- ImmediateGpuResourceRetirementQueue.Instance,
- initialSlotCapacity)
- {
- }
-
- internal TerrainModernRenderer(
- GL gl,
- BindlessSupport bindless,
- Shader shader,
- TerrainAtlas atlas,
- GlGpuDevice? gpuDevice,
- IGpuResourceRetirementQueue resourceRetirement,
- int initialSlotCapacity = 64)
- {
- _gl = gl;
- _bindless = bindless;
- _shader = shader;
- _atlas = atlas;
- _gpuDevice = gpuDevice;
- ArgumentNullException.ThrowIfNull(resourceRetirement);
- _retirementLedger = new GpuRetirementLedger(resourceRetirement);
- _alloc = new GpuRetiredTerrainSlotAllocator(initialSlotCapacity, resourceRetirement);
- _slots = new SlotData?[initialSlotCapacity];
-
- _uTextureIndexALoc = _gl.GetUniformLocation(_shader.Program, "uTextureIndexA");
- _uTextureIndexBLoc = _gl.GetUniformLocation(_shader.Program, "uTextureIndexB");
-
- var constructionResources = new ResourceCleanupGroup();
- try
- {
- // Campaign V slice V6f-2: the tiling UBO. Fixed size — the table is
- // 36 immutable floats packed four to a vec4 — so it is allocated
- // once here and written once on the first bound draw, which is the
- // same cadence the glUniform1fv it replaces already had.
- _tilingUbo = TrackedGlResource.CreateBuffer(
- _gl,
- "creating terrain tiling UBO");
- RetryableGpuResourceRelease tilingUboRelease =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- _tilingUbo,
- TerrainTextureTilingTable.UniformBufferBytes,
- "rolling back terrain tiling UBO");
- constructionResources.Add("terrain tiling UBO", tilingUboRelease.Run);
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- BufferTargetARB.UniformBuffer,
- _tilingUbo,
- 0,
- TerrainTextureTilingTable.UniformBufferBytes,
- BufferUsageARB.StaticDraw,
- "allocating terrain tiling UBO");
-
- _globalVao = TrackedGlResource.CreateVertexArray(
- _gl,
- "creating terrain global VAO");
- RetryableGpuResourceRelease globalVaoRelease =
- TrackedGlResource.CreateRetryableVertexArrayDeletion(
- _gl,
- _globalVao,
- "rolling back terrain global VAO");
- constructionResources.Add(
- "terrain global VAO",
- globalVaoRelease.Run);
- _globalVbo = TrackedGlResource.CreateBuffer(
- _gl,
- "creating terrain global vertex buffer");
- RetryableGpuResourceRelease globalVboRelease =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- _globalVbo,
- () => _globalVboCapacityBytes,
- "rolling back terrain global vertex buffer");
- constructionResources.Add(
- "terrain global vertex buffer",
- globalVboRelease.Run);
- _globalEbo = TrackedGlResource.CreateBuffer(
- _gl,
- "creating terrain global index buffer");
- RetryableGpuResourceRelease globalEboRelease =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- _globalEbo,
- () => _globalEboCapacityBytes,
- "rolling back terrain global index buffer");
- constructionResources.Add(
- "terrain global index buffer",
- globalEboRelease.Run);
- AllocateGpuBuffers(initialSlotCapacity);
- GlResourceCommand.Execute(
- _gl,
- "configure terrain global vertex array",
- () => ConfigureVao(_globalVao, _globalVbo, _globalEbo));
- constructionResources.TransferAll();
- }
- catch (Exception constructionFailure)
- {
- constructionResources.RollbackConstructionAndThrow(
- "TerrainModernRenderer construction failed and its GL prefix did not cleanly roll back.",
- constructionFailure);
- }
-
- }
-
///
- /// Resets the indirect-command submission cursor for a GPU-fenced frame
- /// slot. A retail outside view may draw terrain more than once in a frame;
- /// each draw receives storage that cannot overwrite an earlier command.
+ /// Resets the per-GPU-fenced-frame-slot draw state. A retail outside view
+ /// may draw terrain more than once in a frame.
///
public void BeginFrame(int frameSlot)
{
- if ((uint)frameSlot >= (uint)_indirectBuffersByFrame.Length)
- throw new ArgumentOutOfRangeException(nameof(frameSlot));
+ ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
_retirementLedger.RetryPendingPublications();
_dynamicFrameSlot = frameSlot;
- _dynamicBufferCursor = 0;
_dynamicFrameStarted = true;
}
- private void ActivateNextIndirectBuffer()
- {
- if (!_dynamicFrameStarted)
- throw new InvalidOperationException("BeginFrame must be called before drawing terrain.");
-
- List frameBuffers = _indirectBuffersByFrame[_dynamicFrameSlot];
- if (_dynamicBufferCursor == frameBuffers.Count)
- {
- uint buffer = TrackedGlResource.CreateBuffer(
- _gl!,
- $"creating terrain indirect buffer for frame slot {_dynamicFrameSlot}");
- try
- {
- frameBuffers.Add(new DynamicIndirectBuffer { Buffer = buffer });
- }
- catch
- {
- TrackedGlResource.DeleteBuffer(
- _gl!,
- buffer,
- 0,
- "rolling back terrain indirect buffer");
- throw;
- }
- }
-
- DynamicIndirectBuffer active = frameBuffers[_dynamicBufferCursor++];
- _indirectBuffer = active.Buffer;
- _indirectCapacity = active.Capacity;
- }
-
- private void PersistIndirectCapacity()
- {
- _indirectBuffersByFrame[_dynamicFrameSlot][_dynamicBufferCursor - 1].Capacity =
- _indirectCapacity;
- }
-
- ///
- /// Hand the renderer the current aligned terrain-clip range (binding=2).
- /// Each outside-view slice occupies a distinct range in the current
- /// GPU-fenced frame's UBO arena.
- ///
- public void SetClipUbo(TerrainClipBufferBinding sharedClipBinding) =>
- _sharedClipBinding = sharedClipBinding;
-
///
/// Two-tier streaming entry point. Accepts a prebuilt mesh from
/// built on the worker
@@ -343,59 +135,26 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable
EnsureCapacity(newCap);
}
- // Bake worldOrigin into vertex positions; capture min/max Z for AABB.
- var bakedVerts = new TerrainVertex[VertsPerLandblock];
- float zMin = float.MaxValue, zMax = float.MinValue;
- for (int i = 0; i < VertsPerLandblock; i++)
- {
- var v = meshData.Vertices[i];
- var worldPos = v.Position + worldOrigin;
- bakedVerts[i] = new TerrainVertex(worldPos, v.Normal, v.Data0, v.Data1, v.Data2, v.Data3);
- if (worldPos.Z < zMin) zMin = worldPos.Z;
- if (worldPos.Z > zMax) zMax = worldPos.Z;
- }
- if (zMin == float.MaxValue) { zMin = 0f; zMax = 0f; }
-
- // Bake baseVertex into indices on the CPU side (driver-portable pattern).
- uint baseVertex = (uint)(slot * VertsPerLandblock);
- var bakedIndices = new uint[IndicesPerLandblock];
- for (int i = 0; i < IndicesPerLandblock; i++)
- bakedIndices[i] = meshData.Indices[i] + baseVertex;
-
- // glBufferSubData into the slot's VBO + EBO regions.
- nint vboByteOffset = (nint)(slot * VertsPerLandblock * VertexSize);
- nint eboByteOffset = (nint)(slot * IndicesPerLandblock * IndexSize);
-
- if (_gl is null)
+ // Bake worldOrigin into vertex positions; capture min/max Z for AABB.
+ var bakedVerts = new TerrainVertex[VertsPerLandblock];
+ float zMin = float.MaxValue, zMax = float.MinValue;
+ for (int i = 0; i < VertsPerLandblock; i++)
{
- UploadRhiLandblock(slot, bakedVerts, bakedIndices);
- }
- else
- {
- fixed (TerrainVertex* p = bakedVerts)
- {
- TrackedGlResource.UpdateBufferSubData(
- _gl,
- BufferTargetARB.ArrayBuffer,
- _globalVbo,
- vboByteOffset,
- VertsPerLandblock * VertexSize,
- p,
- $"uploading terrain vertices for 0x{landblockId:X8}");
+ var v = meshData.Vertices[i];
+ var worldPos = v.Position + worldOrigin;
+ bakedVerts[i] = new TerrainVertex(worldPos, v.Normal, v.Data0, v.Data1, v.Data2, v.Data3);
+ if (worldPos.Z < zMin) zMin = worldPos.Z;
+ if (worldPos.Z > zMax) zMax = worldPos.Z;
}
+ if (zMin == float.MaxValue) { zMin = 0f; zMax = 0f; }
- fixed (uint* p = bakedIndices)
- {
- TrackedGlResource.UpdateBufferSubData(
- _gl,
- BufferTargetARB.ElementArrayBuffer,
- _globalEbo,
- eboByteOffset,
- IndicesPerLandblock * IndexSize,
- p,
- $"uploading terrain indices for 0x{landblockId:X8}");
- }
- }
+ // Bake baseVertex into indices on the CPU side (driver-portable pattern).
+ uint baseVertex = (uint)(slot * VertsPerLandblock);
+ var bakedIndices = new uint[IndicesPerLandblock];
+ for (int i = 0; i < IndicesPerLandblock; i++)
+ bakedIndices[i] = meshData.Indices[i] + baseVertex;
+
+ UploadRhiLandblock(slot, bakedVerts, bakedIndices);
_slots[slot] = new SlotData
{
@@ -472,128 +231,15 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable
}
if (_visibleSlots.Count == 0) return;
- // Campaign V slice V6j: the command array is built the same way on both
- // arms; only where it lands differs. The RHI arm writes it into a frame
- // ring slice, which retires the per-frame-slot indirect buffer pool
- // structurally — every allocation within a frame is already distinct
- // memory that outlives the draw recorded against it.
BuildIndirectCommands();
- if (_gl is null)
- {
- if (!_dynamicFrameStarted)
- throw new InvalidOperationException("BeginFrame must be called before drawing terrain.");
- DrawRhi(viewProjection, _visibleSlots.Count);
- return;
- }
-
- ActivateNextIndirectBuffer();
-
- // Grow indirect buffer if needed.
- if (_visibleSlots.Count > _indirectCapacity)
- {
- int grownCapacity = Math.Max(64, _visibleSlots.Count * 2);
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- GLEnum.DrawIndirectBuffer,
- _indirectBuffer,
- checked((long)_indirectCapacity * sizeof(DrawElementsIndirectCommand)),
- checked((long)grownCapacity * sizeof(DrawElementsIndirectCommand)),
- GLEnum.DynamicDraw,
- "growing terrain indirect command buffer");
- _indirectCapacity = grownCapacity;
- }
-
- // Upload DEIC array.
- fixed (DrawElementsIndirectCommand* p = _deicScratch)
- {
- TrackedGlResource.UpdateBufferSubData(
- _gl,
- GLEnum.DrawIndirectBuffer,
- _indirectBuffer,
- 0,
- checked((long)_visibleSlots.Count * sizeof(DrawElementsIndirectCommand)),
- p,
- "uploading terrain indirect commands");
- }
- PersistIndirectCapacity();
-
- // Bind shader + uniforms + atlas handles.
- // Verified Phase W Stage 4 (T4.2): terrain projects from the camera view-proj;
- // no separate landscape viewpoint to sync. uViewProjection derives from
- // the ICamera passed into this method — the same camera used for all other
- // renderers in the unified pipeline. Retail's LScape::update_viewpoint
- // pre-positions terrain to the outdoor landcell, but acdream uses the
- // unified camera matrix everywhere, so no separate viewpoint divergence can occur.
- _shader!.Use();
- UploadTextureTilingOnce();
- // Campaign V slice V6f-2: bind the tiling UBO every draw, not once. GL's
- // uniform-buffer binding points are global and shared with the sky's
- // params block and the SceneLighting block, so a renderer that runs
- // between two terrain draws can take binding 3 out from under us.
- // Self-contained state, per feedback_render_self_contained_gl_state.
- _gl.BindBufferBase(
- BufferTargetARB.UniformBuffer,
- Gpu.GpuBindingModel.UniformTerrainTiling,
- _tilingUbo);
- // Campaign V slice V6f-1: one uViewProjection, matching the field
- // GpuPushConstants already carries, instead of the separate uView and
- // uProjection the shader used to combine per vertex. viewProjection is
- // the same product the visibility pass above already computed.
- _shader.SetMatrix4("uViewProjection", viewProjection);
-
- // Campaign V slice V2b: pass each texture's binding=9 table slot
- // instead of the raw uvec2 handle. GLSL reconstructs
- // sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)) at the use
- // site — see terrain_modern.frag. Slice V4t: the slots come from the
- // device's one table rather than a table private to this renderer.
- (GpuTextureSlot terrainSlot, GpuTextureSlot alphaSlot) =
- _atlas.GetTextureSlots(GpuDevice);
- FlushAndBindTextureTable();
- _gl.ProgramUniform1(_shader.Program, _uTextureIndexALoc, terrainSlot.Index);
- _gl.ProgramUniform1(_shader.Program, _uTextureIndexBLoc, alphaSlot.Index);
-
- // Phase U.3: bind the terrain clip UBO (binding=2). Shared ClipFrame UBO
- // when wired, else the no-clip fallback (count 0 = ungated terrain).
- BindClipUboBinding2();
-
- // #108-residual: retail terrain is SINGLE-SIDED — ACRender::landPolysDraw
- // (0x006b7040) draws each land triangle ONLY when the camera is on the
- // POSITIVE (upper) side of its plane (Plane::which_side2 vs
- // Render::FrameCurrent, zFightTerrainAdjust bias). GL backface culling
- // evaluates the same per-triangle eye-side predicate at rasterization.
- // LandblockMesh emits every triangle CCW in world XY seen from above
- // (LandblockMeshTests winding pin), which the unified camera chain
- // (CreateLookAt up=+Z + Numerics perspective) maps to CCW window
- // winding from above / CW from below (TerrainCullOrientationTests) —
- // so FrontFace(Ccw)+Cull(Back) keeps the top side and culls the
- // underside. WB drew the whole world with culling DISABLED
- // frame-globally (WB GameScene.cs:841 — an editor camera goes
- // underground); inheriting that drew terrain DOUBLE-SIDED, and a
- // below-grade eye (cellar ascent) saw the UNDERSIDE of the grade
- // sheet through the exit-door aperture — the #108 grass window.
- // Self-contained state per feedback_render_self_contained_gl_state;
- // the frame-global CW + cull-off baseline is restored after the draw.
- _gl.Enable(EnableCap.CullFace);
- _gl.CullFace(TriangleFace.Back);
- _gl.FrontFace(FrontFaceDirection.Ccw);
-
- _gl.BindVertexArray(_globalVao);
- _gl.MemoryBarrier(MemoryBarrierMask.CommandBarrierBit);
- _gl.MultiDrawElementsIndirect(
- PrimitiveType.Triangles, DrawElementsType.UnsignedInt,
- (void*)0,
- (uint)_visibleSlots.Count,
- (uint)sizeof(DrawElementsIndirectCommand));
- _gl.BindVertexArray(0);
- _gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
-
- _gl.FrontFace(FrontFaceDirection.CW);
- _gl.Disable(EnableCap.CullFace);
+ if (!_dynamicFrameStarted)
+ throw new InvalidOperationException("BeginFrame must be called before drawing terrain.");
+ DrawRhi(viewProjection, _visibleSlots.Count);
}
///
/// Builds this frame's DrawElementsIndirectCommand array from the
- /// visible slot list. Pure CPU, identical on both arms.
+ /// visible slot list. Pure CPU.
///
private void BuildIndirectCommands()
{
@@ -618,298 +264,13 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable
if (_disposed)
return;
_retirementLedger.RetryPendingPublications();
- if (_gl is null)
- {
- DisposeRhi();
- return;
- }
-
- if (_disposeResources is null)
- {
- var releases = new List<(string Name, Action Release)>();
- if (_globalVao != 0)
- {
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableVertexArrayDeletion(
- _gl,
- _globalVao,
- "deleting terrain global VAO");
- releases.Add(("global-vao", release.Run));
- }
- if (_globalVbo != 0)
- {
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- _globalVbo,
- _globalVboCapacityBytes,
- "deleting terrain global vertex buffer");
- releases.Add(("global-vbo", release.Run));
- }
- if (_globalEbo != 0)
- {
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- _globalEbo,
- _globalEboCapacityBytes,
- "deleting terrain global index buffer");
- releases.Add(("global-ebo", release.Run));
- }
- for (int frame = 0; frame < _indirectBuffersByFrame.Length; frame++)
- {
- List frameBuffers = _indirectBuffersByFrame[frame];
- for (int index = 0; index < frameBuffers.Count; index++)
- {
- DynamicIndirectBuffer buffer = frameBuffers[index];
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- buffer.Buffer,
- checked((long)buffer.Capacity * sizeof(DrawElementsIndirectCommand)),
- "deleting terrain indirect command buffer");
- releases.Add(($"indirect-{frame}-{index}", release.Run));
- }
- }
- if (_fallbackClipUbo != 0)
- {
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- _fallbackClipUbo,
- ClipFrame.TerrainUboBytes,
- "deleting terrain fallback clip UBO");
- releases.Add(("fallback-clip-ubo", release.Run));
- }
- if (_tilingUbo != 0)
- {
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- _tilingUbo,
- TerrainTextureTilingTable.UniformBufferBytes,
- "deleting terrain tiling UBO");
- releases.Add(("tiling-ubo", release.Run));
- }
- _disposeResources = new RetryableResourceReleaseLedger(releases);
- }
-
- ResourceReleaseAttempt attempt = _disposeResources.Advance();
- if (!_disposeResources.IsComplete)
- {
- throw attempt.ToException(
- "One or more terrain GPU resources could not be released.");
- }
-
- _globalVao = 0;
- _globalVbo = 0;
- _globalEbo = 0;
- _globalVboCapacityBytes = 0;
- _globalEboCapacityBytes = 0;
- foreach (List frameBuffers in _indirectBuffersByFrame)
- frameBuffers.Clear();
- _indirectBuffer = 0;
- _indirectCapacity = 0;
- _dynamicFrameStarted = false;
- _fallbackClipUbo = 0;
- _disposeResources = null;
- _disposed = true;
-
- if (attempt.HasFailures)
- {
- throw attempt.ToException(
- "Terrain GPU resources released with exceptional committed outcomes.");
- }
+ DisposeRhi();
}
// ----------------------------------------------------------------
// Private helpers
// ----------------------------------------------------------------
- ///
- /// Upload the texture-array adapter for retail's per-surface repeat count.
- /// Retail passes TerrainTex::tex_tiling directly to
- /// ImgTex::TileCSI / ImgTex::MergeTexture
- /// (`TexMerge::CopyAndTile` 0x00503580, `TexMerge::Merge` 0x005038C0).
- /// Uniform values persist for the lifetime of this linked shader program,
- /// so the immutable atlas table is uploaded on its first bound draw.
- ///
- private void UploadTextureTilingOnce()
- {
- if (_textureTilingUploaded)
- return;
-
- if (_atlas.TilingByLayer.Count != TerrainTextureTilingTable.LayerCapacity)
- {
- throw new InvalidOperationException(
- $"Terrain tiling table has {_atlas.TilingByLayer.Count} entries; " +
- $"expected {TerrainTextureTilingTable.LayerCapacity}.");
- }
-
- // Campaign V slice V6f-2: one whole-buffer write into the binding=3
- // uniform buffer, replacing the glUniform1fv into the loose array. The
- // block is std140, so each value sits at a 16-byte stride with three
- // dead words after it; the span is cleared first so those words are
- // zero rather than whatever the stack held.
- Span block = stackalloc byte[TerrainTextureTilingTable.UniformBufferBytes];
- block.Clear();
- for (int i = 0; i < TerrainTextureTilingTable.LayerCapacity; i++)
- {
- BitConverter.TryWriteBytes(
- block[(i * TerrainTextureTilingTable.UniformElementStrideBytes)..],
- _atlas.TilingByLayer[i]);
- }
-
- fixed (byte* p = block)
- {
- TrackedGlResource.UpdateBufferSubData(
- _gl!,
- BufferTargetARB.UniformBuffer,
- _tilingUbo,
- 0,
- TerrainTextureTilingTable.UniformBufferBytes,
- p,
- "uploading terrain tiling UBO");
- }
-
- _textureTilingUploaded = true;
- }
-
- ///
- /// The GL device whose texture table this renderer samples through.
- /// Campaign V slice V4t: a terrain renderer without one could not resolve a
- /// single texture, so the failure names the composition that built it rather
- /// than dereferencing null mid-draw.
- ///
- private GlGpuDevice GpuDevice => _gpuDevice ?? throw new InvalidOperationException(
- "TerrainModernRenderer was constructed without a GL GPU device: its texture " +
- "slots come from that device's table (Campaign V slice V4t).");
-
- ///
- /// Campaign V slice V4t: drains the device texture table's dirty runs and
- /// (re)binds it at .
- /// Terrain registers at most two slots (the terrain and alpha atlases), so
- /// the table is dirty only on the atlas's first draw — but the bind is
- /// unconditional, because GL storage-buffer binding points are global and
- /// another renderer's binding 9 sits there between two terrain draws.
- /// Deleted with the raw-GL world path when the Vulkan world arm lands and
- /// this renderer's draws go through the encoder, which binds the same table
- /// on every pipeline bind.
- ///
- private void FlushAndBindTextureTable()
- {
- GlGpuDevice device = GpuDevice;
- device.FlushTextureTable();
- _gl!.BindBufferBase(
- GLEnum.ShaderStorageBuffer,
- GpuBindingModel.StorageTextureTable,
- device.TextureTableGlName);
- }
-
- ///
- /// Phase U.3: bind the terrain clip UBO to binding=2. Prefers the shared
- /// UBO range (); otherwise lazily
- /// creates + binds a no-clip fallback (count 0 = ungated) so the shader never
- /// reads an unbound UBO. The fallback is std140-sized to
- /// and zero-filled (count 0).
- ///
- private void BindClipUboBinding2()
- {
- if (_sharedClipBinding.IsValid)
- {
- _sharedClipBinding.Bind(_gl!);
- return;
- }
-
- if (_fallbackClipUbo == 0)
- {
- var zero = stackalloc byte[ClipFrame.TerrainUboBytes];
- for (int i = 0; i < ClipFrame.TerrainUboBytes; i++) zero[i] = 0;
- uint fallback = TrackedGlResource.CreateBuffer(
- _gl!,
- "creating terrain fallback clip UBO");
- try
- {
- TrackedGlResource.AllocateBufferStorage(
- _gl!,
- BufferTargetARB.UniformBuffer,
- fallback,
- 0,
- ClipFrame.TerrainUboBytes,
- BufferUsageARB.DynamicDraw,
- zero,
- "allocating terrain fallback clip UBO");
- _fallbackClipUbo = fallback;
- }
- catch
- {
- TrackedGlResource.DeleteBuffer(
- _gl!,
- fallback,
- 0,
- "rolling back terrain fallback clip UBO");
- throw;
- }
- }
- _gl!.BindBufferBase(BufferTargetARB.UniformBuffer,
- ClipFrame.TerrainClipUboBinding, _fallbackClipUbo);
- }
-
- private void AllocateGpuBuffers(int capacitySlots)
- {
- long vboBytes = checked((long)capacitySlots * VertsPerLandblock * VertexSize);
- long eboBytes = checked((long)capacitySlots * IndicesPerLandblock * IndexSize);
-
- TrackedGlResource.AllocateBufferStorage(
- _gl!,
- BufferTargetARB.ArrayBuffer,
- _globalVbo,
- _globalVboCapacityBytes,
- vboBytes,
- BufferUsageARB.DynamicDraw,
- "allocating terrain global vertex storage");
- _globalVboCapacityBytes = vboBytes;
-
- TrackedGlResource.AllocateBufferStorage(
- _gl!,
- BufferTargetARB.ElementArrayBuffer,
- _globalEbo,
- _globalEboCapacityBytes,
- eboBytes,
- BufferUsageARB.DynamicDraw,
- "allocating terrain global index storage");
- _globalEboCapacityBytes = eboBytes;
- }
-
- private void ConfigureVao(uint vao, uint vbo, uint ebo)
- {
- _gl!.BindVertexArray(vao);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
- _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo);
-
- uint stride = (uint)VertexSize;
-
- // location 0: Position
- _gl.EnableVertexAttribArray(0);
- _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
- // location 1: Normal
- _gl.EnableVertexAttribArray(1);
- _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
- // locations 2-5: Data0..Data3 (uvec4 byte attributes)
- nint dataOffset = 6 * sizeof(float);
- _gl.EnableVertexAttribArray(2);
- _gl.VertexAttribIPointer(2, 4, VertexAttribIType.UnsignedByte, stride, (void*)dataOffset);
- _gl.EnableVertexAttribArray(3);
- _gl.VertexAttribIPointer(3, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 4));
- _gl.EnableVertexAttribArray(4);
- _gl.VertexAttribIPointer(4, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 8));
- _gl.EnableVertexAttribArray(5);
- _gl.VertexAttribIPointer(5, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 12));
-
- _gl.BindVertexArray(0);
- GLHelpers.ThrowOnResourceError(_gl, "configuring terrain VAO");
- }
-
internal static void CollectVisibleCells(
HashSet destination,
uint landblockId,
@@ -1038,132 +399,7 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable
{
if (newCapacity <= _alloc.Capacity)
return;
- if (_gl is null)
- {
- EnsureRhiCapacity(newCapacity);
- return;
- }
-
- var grownSlots = new SlotData?[newCapacity];
- Array.Copy(_slots, grownSlots, _slots.Length);
-
- long newVboBytes = checked((long)newCapacity * VertsPerLandblock * VertexSize);
- long newEboBytes = checked((long)newCapacity * IndicesPerLandblock * IndexSize);
- uint newVbo = 0;
- uint newEbo = 0;
- uint newVao = 0;
- long allocatedNewVboBytes = 0;
- long allocatedNewEboBytes = 0;
- bool published = false;
- try
- {
- newVbo = TrackedGlResource.CreateBuffer(
- _gl,
- "creating grown terrain vertex buffer");
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- BufferTargetARB.ArrayBuffer,
- newVbo,
- 0,
- newVboBytes,
- BufferUsageARB.DynamicDraw,
- "allocating grown terrain vertex buffer");
- allocatedNewVboBytes = newVboBytes;
-
- newEbo = TrackedGlResource.CreateBuffer(
- _gl,
- "creating grown terrain index buffer");
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- BufferTargetARB.ElementArrayBuffer,
- newEbo,
- 0,
- newEboBytes,
- BufferUsageARB.DynamicDraw,
- "allocating grown terrain index buffer");
- allocatedNewEboBytes = newEboBytes;
-
- GLHelpers.ThrowOnResourceError(_gl, "copying terrain buffers (precondition)");
- _gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalVbo);
- _gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newVbo);
- _gl.CopyBufferSubData(
- CopyBufferSubDataTarget.CopyReadBuffer,
- CopyBufferSubDataTarget.CopyWriteBuffer,
- 0,
- 0,
- checked((nuint)_globalVboCapacityBytes));
- _gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalEbo);
- _gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newEbo);
- _gl.CopyBufferSubData(
- CopyBufferSubDataTarget.CopyReadBuffer,
- CopyBufferSubDataTarget.CopyWriteBuffer,
- 0,
- 0,
- checked((nuint)_globalEboCapacityBytes));
- GLHelpers.ThrowOnResourceError(_gl, "copying terrain buffers");
-
- newVao = TrackedGlResource.CreateVertexArray(
- _gl,
- "creating grown terrain VAO");
- ConfigureVao(newVao, newVbo, newEbo);
-
- uint oldVao = _globalVao;
- uint oldVbo = _globalVbo;
- uint oldEbo = _globalEbo;
- long oldVboBytes = _globalVboCapacityBytes;
- long oldEboBytes = _globalEboCapacityBytes;
-
- _globalVao = newVao;
- _globalVbo = newVbo;
- _globalEbo = newEbo;
- _globalVboCapacityBytes = newVboBytes;
- _globalEboCapacityBytes = newEboBytes;
- _slots = grownSlots;
- _alloc.GrowTo(newCapacity);
- published = true;
-
- // Older submitted draws captured the former VAO/buffer bindings.
- // Retire the complete old set only after the replacement is valid.
- RetryableGpuResourceRelease oldVaoRelease =
- TrackedGlResource.CreateRetryableVertexArrayDeletion(
- _gl,
- oldVao,
- "retiring terrain VAO after growth");
- RetryableGpuResourceRelease oldVboRelease =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- oldVbo,
- oldVboBytes,
- "retiring terrain vertex buffer after growth");
- RetryableGpuResourceRelease oldEboRelease =
- TrackedGlResource.CreateRetryableBufferDeletion(
- _gl,
- oldEbo,
- oldEboBytes,
- "retiring terrain index buffer after growth");
- _retirementLedger.RetireMany(
- [oldVaoRelease, oldVboRelease, oldEboRelease]);
- }
- finally
- {
- if (!published)
- {
- TrackedGlResource.DeleteVertexArray(
- _gl,
- newVao,
- "rolling back grown terrain VAO");
- TrackedGlResource.DeleteBuffer(
- _gl,
- newVbo,
- allocatedNewVboBytes,
- "rolling back grown terrain vertex buffer");
- TrackedGlResource.DeleteBuffer(
- _gl,
- newEbo,
- allocatedNewEboBytes,
- "rolling back grown terrain index buffer");
- }
- }
+ EnsureRhiCapacity(newCapacity);
}
private sealed class SlotData
diff --git a/src/AcDream.App/Rendering/TextRenderer.cs b/src/AcDream.App/Rendering/TextRenderer.cs
index 0ef2a366..40a41f6d 100644
--- a/src/AcDream.App/Rendering/TextRenderer.cs
+++ b/src/AcDream.App/Rendering/TextRenderer.cs
@@ -455,7 +455,8 @@ public sealed class TextRenderer : IDisposable
/// into it, and issues one non-indexed draw. Replaces the old growable
/// per-flight VBO + BufferSubData pattern: every UI vertex upload is
/// now the frame's shared ring, reset once per frame by
- /// .
+ ///
+ /// (the raw-GL device's equivalent reset was deleted at Campaign V slice V11).
///
private static void DrawRing(IGpuFrame frame, IGpuPassEncoder encoder, List buf)
{
diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs
index a7a2c9b0..df9ddada 100644
--- a/src/AcDream.App/Rendering/TextureCache.cs
+++ b/src/AcDream.App/Rendering/TextureCache.cs
@@ -3,10 +3,8 @@ using AcDream.Core.Textures;
using AcDream.Core.World;
using AcDream.Content;
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Gpu.Gl;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
-using Silk.NET.OpenGL;
using System.Linq;
using PixelFormatId = DatReaderWriter.Enums.PixelFormat;
using SurfaceType = DatReaderWriter.Enums.SurfaceType;
@@ -14,22 +12,15 @@ using AcDream.App.Rendering.Residency;
namespace AcDream.App.Rendering;
-public sealed unsafe class TextureCache
+public sealed class TextureCache
: Wb.IEntityTextureLifetime,
IDisposable
{
- private readonly GL? _gl;
private readonly IGpuDevice _device;
private readonly IDatReaderWriter _dats;
private readonly string _diagnosticsDirectory;
- // Handle and decoded dimensions are one atomic cache entry. Keeping them
- // in separate dictionaries allowed GetOrUpload(surfaceId) followed by the
- // sized overload to upload a second GL texture and orphan the first.
- private readonly Dictionary
- _surfacesById = new();
private readonly Dictionary<(uint SurfaceId, uint OrigTextureId), (int Width, int Height)>
_decodedDimensionsByTexture = new();
- private uint _magentaHandle;
///
/// Campaign V slice V4a: one registered plus its
@@ -55,7 +46,6 @@ public sealed unsafe class TextureCache
// GPU texture objects/slots until process exit.
private readonly List _adhocGpuTextures = new();
- private readonly Wb.BindlessSupport? _bindless;
private readonly CompositeTextureArrayCache? _compositeTextures;
private bool _destinationRevealUploadPriority;
@@ -105,12 +95,10 @@ public sealed unsafe class TextureCache
// contract), and this convenience overload has no real caller today (both
// production construction sites already target the internal overload
// below) — kept internal rather than deleted to preserve its shape.
- internal TextureCache(GL? gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
+ internal TextureCache(IGpuDevice device, IDatReaderWriter dats)
: this(
- gl,
device,
dats,
- bindless,
ImmediateGpuResourceRetirementQueue.Instance,
Path.Combine(
Path.GetTempPath(),
@@ -119,142 +107,57 @@ public sealed unsafe class TextureCache
{
}
- ///
- /// The GL context, or null on a backend that has none. Campaign V slice V6h:
- /// the UI path (, )
- /// is entirely -driven and runs on either backend,
- /// while the world paths — the legacy Texture2D upload, particle
- /// arrays, and the composite/bindless caches — still speak raw GL and are
- /// unreachable without it. A null context therefore reaches exactly the same
- /// code a null already gated, and every world
- /// entry point throws with the slice that owns it named. Removed at V4t,
- /// which ports the world texture stack onto the RHI.
- ///
internal TextureCache(
- GL? gl,
IGpuDevice device,
IDatReaderWriter dats,
- Wb.BindlessSupport? bindless,
IGpuResourceRetirementQueue retirementQueue,
string diagnosticsDirectory,
ResidencyBudgetOptions? budgets = null)
{
budgets ??= ResidencyBudgetOptions.Default;
- _gl = gl;
- if (gl is null && bindless is not null)
- {
- throw new ArgumentException(
- "Bindless composite/particle texture caches require a GL context.",
- nameof(bindless));
- }
-
_device = device ?? throw new ArgumentNullException(nameof(device));
_dats = dats;
- _bindless = bindless;
ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory);
_diagnosticsDirectory = diagnosticsDirectory;
ArgumentNullException.ThrowIfNull(retirementQueue);
- if (bindless is not null)
- {
- var resources = new ResourceCleanupGroup();
- CompositeTextureArrayCache? composite = null;
- StandaloneBindlessTextureCache? particles = null;
- try
- {
- composite = new CompositeTextureArrayCache(
- gl!,
- bindless,
- WorldDevice,
- retirementQueue,
- budgets.CompositeUnownedBytes,
- budgets.CompositePhysicalBytes);
- resources.Add("composite texture cache", composite.Dispose);
- particles = new StandaloneBindlessTextureCache(
- new ParticleTextureBackend(this),
- retirementQueue,
- budgets.StandaloneUnownedBytes,
- budgets.StandaloneUnownedEntries);
- resources.Add("particle texture cache", particles.Dispose);
- resources.TransferAll();
- }
- catch (Exception constructionFailure)
- {
- resources.RollbackConstructionAndThrow(
- "TextureCache construction failed and its child-cache prefix did not cleanly roll back.",
- constructionFailure);
- }
- _compositeTextures = composite;
- _particleTextures = particles;
- }
- else
+ // Campaign V slice V6l: both owner-scoped caches exist on both arms.
+ //
+ // Everything about them that matters — sharing equivalent surfaces
+ // between owners, the bounded unowned LRU, the metered upload budget,
+ // and retirement behind the frame-flight fence — is already
+ // backend-neutral; only how one entry is created and destroyed
+ // differs, which is exactly what the two backend interfaces are for.
+ var resources = new ResourceCleanupGroup();
+ CompositeTextureArrayCache? composite = null;
+ StandaloneBindlessTextureCache? particles = null;
+ try
{
- // Campaign V slice V6l: both owner-scoped caches exist on both arms.
- //
- // Everything about them that matters — sharing equivalent surfaces
- // between owners, the bounded unowned LRU, the metered upload budget,
- // and retirement behind the frame-flight fence — is already
- // backend-neutral; only how one entry is created and destroyed
- // differs, which is exactly what the two backend interfaces are for.
- // RhiCompositeTextureArrayBackend is V6i-2's, built and exercised at
- // startup since that slice but with no production consumer until now;
- // ParticleRhiTextureBackend is this slice's.
- var resources = new ResourceCleanupGroup();
- CompositeTextureArrayCache? composite = null;
- StandaloneBindlessTextureCache? particles = null;
- try
- {
- composite = new CompositeTextureArrayCache(
- new RhiCompositeTextureArrayBackend(device),
- retirementQueue,
- budgets.CompositeUnownedBytes,
- budgets.CompositePhysicalBytes);
- resources.Add("composite texture cache", composite.Dispose);
- particles = new StandaloneBindlessTextureCache(
- new ParticleRhiTextureBackend(this),
- retirementQueue,
- budgets.StandaloneUnownedBytes,
- budgets.StandaloneUnownedEntries);
- resources.Add("particle texture cache", particles.Dispose);
- resources.TransferAll();
- }
- catch (Exception constructionFailure)
- {
- resources.RollbackConstructionAndThrow(
- "TextureCache construction failed and its child-cache prefix did not cleanly roll back.",
- constructionFailure);
- }
-
- _compositeTextures = composite;
- _particleTextures = particles;
+ composite = new CompositeTextureArrayCache(
+ new RhiCompositeTextureArrayBackend(device),
+ retirementQueue,
+ budgets.CompositeUnownedBytes,
+ budgets.CompositePhysicalBytes);
+ resources.Add("composite texture cache", composite.Dispose);
+ particles = new StandaloneBindlessTextureCache(
+ new ParticleRhiTextureBackend(this),
+ retirementQueue,
+ budgets.StandaloneUnownedBytes,
+ budgets.StandaloneUnownedEntries);
+ resources.Add("particle texture cache", particles.Dispose);
+ resources.TransferAll();
}
+ catch (Exception constructionFailure)
+ {
+ resources.RollbackConstructionAndThrow(
+ "TextureCache construction failed and its child-cache prefix did not cleanly roll back.",
+ constructionFailure);
+ }
+
+ _compositeTextures = composite;
+ _particleTextures = particles;
}
- ///
- /// The GL context the world texture paths need. Campaign V slice V6h: a
- /// Vulkan-composed cache serves the UI path through
- /// alone and never reaches here, so a failure names the slice that owns the
- /// port rather than dereferencing null.
- ///
- private GL Gl => _gl ?? throw new InvalidOperationException(
- "This TextureCache owns no GL context: the world texture paths " +
- "(Texture2D upload, particle arrays, composite/bindless caches) are " +
- "unavailable until Campaign V slice V4t ports them onto the RHI.");
-
- ///
- /// The GL backend's device, for the world texture paths' table
- /// registrations (Campaign V slice V4t). Those paths already require a GL
- /// context — see — so the same construction that makes
- /// non-null makes this cast sound; a Vulkan-composed
- /// cache serves only the UI path through and never
- /// reaches here.
- ///
- private GlGpuDevice WorldDevice => _device as GlGpuDevice
- ?? throw new InvalidOperationException(
- "This TextureCache's device is not the GL backend's: the world " +
- "texture paths intern their bindless handles into GlGpuDevice's " +
- "texture table (Campaign V slice V4t).");
-
internal void RegisterResidencySources(ResidencyManager manager)
{
ArgumentNullException.ThrowIfNull(manager);
@@ -286,44 +189,6 @@ public sealed unsafe class TextureCache
BudgetBytes: textures.BudgetBytes);
}
- ///
- /// Get or upload the GL texture handle for a Surface id. Returns a
- /// 1x1 magenta fallback if the Surface or its RenderSurface chain is
- /// missing or uses an unsupported format.
- ///
- public uint GetOrUpload(uint surfaceId)
- => GetOrUploadSurfaceCore(surfaceId, out _, out _);
-
- ///
- /// Like but also returns the decoded
- /// pixel dimensions. UI 9-slice geometry needs the source size to
- /// compute slice UVs. Cached alongside the handle.
- ///
- public uint GetOrUpload(uint surfaceId, out int width, out int height)
- => GetOrUploadSurfaceCore(surfaceId, out width, out height);
-
- private uint GetOrUploadSurfaceCore(uint surfaceId, out int width, out int height)
- {
- if (_surfacesById.TryGetValue(surfaceId, out var existing))
- {
- width = existing.Width;
- height = existing.Height;
- return existing.Handle;
- }
-
- DecodedTexture decoded = DecodeFromDats(
- surfaceId,
- origTextureOverride: null,
- paletteOverride: null);
- if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
- DumpAlphaHistogram(surfaceId, decoded);
- uint h = UploadRgba8(decoded);
- _surfacesById.Add(surfaceId, (h, decoded.Width, decoded.Height));
- width = decoded.Width;
- height = decoded.Height;
- return h;
- }
-
///
/// Upload a UI sprite by its RenderSurface DataId (0x06xxxxxx), decoded
/// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the
@@ -491,20 +356,12 @@ public sealed unsafe class TextureCache
}
///
- /// The identity a UI upload is accounted under. On GL it is the texture's
- /// own GL name — unchanged, so the VRAM ledger and the
- /// ACDREAM_DUMP_SURFACES histogram key off exactly what they always
- /// did. On any other backend there is no such name, so a descending
- /// synthetic counter supplies one; it starts at uint.MaxValue because
- /// GL hands out small ascending names and the two spaces share the
- /// _uploadMetadata dictionary. The value is a dictionary key and a
- /// dedup token only — Campaign V slice V6d removed the last draw-time
- /// consumer of a raw GL name, so nothing binds it.
+ /// The identity a UI upload is accounted under. There is no GL name on the
+ /// Vulkan-only backend, so a descending synthetic counter supplies one; the
+ /// value is a dictionary key and a dedup token only — Campaign V slice V6d
+ /// removed the last draw-time consumer of a raw GL name, so nothing binds it.
///
- private uint UploadAccountingName(IGpuTexture texture) =>
- texture is GlGpuTexture glTexture
- ? glTexture.GlName
- : _nextSyntheticUploadName--;
+ private uint UploadAccountingName(IGpuTexture texture) => _nextSyntheticUploadName--;
private uint _nextSyntheticUploadName = uint.MaxValue;
@@ -521,44 +378,6 @@ public sealed unsafe class TextureCache
GpuAddressMode.Repeat,
MaxAnisotropy: 1f);
- ///
- /// Alpha-channel histogram for one decoded texture. Used to diagnose
- /// "why are clouds not transparent" — if cloud textures come out with
- /// alpha = 1.0 everywhere we know the decode path strips the alpha
- /// channel somewhere. Printed once per unique surfaceId under
- /// ACDREAM_DUMP_SKY=1. Adds ~2ms per texture upload, negligible.
- ///
- private static void DumpAlphaHistogram(uint surfaceId, DecodedTexture decoded)
- {
- if (decoded.Rgba8.Length == 0 || decoded.Width == 0 || decoded.Height == 0)
- {
- System.Console.WriteLine($"[tex-alpha] surf=0x{surfaceId:X8} empty");
- return;
- }
- int total = decoded.Rgba8.Length / 4;
- // Bucket alpha in 10 bins.
- var buckets = new int[10];
- int aMin = 255, aMax = 0;
- long aSum = 0;
- for (int i = 0; i < decoded.Rgba8.Length; i += 4)
- {
- int a = decoded.Rgba8[i + 3];
- if (a < aMin) aMin = a;
- if (a > aMax) aMax = a;
- aSum += a;
- int b = a * 10 / 256;
- if (b > 9) b = 9;
- buckets[b]++;
- }
- float aMean = aSum / (float)total / 255f;
- var pct = new string[10];
- for (int i = 0; i < 10; i++) pct[i] = $"{100.0 * buckets[i] / total:F0}%";
- System.Console.WriteLine(
- $"[tex-alpha] surf=0x{surfaceId:X8} {decoded.Width}x{decoded.Height} " +
- $"a_min={aMin / 255f:F3} a_max={aMax / 255f:F3} a_mean={aMean:F3} " +
- $"bins[0-9]={string.Join(",", pct)}");
- }
-
///
/// Acquires the exact DAT-decoded one-layer texture array for a live
/// particle emitter. Equivalent surfaces are shared; the cache ownership
@@ -588,81 +407,17 @@ public sealed unsafe class TextureCache
origTextureOverride: null,
paletteOverride: null);
- // Campaign V slice V6l: the RHI arm has no GL name to intern a bindless
- // handle from, so the image is created through IGpuDevice and paired
- // with a real sampler object. The decode above is the same one the GL
- // arm uses, so the pixels are identical; the shader still samples layer
- // zero of a one-layer array, which is what a Texture2D registered into
- // the table is on Vulkan (VulkanTextureFormatMapping.SampledViewTypeOf).
- if (_gl is null)
- return AcquireParticleTextureRhi(textures, ownerId, surfaceId, decoded);
-
- uint name = UploadRgba8AsLayer1Array(decoded);
- ulong handle = 0;
- try
- {
- handle = _bindless!.GetResidentHandle(name);
- Wb.GLHelpers.ThrowOnResourceError(
- Gl,
- $"making particle surface 0x{surfaceId:X8} resident");
- GpuTextureSlot slot = WorldDevice.RegisterWorldTextureHandle(handle);
- var resource = new StandaloneBindlessTextureResource
- {
- SurfaceId = surfaceId,
- Name = name,
- Handle = handle,
- Slot = slot,
- Bytes = checked((long)decoded.Width * decoded.Height * 4L),
- };
- textures.AddAndAcquire(ownerId, resource);
- return slot;
- }
- catch (Exception residencyFailure)
- {
- List? cleanupFailures = null;
- void Attempt(Action cleanup)
- {
- try { cleanup(); }
- catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
- }
-
- bool residencyReleased = handle == 0;
- if (handle != 0)
- {
- Attempt(() =>
- {
- // Slice V4t: the table entry may or may not have been made
- // before the failure. Releasing an unregistered handle is a
- // no-op, so this covers both without asking which.
- WorldDevice.ReleaseWorldTextureHandle(handle);
- _bindless!.MakeNonResident(handle);
- Wb.GLHelpers.ThrowOnResourceError(
- Gl,
- "rolling back particle texture residency");
- residencyReleased = true;
- });
- }
- if (residencyReleased)
- Attempt(() => DeleteUploadedTexture(name));
- if (cleanupFailures is not null)
- {
- cleanupFailures.Insert(0, residencyFailure);
- throw new AggregateException(
- "Particle texture residency and rollback both failed.",
- cleanupFailures);
- }
- throw;
- }
+ return AcquireParticleTextureRhi(textures, ownerId, surfaceId, decoded);
}
///
/// Campaign V slice V6l: one particle surface as a device texture-table
- /// slot, owned by the same emitter-scoped cache the GL arm uses.
+ /// slot, owned by the same emitter-scoped cache.
///
- /// Linear/clamped is the filtering the GL arm's own one-layer array
- /// upload sets on itself, and a particle sheet's UVs never leave [0,1] —
- /// the quad's own texcoords are the unit square — so the wrap mode is not a
- /// visible choice, it is just the safe one.
+ /// Linear/clamped matches the filtering the deleted GL arm's own
+ /// one-layer array upload set on itself, and a particle sheet's UVs never
+ /// leave [0,1] — the quad's own texcoords are the unit square — so the
+ /// wrap mode is not a visible choice, it is just the safe one.
///
private GpuTextureSlot AcquireParticleTextureRhi(
StandaloneBindlessTextureCache textures,
@@ -714,8 +469,9 @@ public sealed unsafe class TextureCache
/// Owner-scoped bindless variant for a server-supplied original-texture
/// replacement. Stores compatible composites in a pooled Texture2DArray
/// and returns its resident handle plus the assigned layer. Equivalent
- /// composites are shared until their final live owner leaves. Throws if
- /// BindlessSupport wasn't provided.
+ /// composites are shared until their final live owner leaves. Returns
+ /// (an empty location) if a composite upload
+ /// can't start or the decoded size can't be prepared this frame.
///
internal BindlessTextureLocation GetOrUploadWithOrigTextureOverrideBindless(
uint ownerLocalId,
@@ -750,7 +506,8 @@ public sealed unsafe class TextureCache
/// top of the texture's default palette before decoding, stores compatible
/// composites in a pooled Texture2DArray, and returns its resident handle
/// plus the assigned layer. Structural identity is computed once per entity.
- /// Throws if BindlessSupport wasn't provided to the constructor.
+ /// Returns (an empty location) if a composite
+ /// upload can't start or the decoded size can't be prepared this frame.
///
internal BindlessTextureLocation GetOrUploadWithPaletteOverrideBindless(
uint ownerLocalId,
@@ -853,14 +610,6 @@ public sealed unsafe class TextureCache
EnsureCompositeTexturesAvailable().ReleaseOwner(localEntityId);
}
- private void EnsureBindlessAvailable()
- {
- if (_bindless is null)
- throw new InvalidOperationException(
- "TextureCache constructed without BindlessSupport — cannot generate bindless handles. " +
- "WbDrawDispatcher requires the bindless-aware ctor overload (pass non-null BindlessSupport).");
- }
-
///
/// Campaign V slice V6l: no longer gated on bindless. The composite cache is
/// constructed on both arms — V6i-2's RHI backend is what serves the one
@@ -880,29 +629,9 @@ public sealed unsafe class TextureCache
_particleTextures ?? throw new InvalidOperationException(
"This TextureCache owns no standalone particle texture cache.");
- private sealed class ParticleTextureBackend(TextureCache owner)
- : IStandaloneBindlessTextureBackend
- {
- public void MakeNonResident(StandaloneBindlessTextureResource resource)
- {
- // Slice V4t: retire the table entry before its handle stops being
- // resident. Idempotent, so a retried release stays correct.
- owner.WorldDevice.ReleaseWorldTextureHandle(resource.Handle);
- owner._bindless!.MakeNonResident(resource.Handle);
- Wb.GLHelpers.ThrowOnResourceError(
- owner.Gl,
- $"releasing particle texture handle {resource.Handle}");
- }
-
- public void Delete(StandaloneBindlessTextureResource resource)
- => owner.DeleteUploadedTexture(resource.Name);
- }
-
///
- /// Campaign V slice V6l: the same ownership boundary on a backend with no
- /// bindless handles. The table slot is released first and the image second,
- /// which is the same order the GL arm uses and for the same reason — a
- /// submitted-but-unretired frame may still sample the slot, and
+ /// Campaign V slice V6l: the table slot is released first and the image
+ /// second — a submitted-but-unretired frame may still sample the slot, and
/// is what defers its reuse.
///
private sealed class ParticleRhiTextureBackend(TextureCache owner)
@@ -1043,7 +772,6 @@ public sealed unsafe class TextureCache
bucketsByTriple[tripleKey] = bucketsByTriple.GetValueOrDefault(tripleKey) + 1;
}
- foreach (var kv in _surfacesById) Emit(kv.Key, kv.Value.Handle);
_particleTextures?.VisitEntries(resource => Emit(resource.SurfaceId, resource.Name));
_compositeTextures?.VisitEntries((surfaceId, width, height) =>
{
@@ -1191,101 +919,6 @@ public sealed unsafe class TextureCache
return UiTextureTableHandle.FromSlot(entry.Slot);
}
- private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
- {
- uint tex = Gl.GenTexture();
- if (tex == 0)
- throw new InvalidOperationException("OpenGL did not create a 2D texture.");
- try
- {
- Gl.BindTexture(TextureTarget.Texture2D, tex);
-
- fixed (byte* p = decoded.Rgba8)
- Gl.TexImage2D(
- TextureTarget.Texture2D,
- 0,
- InternalFormat.Rgba8,
- (uint)decoded.Width,
- (uint)decoded.Height,
- 0,
- PixelFormat.Rgba,
- PixelType.UnsignedByte,
- p);
-
- // Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat
- // font's small glyphs. Other surfaces use bilinear.
- int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear;
- Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter);
- Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, filter);
- Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
- Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
- Wb.GLHelpers.ThrowOnResourceError(
- Gl,
- $"uploading 2D RGBA8 texture {decoded.Width}x{decoded.Height}");
-
- TrackUploadedTexture(tex, decoded.Width, decoded.Height);
- return tex;
- }
- catch
- {
- Gl.DeleteTexture(tex);
- throw;
- }
- finally
- {
- Gl.BindTexture(TextureTarget.Texture2D, 0);
- }
- }
-
- ///
- /// Variant of that uploads pixel data as a 1-layer
- /// Texture2DArray. Required by the WB modern rendering path which samples via
- /// sampler2DArray in its bindless shader. Pixel data is identical.
- ///
- private uint UploadRgba8AsLayer1Array(DecodedTexture decoded)
- {
- uint tex = Gl.GenTexture();
- if (tex == 0)
- throw new InvalidOperationException("OpenGL did not create a one-layer texture array.");
- try
- {
- Gl.BindTexture(TextureTarget.Texture2DArray, tex);
-
- fixed (byte* p = decoded.Rgba8)
- Gl.TexImage3D(
- TextureTarget.Texture2DArray,
- 0,
- InternalFormat.Rgba8,
- (uint)decoded.Width,
- (uint)decoded.Height,
- depth: 1,
- border: 0,
- PixelFormat.Rgba,
- PixelType.UnsignedByte,
- p);
-
- Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
- Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
- Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
- Wb.GLHelpers.ThrowOnResourceError(
- Gl,
- $"uploading one-layer RGBA8 array {decoded.Width}x{decoded.Height}");
-
- TrackUploadedTexture(tex, decoded.Width, decoded.Height);
- return tex;
- }
- catch
- {
- Gl.DeleteTexture(tex);
- throw;
- }
- finally
- {
- Gl.BindTexture(TextureTarget.Texture2DArray, 0);
- }
- }
-
private void TrackUploadedTexture(uint name, int width, int height)
{
_uploadMetadata[name] = (width, height, "RGBA8_DECODED");
@@ -1294,19 +927,10 @@ public sealed unsafe class TextureCache
Wb.GpuMemoryTracker.TrackAllocation(bytes, Wb.GpuResourceType.Texture);
}
- private void DeleteUploadedTexture(uint name)
- {
- Gl.DeleteTexture(name);
- Wb.GLHelpers.ThrowOnResourceError(Gl, $"deleting uploaded texture {name}");
- UntrackUploadedTexture(name);
- }
-
///
- /// Memory-tracking bookkeeping only, without a raw GL delete — used for
- /// the Campaign V slice V4a UI-path entries,
- /// whose GL name is released by through
- /// the device's own retirement queue rather than by
- /// .
+ /// Memory-tracking bookkeeping only — used for every
+ /// entry, whose GPU resource is released by
+ /// through the device's own retirement queue.
///
private void UntrackUploadedTexture(uint name)
{
@@ -1328,17 +952,6 @@ public sealed unsafe class TextureCache
_paletteIndexedByTexture.Clear();
- // Legacy Texture2D textures.
- foreach (var entry in _surfacesById.Values)
- DeleteUploadedTexture(entry.Handle);
- _surfacesById.Clear();
-
- if (_magentaHandle != 0)
- {
- DeleteUploadedTexture(_magentaHandle);
- _magentaHandle = 0;
- }
-
// RenderSurface (UI sprite) textures — Campaign V slice V4a: each
// entry's IGpuTexture.Dispose() releases the underlying GL name
// through the device's own retirement queue, so only the memory-
diff --git a/src/AcDream.App/Rendering/TrackedTextureConstruction.cs b/src/AcDream.App/Rendering/TrackedTextureConstruction.cs
deleted file mode 100644
index 5d19a0a9..00000000
--- a/src/AcDream.App/Rendering/TrackedTextureConstruction.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace AcDream.App.Rendering;
-
-internal static class TrackedTextureConstruction
-{
- public static uint Create(
- GlTextureConstructionTransaction transaction,
- Action initialize)
- {
- ArgumentNullException.ThrowIfNull(transaction);
- ArgumentNullException.ThrowIfNull(initialize);
- uint texture = transaction.Allocate();
- initialize(texture);
- return texture;
- }
-
- public static uint Create(
- GlTextureConstructionTransaction transaction,
- Silk.NET.OpenGL.GL gl,
- string context,
- Action initialize)
- {
- ArgumentNullException.ThrowIfNull(transaction);
- ArgumentNullException.ThrowIfNull(gl);
- ArgumentException.ThrowIfNullOrWhiteSpace(context);
- ArgumentNullException.ThrowIfNull(initialize);
-
- uint texture = transaction.Allocate();
- GlResourceCommand.Execute(gl, context, () => initialize(texture));
- return texture;
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs b/src/AcDream.App/Rendering/Wb/BindlessSupport.cs
deleted file mode 100644
index cde447c2..00000000
--- a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using Silk.NET.OpenGL;
-using Silk.NET.OpenGL.Extensions.ARB;
-using AcDream.App.Rendering;
-
-namespace AcDream.App.Rendering.Wb;
-
-///
-/// Thin wrapper around + capability detection
-/// for the modern rendering path. Constructed once at startup via
-/// , which returns false if the extension isn't present.
-///
-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(out var ext))
- {
- support = new BindlessSupport(gl, ext);
- return true;
- }
- support = null;
- return false;
- }
-
- /// Get a 64-bit bindless handle for the texture and make it resident.
- /// Idempotent: handle is the same for a given texture name.
- 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;
- }
-
- ///
- /// 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 GlGpuDevice.RegisterTexture,
- /// which registers a (texture, sampler) pair per the RHI contract — "the
- /// same texture registered with two samplers occupies two slots." The
- /// texture-only above cannot express
- /// that; ManagedGLTextureArray already calls the equivalent
- /// ArbBindlessTexture.GetTextureSamplerHandle directly through
- /// OpenGLGraphicsDevice.BindlessExtension, so this simply exposes
- /// the same GL entry point through this class for the RHI's use.
- ///
- 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;
- }
-
- /// Release residency for a handle. Call before deleting the underlying texture.
- 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.
-
- /// Detect GL_ARB_shader_draw_parameters in addition to bindless.
- /// N.5's vertex shader uses gl_BaseInstanceARB and gl_DrawIDARB
- /// from this extension.
- public bool HasShaderDrawParameters(GL gl)
- {
- return gl.IsExtensionPresent("GL_ARB_shader_draw_parameters");
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
index f268f899..982206c9 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
@@ -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();
- // 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();
- // 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? _pointSnapshot;
private sealed class CachedCellLightSet
@@ -125,51 +99,17 @@ public sealed unsafe partial class EnvCellRenderer :
private readonly List _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[] _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;
+ ///
+ /// 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.
+ ///
+ 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> _activeSnapshotGlobalGroups = new();
private readonly List _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;
- }
-
/// Resets the per-frame submission cursor for the GPU-fenced slot.
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 :
}
}
- ///
- /// Phase U.3: hand the renderer the SHARED per-cell clip-region SSBO
- /// (binding=2) created by . 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.
- ///
- 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? filter,
IReadOnlyList? 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 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 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 instances in _activeSnapshotGlobalGroups.Values)
@@ -1359,59 +1165,7 @@ public sealed unsafe partial class EnvCellRenderer :
}
}
- private void DeleteDynamicBufferSet(DynamicBufferSet set)
- {
- List? 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 allInstances,
IReadOnlyList 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)
- // ---------------------------------------------------------------------------
-
- ///
- /// Campaign V slice V4t: drains the device texture table's dirty runs and
- /// (re)binds it at
- /// .
- /// 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.
- ///
- 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)
- // ---------------------------------------------------------------------------
-
- ///
- /// Bind the per-cell clip-region SSBO to binding=2. Prefers the shared
- /// buffer (); otherwise
- /// lazily creates + binds a one-slot no-clip fallback (count 0 = pass-all) so
- /// the shader never reads an unbound SSBO.
- ///
- 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 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 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 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));
- }
}
diff --git a/src/AcDream.App/Rendering/Wb/GLHelpers.cs b/src/AcDream.App/Rendering/Wb/GLHelpers.cs
deleted file mode 100644
index 8e0640d6..00000000
--- a/src/AcDream.App/Rendering/Wb/GLHelpers.cs
+++ /dev/null
@@ -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;
- }
-
- ///
- /// Always-on error boundary for resource transactions. Most render-path
- /// checks remain Debug-only because glGetError 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.
- ///
- [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
- ///
- /// Checks for OpenGL errors and provides context-specific information
- ///
- 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
-
-
- ///
- /// Gets detailed information about the current texture state for debugging
- ///
- 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();
- }
-
- ///
- /// Logs current OpenGL state for debugging
- ///
- 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());
- }
-
- ///
- /// 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.
- ///
- 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);
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/GLSLShader.cs b/src/AcDream.App/Rendering/Wb/GLSLShader.cs
deleted file mode 100644
index f4eabecf..00000000
--- a/src/AcDream.App/Rendering/Wb/GLSLShader.cs
+++ /dev/null
@@ -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 _uniformLocations = [];
- private Dictionary _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);
- });
- }
- }
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/GLStateScope.cs b/src/AcDream.App/Rendering/Wb/GLStateScope.cs
deleted file mode 100644
index 49abea1c..00000000
--- a/src/AcDream.App/Rendering/Wb/GLStateScope.cs
+++ /dev/null
@@ -1,230 +0,0 @@
-using Silk.NET.OpenGL;
-using System;
-
-namespace AcDream.App.Rendering.Wb {
- ///
- /// A RAII scope for saving and restoring OpenGL state.
- ///
- 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;
-
- ///
- /// Captures the current OpenGL state.
- ///
- ///
- 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);
- }
-
- ///
- /// Restores only the scissor state from the scope.
- ///
- 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]);
- }
-
- ///
- /// Restores the captured OpenGL state.
- ///
- 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;
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs
index 8138e068..9211cb68 100644
--- a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs
+++ b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs
@@ -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 vkCmdCopyBuffer. 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
-/// WbDrawDispatcher, EnvCellRenderer and ParticleRenderer
-/// still bind // directly
-/// on the GL arm.
+/// resource handle type moved.
///
-/// Campaign V slice V6i-3 made the GL context optional. A backend that has
-/// none builds no vertex array and publishes no raw names — ,
-/// and are 0 there — and its consumers bind
+/// 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
/// and 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.
+/// encoder, which is the same 32-byte position/normal/texcoord layout
+/// expressed as pipeline vertex input.
///
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.");
- ///
- /// Campaign V slice V4b transitional bridge. The arena owns its stores as
- /// , but its consumers — the vertex array object here,
- /// and WbDrawDispatcher/EnvCellRenderer/ParticleRenderer
- /// through / — are still raw GL until slice
- /// V4c. This is the only place that reaches through the interface, and it
- /// disappears with those consumers.
- ///
- 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; }
-
- ///
- /// 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
- /// .
- ///
- public uint VBO =>
- _gl is null || _vertexBuffer is null ? 0u : RequireGlBuffer(_vertexBuffer).GlName;
-
- /// The index store's raw GL name. See .
- public uint IBO =>
- _gl is null || _indexBuffer is null ? 0u : RequireGlBuffer(_indexBuffer).GlName;
-
///
/// The vertex store as the contract's own handle. This is what a pass
- /// encoder binds, and it is live on both arms — is the
- /// GL-only expression of the same thing.
+ /// encoder binds.
///
internal IGpuBuffer? VertexStore => _vertexBuffer;
/// The index store as the contract's own handle. See .
internal IGpuBuffer? IndexStore => _indexBuffer;
- ///
- /// True once both backing stores exist. The backend-neutral form of the
- /// VAO != 0 readiness test the raw-GL draw paths make.
- ///
+ /// True once both backing stores exist.
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
///
/// Copies at most 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
+ /// () publishes the destination.
///
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
///
/// The arena's own flight gate — 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
- /// . Stages match
- /// TrackedGlResource.CreateRetryableBufferDeletion 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.
///
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;
diff --git a/src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs b/src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs
index 0b60bd74..de20127c 100644
--- a/src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs
+++ b/src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs
@@ -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.
///
-/// So the coupling is expressed as an interface at exactly that surface
-/// and declares it — every member already
-/// existed, so the GL arm executes not one changed statement. What this buys is
-/// that ObjectMeshManager and WbMeshAdapter no longer NAME a
-/// backend, which is the prerequisite for the slice that gives them a second
+/// So the coupling is expressed as an interface at exactly that surface,
+/// and OpenGLGraphicsDevice declared it — every member already existed,
+/// so the GL arm executed not one changed statement. What this buys is that
+/// ObjectMeshManager and WbMeshAdapter no longer NAME a backend,
+/// which is the prerequisite for the slice that gives them a second
/// implementation.
///
/// What slice V6i-3 then moved. 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
/// IGpuDevice.CreateBuffer and publishes them as
/// GlobalMeshBuffer.VertexStore/IndexStore, which a pass encoder
-/// binds; the vertex array is built only where one exists. What still reads
-/// is the LEGACY per-mesh upload the N.5 ship amendment made
-/// unreachable, and AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice
-/// is the second implementation this interface was cut for.
+/// binds; the vertex array is built only where one exists.
+/// is the
+/// second implementation this interface was cut for.
+///
+/// Campaign V slice V11 deleted OpenGLGraphicsDevice along
+/// with the rest of the raw-GL arm it fronted, so
+/// is now
+/// the interface's only implementation. 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.
///
internal interface IMeshPipelineDevice : IDisposable
{
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs
deleted file mode 100644
index 7a3b190a..00000000
--- a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs
+++ /dev/null
@@ -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 {
- ///
- /// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET.
- ///
- 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);
- }
- });
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
deleted file mode 100644
index 7443fc58..00000000
--- a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
+++ /dev/null
@@ -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 {
- ///
- /// OpenGL index buffer
- ///
- public unsafe class ManagedGLIndexBuffer : IIndexBuffer {
- private uint bufferId;
- private readonly OpenGLGraphicsDevice _device;
- private void* _mappedPtr;
- private GL GL => _device.GL;
-
- ///
- public int Size { get; private set; }
-
- ///
- public BufferUsage Usage { get; private set; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Buffer usage
- /// The size of the buffer, in bytes
- 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);
- }
-
- ///
- public void SetData(uint[] data) {
- SetData(data.AsSpan());
- }
-
- ///
- public unsafe void SetData(Span 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 mappedSpan = new Span(_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);
- }
- }
-
-
- ///
- public unsafe void SetSubData(Span 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 mappedSpan = new Span((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);
- }
- }
- }
-
-
- ///
- 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);
- }
- }
-
- ///
- public void Bind() {
- RenderStateCache.CurrentIBO = 0;
- GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
- GLHelpers.CheckErrors(GL);
- }
-
- ///
- 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;
- }
- });
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
deleted file mode 100644
index 31e252e4..00000000
--- a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
+++ /dev/null
@@ -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;
-
- ///
- public IntPtr NativePtr => (IntPtr)_texture;
-
- ///
- public int Width { get; private set; }
-
- ///
- public int Height { get; private set; }
-
- public TextureFormat Format => TextureFormat.RGBA8;
-
- ///
- 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;
- }
-
- ///
- 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();
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
deleted file mode 100644
index 4a6e263f..00000000
--- a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
+++ /dev/null
@@ -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;
- ///
- /// Campaign V slice V6i-2: the device whose one texture table this
- /// array's two resident handles are interned into. Before this slice
- /// ObjectMeshManager 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 instead. Null only
- /// for the legacy OpenGLGraphicsDevice.CreateTextureArrayInternal
- /// entry points, which no shared atlas uses.
- ///
- 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 _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();
-
- ///
- /// #105 diagnostic: staged layer updates (retained decoded payloads) not yet
- /// applied to the GL texture by . 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.
- ///
- 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? 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 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);
- }
-
- ///
- /// 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.
- ///
- 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;
- }
- }
-
- ///
- /// 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.
- ///
- internal bool IsPhysicalRetirementComplete =>
- Volatile.Read(ref _disposeQueued) != 0
- && Volatile.Read(ref _disposeRelease) is null;
-
- bool IWorldTextureArray.HasDurableDisposeOwnership => HasDurableDisposeOwnership;
-
- bool IWorldTextureArray.IsPhysicalRetirementComplete => IsPhysicalRetirementComplete;
-
- ///
- /// Campaign V slice V6i-2: this array's device-table slot for the
- /// requested address mode.
- ///
- /// The interning call is the one ObjectMeshManager made
- /// itself before this slice, moved one level down so the caller can be
- /// written against instead of against a
- /// 64-bit ARB_bindless_texture 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.
- ///
- 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);
- }
-
- ///
- /// Retires both table entries. 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.
- ///
- 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 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;
- }
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs
deleted file mode 100644
index ebcc56d2..00000000
--- a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs
+++ /dev/null
@@ -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 {
- ///
- /// OpenGL uniform buffer
- ///
- public unsafe class ManagedGLUniformBuffer : IUniformBuffer {
- private uint bufferId;
- private readonly OpenGLGraphicsDevice _device;
- private GL GL => _device.GL;
-
- ///
- public int Size { get; private set; }
-
- ///
- public BufferUsage Usage { get; private set; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Graphics device
- /// Buffer usage
- /// The size of the buffer, in bytes
- 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;
- }
-
- ///
- public unsafe void SetData(T[] data) where T : unmanaged {
- SetData(data.AsSpan());
- }
-
- ///
- public unsafe void SetData(Span data) where T : unmanaged {
- uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf();
-
- // 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);
- }
- }
-
- ///
- public unsafe void SetSubData(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
- SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
- }
-
- ///
- public unsafe void SetSubData(Span 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();
-
- // 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);
- }
- }
-
- ///
- /// Sets a single piece of data in the buffer.
- ///
- public unsafe void SetData(ref T data) where T : unmanaged {
- fixed (T* pData = &data) {
- SetData(new Span(pData, 1));
- }
- }
-
- ///
- /// Binds the buffer to the specified binding point.
- ///
- /// The binding point to bind to
- public void Bind(uint bindingPoint) {
- GL.BindBufferBase(GLEnum.UniformBuffer, bindingPoint, bufferId);
- GLHelpers.CheckErrors(GL);
- }
-
- ///
- public void Bind() {
- GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
- GLHelpers.CheckErrors(GL);
- }
-
- ///
- 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;
- }
- });
- }
-
- ///
- /// 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.
- ///
- internal void DisposeImmediately() {
- if (bufferId == 0)
- return;
-
- RetryableGpuResourceRelease release =
- TrackedGlResource.CreateRetryableBufferDeletion(
- GL,
- bufferId,
- Size,
- "rolling back unpublished managed uniform buffer");
- release.Run();
- bufferId = 0;
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs
deleted file mode 100644
index 583e82d9..00000000
--- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs
+++ /dev/null
@@ -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);
- });
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs
deleted file mode 100644
index d9a3752a..00000000
--- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs
+++ /dev/null
@@ -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 {
- ///
- /// OpenGL vertex buffer
- ///
- public unsafe class ManagedGLVertexBuffer : IVertexBuffer {
- private uint bufferId;
- private readonly OpenGLGraphicsDevice _device;
- private void* _mappedPtr;
- private GL GL => _device.GL;
-
- ///
- public int Size { get; private set; }
-
- ///
- public BufferUsage Usage { get; private set; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Buffer usage
- /// The size of the buffer, in bytes
- 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);
- }
-
- ///
- public unsafe void SetData(T[] data) where T : IVertex {
- SetData(data.AsSpan());
- }
-
- ///
- public unsafe void SetData(Span data) where T : IVertex {
- uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf();
-
- // 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 mappedSpan = new Span(_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 mappedSpan = new Span(mappedPtr, data.Length);
- data.CopyTo(mappedSpan);
- }
- finally {
- // Unmap the buffer
- GL.UnmapBuffer(GLEnum.ArrayBuffer);
- GLHelpers.CheckErrors(GL);
- }
- }
- }
-
- public unsafe void SetSubData(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
- SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
- }
-
- ///
- public unsafe void SetSubData(Span 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();
-
- // 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 mappedSpan = new Span((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 mappedSpan = new Span(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;
- }
- });
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs
index a3b497eb..f670a7ec 100644
--- a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs
+++ b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs
@@ -1,6 +1,4 @@
using System.Runtime.InteropServices;
-using DatReaderWriter.Enums;
-using Chorizite.Core.Render;
namespace AcDream.App.Rendering.Wb {
///
@@ -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;
- }
}
diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
index 4c47d8e9..c7c5a835 100644
--- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
+++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
@@ -28,7 +28,17 @@ namespace AcDream.App.Rendering.Wb
///
public class ObjectRenderData
{
+ ///
+ /// 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 arena has no VAO/VBO
+ /// concept at all (see ). This
+ /// is always 0 now; it survives only because
+ /// WbDrawDispatcher.cs's legacy (non-RHI) dispatcher still reads
+ /// it into its own dead anyVao bookkeeping.
+ ///
public uint VAO { get; set; }
+ /// See — always 0 for the same reason.
public uint VBO { get; set; }
public int VertexCount { get; set; }
public List Batches { get; set; } = new();
@@ -76,6 +86,11 @@ namespace AcDream.App.Rendering.Wb
///
public class ObjectRenderBatch
{
+ /// See — 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 /.
+ ///
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
///
private readonly IMeshPipelineDevice _graphicsDevice;
- ///
- /// The GL context the LEGACY (pre-modern-path) upload bodies write
- /// through.
- ///
- /// Campaign V slice V6i-3 narrowed what still needs it. The modern
- /// path's arena upload is 's, and that is
- /// now 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 _useModernRendering 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.
- ///
- 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
///
private readonly AcDream.App.Rendering.Gpu.IGpuDevice _gpuDevice;
- ///
- /// 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.
- ///
- 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.");
-
///
/// Campaign V slice V6i-2: how a shared atlas's physical array is made.
/// Composed once; see .
@@ -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
///
/// #105 diagnostic: counts staged-but-unflushed texture layer updates across all
- /// shared atlases (see ).
+ /// shared atlases (see ).
/// Render thread only — _globalAtlases is render-thread-owned.
///
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();
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)
diff --git a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
deleted file mode 100644
index 1e926865..00000000
--- a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
+++ /dev/null
@@ -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 {
- ///
- /// OpenGL graphics device
- ///
- 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> _glThreadQueue = new();
- private readonly ConcurrentQueue> _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 action) {
- _glThreadQueue.Enqueue(action);
- }
-
- internal void QueueGLActionForNextPass(Action 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? 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; }
-
- /// OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs).
- public uint WrapSampler { get; private set; }
- /// OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs).
- public uint ClampSampler { get; private set; }
- internal float MaxSupportedAnisotropy { get; private set; }
-
- private ManagedGLUniformBuffer? _sceneDataBuffer;
- /// Shared SceneData UBO.
- 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;
-
- ///
- 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());
- 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);
- }
- }
-
- ///
- /// Retires a GL resource only after every submitted draw that could
- /// reference it has completed on the GPU.
- ///
- 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(List data) where T : unmanaged {
- EnsureInstanceBufferCapacity(data.Count, Marshal.SizeOf(), true);
- var span = CollectionsMarshal.AsSpan(data);
- if (InstanceVBOPtr != null) {
- var destSpan = new Span(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()), ptr);
- }
- }
- }
-
- public void UpdateInstanceBuffer(Span data) where T : unmanaged {
- EnsureInstanceBufferCapacity(data.Length, Marshal.SizeOf(), true);
- if (InstanceVBOPtr != null) {
- var destSpan = new Span(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()), ptr);
- }
- }
- }
-
- ///
- 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);
- }
-
- ///
- public override IIndexBuffer CreateIndexBuffer(int size,
- Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
- return new ManagedGLIndexBuffer(this, usage, size);
- }
-
- ///
- public override IVertexBuffer CreateVertexBuffer(int size,
- Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
- return new ManagedGLVertexBuffer(this, usage, size);
- }
-
- ///
- public override IVertexArray CreateArrayBuffer(IVertexBuffer vertexBuffer, VertexFormat format) {
- return new ManagedGLVertexArray(this, vertexBuffer, format);
- }
-
- ///
- 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();
- }
- }
-
- ///
- 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 _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();
- }
- }
- }
-
- ///
- 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);
- }
-
- ///
- /// Creates a texture with custom texture parameters.
- ///
- 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);
- }
-
- ///
- 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);
- }
-
- ///
- public override ITextureArray
- CreateTextureArrayInternal(TextureFormat format, int width, int height, int size) {
- return new ManagedGLTextureArray(this, format, width, height, size, _log);
- }
-
- ///
- /// Creates a texture array with custom texture parameters.
- ///
- public ITextureArray CreateTextureArrayInternal(TextureFormat format, int width, int height, int size, TextureParameters texParams) {
- return new ManagedGLTextureArray(this, format, width, height, size, _log, texParams);
- }
-
- ///
- 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);
- }
-
- ///
- public override void EndFrame() {
- }
-
- ///
- 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;
- }
- }
-
- ///
- 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.");
- }
- }
-
- ///
- 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);
- }
-
- ///
- public override void BindFramebuffer(IFramebuffer? framebuffer) {
- uint fboId = framebuffer != null ? (uint)framebuffer.NativeHandle.ToInt32() : 0;
- GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId);
- }
-
- ///
- 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? 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);
- }
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs
deleted file mode 100644
index 6453ed7e..00000000
--- a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-namespace AcDream.App.Rendering.Wb;
-
-///
-/// Tracks currently-bound GL state to skip redundant rebinds across the
-/// WB-derived render path. Previously these were static fields on
-/// BaseObjectRenderManager in the WorldBuilder.Shared project; inlined
-/// here in Phase O-T7 to eliminate the WorldBuilder project reference.
-///
-/// Semantics are identical to the WB originals:
-/// CurrentAtlas — slot index of the currently bound texture atlas.
-/// CurrentVAO — OpenGL name of the currently bound vertex array object.
-/// CurrentIBO — OpenGL name of the currently bound index buffer object.
-/// Sentinel value 0 means "no valid binding cached."
-///
-///
-/// 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.
-///
-public static class RenderStateCache
-{
- public static uint CurrentAtlas = 0;
- public static uint CurrentVAO = 0;
- public static uint CurrentIBO = 0;
-}
diff --git a/src/AcDream.App/Rendering/Wb/TrackedGlResource.cs b/src/AcDream.App/Rendering/Wb/TrackedGlResource.cs
deleted file mode 100644
index 6a02e3fd..00000000
--- a/src/AcDream.App/Rendering/Wb/TrackedGlResource.cs
+++ /dev/null
@@ -1,283 +0,0 @@
-using Silk.NET.OpenGL;
-using AcDream.App.Rendering;
-using System.Runtime.ExceptionServices;
-
-namespace AcDream.App.Rendering.Wb;
-
-///
-/// 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.
-///
-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 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 create,
- Action 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);
- }
-}
diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
index 54ebdfbd..e64b042d 100644
--- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
+++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
@@ -13,21 +13,20 @@ using AcDream.Core.Terrain;
using AcDream.Core.World;
using AcDream.App.Rendering.Selection;
using DatReaderWriter.Enums;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb;
///
/// Draws entities using WB's (a single global
-/// VAO/VBO/IBO under modern rendering) with acdream's
-/// for bindless texture resolution. Exact pass classification travels with
-/// each immutable prepared mesh batch.
+/// vertex/index arena under modern rendering) with acdream's
+/// for texture resolution. Exact pass classification
+/// travels with each immutable prepared mesh batch.
///
///
/// Atlas-tier entities (ServerGuid == 0): mesh data comes from WB's
/// via .
/// Shared textures reuse each batch's WB atlas handle and layer, returning
-/// 64-bit resident handles stored in the per-group SSBO.
+/// a device texture-table slot stored in the per-group SSBO.
///
///
///
@@ -40,33 +39,32 @@ namespace AcDream.App.Rendering.Wb;
///
///
///
-/// GL strategy (N.5 — mandatory): glMultiDrawElementsIndirect with SSBOs
-/// and GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters.
-/// All visible (entity, batch) pairs are bucketed by ;
-/// each group becomes one DrawElementsIndirectCommand. Three GPU buffers
-/// are uploaded per frame: instance matrices (SSBO binding 0), per-group batch
-/// metadata/texture handles (SSBO binding 1), and the indirect draw commands.
-/// Opaque world groups remain MDI-batched. Transparent world instances enter
-/// so ordinary GfxObj parts and particles share
-/// retail's stable far-to-near stream; sealed off-screen consumers retain the
-/// immediate transparent MDI path.
+/// Draw strategy (Campaign V — Vulkan only): multi-draw-indexed-indirect
+/// with SSBOs, recorded through the RHI encoder in the sibling
+/// WbDrawDispatcher.Rhi.cs partial. All visible (entity, batch) pairs are
+/// bucketed by ; each group becomes one
+/// DrawElementsIndirectCommand. Per-frame ring allocations carry instance
+/// matrices (binding 0), per-group batch metadata/texture-table slots (binding
+/// 1), and the indirect draw commands. Opaque world groups remain MDI-batched.
+/// Transparent world instances enter so ordinary
+/// GfxObj parts and particles share retail's stable far-to-near stream; sealed
+/// off-screen consumers retain the immediate transparent MDI path.
///
///
///
-/// Shader: mesh_modern (bindless + gl_DrawIDARB /
-/// gl_BaseInstanceARB). Missing bindless/draw-parameters throws
-/// at startup — there is no legacy fallback.
+/// Shader: mesh_modern, compiled from committed SPIR-V. Missing a
+/// mandatory GPU capability (the device texture table, MDI, or SSBOs) throws at
+/// renderer construction — there is no legacy fallback.
///
///
///
-/// Modern rendering assumption: WB's _useModernRendering path (GL
-/// 4.3 + bindless) puts every mesh in a single shared VAO/VBO/IBO and uses
-/// FirstIndex + BaseVertex per batch. The dispatcher honors those
-/// offsets inside each DrawElementsIndirectCommand via
-/// glMultiDrawElementsIndirect.
+/// Modern rendering assumption: WB's modern-rendering path puts every
+/// mesh in a single shared vertex/index arena and uses FirstIndex +
+/// BaseVertex per batch. The dispatcher honors those offsets inside each
+/// DrawElementsIndirectCommand via multi-draw-indexed-indirect.
///
///
-public sealed unsafe partial class WbDrawDispatcher : IDisposable
+public sealed partial class WbDrawDispatcher : IDisposable
{
///
/// Which subset of entities to walk in a single Draw call.
@@ -86,8 +84,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
All,
}
- private readonly GL? _gl;
- private readonly Shader? _shader;
private readonly TextureCache _textures;
private readonly WbMeshAdapter _meshAdapter;
private readonly EntitySpawnAdapter _entitySpawnAdapter;
@@ -98,7 +94,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
private readonly RetainedScratchCapacityPolicy _alphaScratchPolicy;
private int _scratchPeakUnits;
- private readonly BindlessSupport? _bindless;
private ICurrentRenderDispatcherObserver? _currentRenderSceneObserver;
public readonly record struct DrawStats(
@@ -444,99 +439,47 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
///
public bool AlphaToCoverage { get; set; } = true;
- // SSBO buffer ids
- private uint _instanceSsbo;
- private uint _batchSsbo;
- private uint _indirectBuffer;
- private int _instanceSsboCapacityBytes;
- private int _batchSsboCapacityBytes;
- private int _indirectBufferCapacityBytes;
-
- // Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
- // _instanceSsbo. One uint per instance selecting its CellClip slot. In U.3
- // this is ALL ZEROS (every instance → slot 0 → no-clip), so the render is
- // identical to pre-U.3. U.4 populates real slot indices.
- private uint _clipSlotSsbo;
- private int _clipSlotSsboCapacityBytes;
+ // Phase U.3: per-instance clip-slot data (binding=3 on the RHI ring). One
+ // uint per instance selecting its CellClip slot. In U.3 this is ALL ZEROS
+ // (every instance → slot 0 → no-clip), so the render is identical to
+ // pre-U.3. U.4 populates real slot indices.
private uint[] _clipSlotData = new uint[256];
// Fix B (A7 #3): per-OBJECT light selection (minimize_object_lighting). Two
- // SSBOs replace the single global nearest-8-to-CAMERA UBO set for point/spot
- // lights — see mesh_modern.vert binding=4/5. _globalLightsSsbo (binding=4)
- // holds the per-frame point-light snapshot (LightManager.PointSnapshot);
- // _instLightSetSsbo (binding=5) holds MaxLightsPerObject int indices per
- // instance INTO it (-1 = unused), laid out parallel to _instanceSsbo.
- private uint _globalLightsSsbo;
- private uint _instLightSetSsbo;
- private int _globalLightsSsboCapacityBytes;
- private int _instLightSetSsboCapacityBytes;
+ // ring sections replace the single global nearest-8-to-CAMERA UBO set for
+ // point/spot lights — see mesh_modern.vert binding=4/5. The global-lights
+ // section (binding=4) holds the per-frame point-light snapshot
+ // (LightManager.PointSnapshot); the light-set section (binding=5) holds
+ // MaxLightsPerObject int indices per instance INTO it (-1 = unused), laid
+ // out parallel to the instance data.
private int[] _lightSetData = new int[256 * LightManager.MaxLightsPerObject];
private float[] _globalLightData = new float[GlobalLightPacker.FloatsPerLight * 16]; // 16 floats (4 vec4) per GlobalLight
// #142: per-instance "indoor" flag (binding=6), one uint per instance, parallel
- // to _instanceSsbo. 1 = object parented to an EnvCell (skip the sun in the
- // shader's uLightingMode==0 branch); 0 = outdoor object (gets the sun).
- // Mechanically a clone of _clipSlotData / _clipSlotSsbo.
- private uint _instIndoorSsbo;
- private int _instIndoorSsboCapacityBytes;
+ // to the instance data. 1 = object parented to an EnvCell (skip the sun in
+ // the shader's uLightingMode==0 branch); 0 = outdoor object (gets the sun).
+ // Mechanically a clone of _clipSlotData.
private uint[] _indoorData = new uint[256];
// #188: per-instance opacity multiplier (binding=7), one float per
- // instance, parallel to _instanceSsbo. 1.0 = unmodified (the dat's own
+ // instance, parallel to the instance data. 1.0 = unmodified (the dat's own
// material/texture alpha, untouched); < 1.0 multiplies the shader's
// sampled alpha for an entity mid-TransparentPartHook fade. Mechanically
- // a clone of _indoorData / _instIndoorSsbo, one binding higher.
- private uint _instAlphaSsbo;
- private int _instAlphaSsboCapacityBytes;
+ // a clone of _indoorData, one binding higher.
private float[] _alphaData = new float[256];
+
+ private bool _dynamicFrameStarted;
+
+ // Campaign V slice V11: the raw-GL upload path (and its per-frame triple-
+ // buffered SSBO pool) is gone — the RHI arm's frame ring
+ // (WbDrawDispatcher.Rhi.cs) owns the equivalent ring allocations now, so
+ // there is no dynamic buffer set left to count. Kept for the diagnostic
+ // consumer (RenderFrameDiagnosticSources) that reads this alongside the
+ // other renderers'.
+ internal int DynamicBufferSetCount => 0;
+
// Retail SmartBox click confirmation: per-instance CMaterial luminosity /
// diffuse replacement (binding=8), parallel to the transform buffer.
- private uint _instSelectionLightingSsbo;
- private int _instSelectionLightingSsboCapacityBytes;
-
- // Campaign V slice V4t (2026-07-28): the interim per-renderer
- // GlBindlessHandleTable is retired. Batch data now carries the device's own
- // GpuTextureSlot, produced by the texture stack at upload time, so this
- // renderer only has to flush and bind that one table at
- // GpuBindingModel.StorageTextureTable before each of its raw-GL draws — it
- // does not submit through the encoder, so it never reaches
- // GlGpuDevice.FlushBeforeDraw. Taken from the mesh adapter rather than
- // wired separately: a batch's slot and the table that resolves it must come
- // from the same device by construction.
- private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
- _meshAdapter.WorldTextureTable;
-
- private sealed class DynamicBufferSet
- {
- public uint InstanceSsbo;
- public uint BatchSsbo;
- public uint IndirectBuffer;
- public uint ClipSlotSsbo;
- public uint GlobalLightsSsbo;
- public uint InstanceLightSetSsbo;
- public uint InstanceIndoorSsbo;
- public uint InstanceAlphaSsbo;
- public uint InstanceSelectionLightingSsbo;
- public int InstanceCapacityBytes;
- public int BatchCapacityBytes;
- public int IndirectCapacityBytes;
- public int ClipSlotCapacityBytes;
- public int GlobalLightsCapacityBytes;
- public int InstanceLightSetCapacityBytes;
- public int InstanceIndoorCapacityBytes;
- public int InstanceAlphaCapacityBytes;
- public int InstanceSelectionLightingCapacityBytes;
- }
-
- private readonly List[] _dynamicBufferSetsByFrame =
- [[], [], []];
- private int _dynamicFrameSlot;
- private int _dynamicBufferSetCursor;
- private bool _dynamicFrameStarted;
- private DynamicBufferSet? _activeDynamicBufferSet;
-
- internal int DynamicBufferSetCount =>
- _dynamicBufferSetsByFrame.Sum(frameSets => frameSets.Count);
private Vector2[] _selectionLightingData = new Vector2[256];
// This frame's point-light snapshot, handed in by GameWindow before Draw via
// SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1).
@@ -554,13 +497,13 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
private bool _currentEntityIndoor;
private Vector2 _currentEntitySelectionLighting = new(0f, 1f);
- // Phase U.3: the SHARED per-cell clip-region SSBO (binding=2), owned by the
- // GameWindow-level ClipFrame and handed to us via SetClipRegionSsbo. When 0
- // (not yet wired), we bind our OWN fallback no-clip region buffer below so the
- // shader never reads an unbound SSBO. The fallback holds exactly slot 0
- // (count 0 = pass-all), matching ClipFrame.NoClip's slot 0.
+ // Phase U.3: the SHARED per-cell clip-region SSBO (binding=2) id, owned by
+ // the GL-arm ClipFrame and handed in via SetClipRegionSsbo. Campaign V
+ // slice V11: the raw-GL world path that read this is gone, and the RHI arm
+ // binds clip regions from IWorldPassScope.Sections instead (see
+ // WbDrawDispatcher.Rhi.cs), so this is now write-only — kept because
+ // GlWorldPassSurface still calls the setter unconditionally.
private uint _sharedClipRegionSsbo;
- private uint _fallbackClipRegionSsbo;
// Phase U.4: per-frame clip-slot routing handed in via SetClipRouting before
// each Draw. When _clipRoutingActive is false (the U.3 path / outdoor root /
@@ -786,91 +729,28 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
// render-thread only like the walk itself.
private static readonly Dictionary _walkRejectCounts = new();
- // CPU + GPU timing for [WB-DIAG] under ACDREAM_WB_DIAG=1.
+ // CPU + GPU timing for [WB-DIAG] under ACDREAM_WB_DIAG=1. The GPU samples
+ // are written by the RHI arm's SampleRhiTimers (WbDrawDispatcher.Rhi.cs)
+ // from the device's own timer pool; the raw-GL query-object ring that used
+ // to feed them is gone with the raw-GL draw path.
private readonly System.Diagnostics.Stopwatch _cpuStopwatch = new();
private readonly long[] _cpuSamples = new long[256]; // microseconds
private int _cpuSampleCursor;
- // GPU timing uses a ring of 3 query-pair slots so the read of frame N-3's
- // result lands when the GPU has finished (~50ms after issue on a typical
- // 60fps frame). Ring of 3 is the vendor-neutral choice: NVIDIA drivers with
- // triple-buffering+vsync can queue ~3 frames ahead, AMD typically 1-2,
- // Intel iGPUs vary. ResultAvailable is the safety guard if the GPU is
- // still working when we try to read.
- private const int GpuQueryRingDepth = 3;
- private readonly uint[] _gpuQueryOpaque = new uint[GpuQueryRingDepth];
- private readonly uint[] _gpuQueryTransparent = new uint[GpuQueryRingDepth];
- // #125: a glGenQueries name does not become a QUERY OBJECT until its first
- // glBeginQuery — GetQueryObject on a never-begun name is GL_INVALID_OPERATION.
- // The N.6 ring assumed ONE Draw per frame with both passes always non-empty;
- // the pview pipeline issues MANY small Draws per frame (landscape slices,
- // per-cell buckets, dynamics), where zero-draw passes routinely skip
- // BeginQuery. Under ACDREAM_WB_DIAG=1 the slot read then queued an
- // InvalidOperation EVERY frame — silently, until WB's diligent texture-path
- // glGetError checks ate the stale errors and treated their own successful
- // uploads as failures ([wb-error] + sticky drop) and ProcessDirtyUpdates'
- // check threw (process death; tower-wbdiag3.log). Track which slots were
- // actually begun and only read those.
- private readonly bool[] _gpuQueryOpaqueBegun = new bool[GpuQueryRingDepth];
- private readonly bool[] _gpuQueryTransparentBegun = new bool[GpuQueryRingDepth];
- private int _gpuQueryFrameIndex;
private readonly long[] _gpuSamples = new long[256]; // microseconds
private int _gpuSampleCursor;
- private bool _gpuQueriesInitialized;
-
- // Constructor accessibility is internal because EntityClassificationCache
- // is internal — a public ctor with an internal-typed parameter would be
- // an inconsistent-accessibility error. The dispatcher is constructed
- // exclusively from GameWindow (same assembly), so internal is fine.
- internal WbDrawDispatcher(
- GL gl,
- Shader shader,
- TextureCache textures,
- WbMeshAdapter meshAdapter,
- EntitySpawnAdapter entitySpawnAdapter,
- BindlessSupport bindless,
- EntityClassificationCache classificationCache,
- AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades,
- IRetailSelectionRenderSink? selectionSink = null,
- RetailAlphaQueue? alphaQueue = null,
- long? alphaScratchBudgetBytes = null)
- {
- ArgumentNullException.ThrowIfNull(gl);
- ArgumentNullException.ThrowIfNull(shader);
- ArgumentNullException.ThrowIfNull(textures);
- ArgumentNullException.ThrowIfNull(meshAdapter);
- ArgumentNullException.ThrowIfNull(entitySpawnAdapter);
- ArgumentNullException.ThrowIfNull(classificationCache);
- ArgumentNullException.ThrowIfNull(translucencyFades);
-
- _gl = gl;
- _shader = shader;
- _textures = textures;
- _meshAdapter = meshAdapter;
- _entitySpawnAdapter = entitySpawnAdapter;
- _cache = classificationCache;
- _translucencyFades = translucencyFades;
- _selectionSink = selectionSink;
- _selectionLighting = selectionSink as IRetailSelectionLightingSource;
- _alphaQueue = alphaQueue;
- _alphaSource = new AlphaDrawSource(this);
- long scratchBudget = alphaScratchBudgetBytes
- ?? AlphaScratchBudgetProfile.Create(
- ResidencyBudgetOptions.Default.AlphaScratchBytes).DispatcherBytes;
- _alphaScratchPolicy =
- new RetainedScratchCapacityPolicy(scratchBudget);
- _bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
- }
///
- /// Selects the fence-protected frame slot and resets its draw-call cursor.
- /// Every Draw/alpha preparation in one frame receives a distinct buffer
- /// set, so later per-cell submissions cannot overwrite an earlier draw's
- /// still-pending SSBO and indirect-command data.
+ /// Marks the start of a fence-protected frame.
+ /// is the shared GPU frame-ring slot every renderer in the frame receives
+ /// (see ) — the RHI arm's own
+ /// ring allocations (WbDrawDispatcher.Rhi.cs) come from the current
+ /// IGpuFrame instead, so this dispatcher no longer indexes its own
+ /// buffer-set pool by it; the parameter is kept so every renderer's
+ /// BeginFrame call stays uniform.
///
public void BeginFrame(int frameSlot)
{
- if ((uint)frameSlot >= (uint)_dynamicBufferSetsByFrame.Length)
- throw new ArgumentOutOfRangeException(nameof(frameSlot));
+ _ = frameSlot;
if (_groupFrame == long.MaxValue)
throw new InvalidOperationException("Instance-group frame identity was exhausted.");
@@ -881,10 +761,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
_groups,
_retiredGroupKeys,
_groupFrame - 1);
- _dynamicFrameSlot = frameSlot;
- _dynamicBufferSetCursor = 0;
_dynamicFrameStarted = true;
- _activeDynamicBufferSet = null;
_currentRenderSceneObserver?.BeginDispatcherFrame();
}
@@ -908,10 +785,12 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
///
/// Phase U.3: hand the dispatcher the SHARED per-cell clip-region SSBO
- /// (binding=2) that created. The
- /// dispatcher re-binds it to binding=2 immediately before each MDI so a
- /// consumer that touched binding=2 in between can't leave it pointing
- /// elsewhere. Pass 0 to fall back to the internal no-clip region buffer.
+ /// (binding=2) that created. Campaign
+ /// V slice V11: the raw-GL draw path that rebound this id is gone —
+ /// GlWorldPassSurface still calls this setter unconditionally, so it
+ /// is kept as a harmless store; the RHI arm binds clip regions from
+ /// IWorldPassScope.Sections instead (see
+ /// WbDrawDispatcher.Rhi.cs).
///
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
@@ -1513,6 +1392,11 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
foreach (InstanceGroup group in _groups.Values)
group.ClearPerInstanceData();
+ // Campaign V slice V11: no longer read for its own sake (the raw-GL
+ // VAO bind it fed is gone) — kept only because the packed-oracle
+ // partial (WbDrawDispatcher.PackedOracle.cs) mirrors this exact
+ // "first non-zero mesh id" computation and shares ExecuteClassifiedGroups'
+ // signature with it.
uint anyVao = 0;
// Project the 5-tuple enumerable into LandblockEntry records for WalkEntities.
@@ -2039,8 +1923,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
// histogram exactly as it will be uploaded to binding=3 (grp.Slots) plus the
// culled-entity count. Routed draws only (the landscape pass under DrawInside) so the
// unrouted per-cell bucket draws don't oscillate the print-on-change signature.
- // Emitted BEFORE the anyVao / totalInstances early-outs so an all-culled frame still
- // reports (inst=0).
+ // Emitted BEFORE the MeshSourceReady / totalInstances early-outs so an
+ // all-culled frame still reports (inst=0).
if (RenderingDiagnostics.ProbeClipRouteEnabled && _clipRoutingActive)
EmitClipRouteDispatchProbe(probeCulledEntities);
@@ -2057,49 +1941,29 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
}
///
- /// Whether there is a mesh source to draw from.
- ///
- /// Campaign V slice V6j: on GL that question is "did we find a vertex
- /// array", which answers. The encoder arm has no
- /// vertex array — the pipeline owns one shaped by
- /// GpuVertexLayout.WorldMesh — so it asks the arena the
- /// backend-neutral form of the same question, which V6i-3 published as
- /// HasStores.
+ /// Whether there is a mesh source to draw from. The encoder arm has no
+ /// vertex array of its own — the pipeline owns one shaped by
+ /// GpuVertexLayout.WorldMesh — so this asks the shared mesh arena
+ /// directly, the backend-neutral question V6i-3 published as
+ /// HasStores.
///
- private bool MeshSourceReady(uint anyVao) =>
- _gl is not null
- ? anyVao != 0
- : _meshAdapter.MeshManager?.GlobalBuffer is { HasStores: true };
+ private bool MeshSourceReady() =>
+ _meshAdapter.MeshManager?.GlobalBuffer is { HasStores: true };
private bool BeginEntityDispatch(
ICamera camera,
out Matrix4x4 viewProjection,
out Vector3 cameraWorldPosition)
{
- _shader?.Use();
_selectionLighting?.TickLighting();
_indoorProbeFrameCounter++;
viewProjection = camera.View * camera.Projection;
- _shader?.SetMatrix4("uViewProjection", viewProjection);
- _shader?.SetInt("uLightingMode", 0);
- _shader?.SetInt(
- "uLightDebug",
- RenderingDiagnostics.LightDebugMode);
_missRequested.Clear();
bool diagnosticsEnabled = string.Equals(
Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"),
"1",
StringComparison.Ordinal);
- if (diagnosticsEnabled && _gl is not null && !_gpuQueriesInitialized)
- {
- for (int index = 0; index < GpuQueryRingDepth; index++)
- {
- _gpuQueryOpaque[index] = _gl.GenQuery();
- _gpuQueryTransparent[index] = _gl.GenQuery();
- }
- _gpuQueriesInitialized = true;
- }
_cpuStopwatch.Restart();
cameraWorldPosition = Vector3.Zero;
@@ -2108,6 +1972,13 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
return diagnosticsEnabled;
}
+ ///
+ /// is no longer read — it survives as a
+ /// parameter only because WbDrawDispatcher.PackedOracle.cs calls
+ /// this positionally with its own mirrored classification's
+ /// PackedRangeClassification.AnyVao and that partial is out of
+ /// scope for this collapse.
+ ///
private void ExecuteClassifiedGroups(
Matrix4x4 vp,
Vector3 camPos,
@@ -2120,7 +1991,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
bool observeCurrentPath)
{
// Nothing visible — skip the pass entirely.
- if (!MeshSourceReady(anyVao))
+ if (!MeshSourceReady())
{
LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0);
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
@@ -2277,230 +2148,18 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
deferTransparent,
camPos);
- // Campaign V slice V6j: on the encoder arm every per-frame upload below
- // is a frame ring slice bound through the borrowed world pass, which
- // retires the buffer-set pool structurally. See WbDrawDispatcher.Rhi.cs.
- if (_gl is null)
- {
- SubmitRhi(vp, immediateInstances, totalDraws, diag);
- _cpuStopwatch.Stop();
- if (diag)
- {
- long rhiCpuUs = _cpuStopwatch.ElapsedTicks * 1_000_000L
- / System.Diagnostics.Stopwatch.Frequency;
- _cpuSamples[_cpuSampleCursor] = rhiCpuUs;
- _cpuSampleCursor = (_cpuSampleCursor + 1) % _cpuSamples.Length;
- _drawsIssued += _opaqueDrawCount + _transparentDrawCount;
- _instancesIssued += totalInstances;
- MaybeFlushDiag();
- }
- return;
- }
-
- // ── Phase 5: upload four buffers ────────────────────────────────────
- ActivateNextDynamicBufferSet();
- fixed (float* ip = _instanceData)
- UploadSsbo(_instanceSsbo, 0, ref _instanceSsboCapacityBytes,
- ip, immediateInstances * 16 * sizeof(float));
-
- fixed (BatchData* bp = _batchData)
- UploadSsbo(_batchSsbo, 1, ref _batchSsboCapacityBytes,
- bp, totalDraws * sizeof(BatchData));
-
- // Phase U.4: per-instance clip-slot buffer (binding=3), one uint per
- // instance, laid out parallel to _instanceData in Phase 3's group loop so
- // instanceClipSlot[instanceIndex] tracks Instances[instanceIndex]. On the
- // U.3 / outdoor path every entry is 0 ⇒ slot 0 ⇒ no-clip (identical to
- // U.3); under indoor routing it holds the per-instance slot from
- // ResolveEntitySlot. No clear here — Phase 3 wrote exactly immediateInstances
- // entries; only [0..immediateInstances) is uploaded, so any stale tail is
- // never read by the shader.
- fixed (uint* sp = _clipSlotData)
- UploadSsbo(_clipSlotSsbo, 3, ref _clipSlotSsboCapacityBytes,
- sp, immediateInstances * sizeof(uint));
-
- // #142: per-instance indoor flag buffer (binding=6), one uint per instance,
- // laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances)
- // is uploaded — stale tail never read (same guarantee as clip-slot above).
- fixed (uint* dp = _indoorData)
- UploadSsbo(_instIndoorSsbo, 6, ref _instIndoorSsboCapacityBytes,
- dp, immediateInstances * sizeof(uint));
-
- // #188: per-instance opacity buffer (binding=7), one float per instance,
- // laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances)
- // is uploaded — stale tail never read (same guarantee as clip-slot above).
- fixed (float* ap = _alphaData)
- UploadSsbo(_instAlphaSsbo, 7, ref _instAlphaSsboCapacityBytes,
- ap, immediateInstances * sizeof(float));
-
- // SmartBox click lighting: x=luminosity, y=diffuse. mesh_modern.vert
- // reads this only for the object path (uLightingMode=0), so EnvCell's
- // independent mode-1 renderer does not consume this binding.
- fixed (Vector2* hp = _selectionLightingData)
- UploadSsbo(_instSelectionLightingSsbo, 8, ref _instSelectionLightingSsboCapacityBytes,
- hp, immediateInstances * sizeof(float) * 2);
-
- // Fix B: global point-light buffer (binding=4) + per-instance light-set
- // buffer (binding=5). The global buffer is this frame's PointSnapshot; the
- // per-instance buffer holds 8 int indices into it per instance, laid out
- // parallel to _instanceData in Phase 3. Both bound with ≥1 element so the
- // shader never reads an unbound SSBO on a no-lights frame.
- UploadGlobalLights();
- fixed (int* lp = _lightSetData)
- UploadSsbo(_instLightSetSsbo, 5, ref _instLightSetSsboCapacityBytes,
- lp, immediateInstances * LightManager.MaxLightsPerObject * sizeof(int));
-
- // Campaign V slice V2 (binding=9): uploads only when ToInput registered
- // a genuinely new handle this frame; otherwise just rebinds.
- FlushAndBindTextureTable();
-
- fixed (DrawElementsIndirectCommand* cp = _indirectCommands)
- {
- UploadDynamicBuffer(
- BufferTargetARB.DrawIndirectBuffer,
- _indirectBuffer,
- ref _indirectBufferCapacityBytes,
- cp,
- totalDraws * sizeof(DrawElementsIndirectCommand));
- }
-
- PersistActiveDynamicBufferCapacities();
-
- // Phase U.3: bind the SHARED per-cell clip-region SSBO (binding=2). The
- // GameWindow-level ClipFrame already uploaded + bound it this frame; we
- // re-bind defensively in case another consumer touched binding=2 since.
- // When no shared id is set (0), bind our own no-clip fallback so the
- // shader never reads an unbound SSBO at binding=2.
- BindClipRegionBinding2();
-
- // ── Phase 6: bind global VAO once ───────────────────────────────────
- _gl.BindVertexArray(anyVao);
-
- if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal))
- _gl.Disable(EnableCap.CullFace);
-
- // GPU timing: compute this frame's ring slot. We read frame N-3's
- // result (the oldest data in the ring) before overwriting it with
- // frame N's queries. Hoisted to function scope so both the opaque
- // and transparent passes below can reference gpuQuerySlot. See spec
- // §3 Q1/Q2 + §4 in
- // docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md.
- int gpuQuerySlot = _gpuQueryFrameIndex % GpuQueryRingDepth;
- // diag is part of the gate so the read/issue/increment trio stays
- // symmetric — without it, toggling ACDREAM_WB_DIAG mid-session would
- // freeze the frame counter (gated by diag below) while the read kept
- // re-reading the same slot, producing duplicate stale samples.
- if (diag && _gpuQueriesInitialized && _gpuQueryFrameIndex >= GpuQueryRingDepth)
- {
- // #125: only read slots whose query objects were actually BEGUN (a
- // zero-draw pass skips BeginQuery; reading a never-begun name is
- // GL_INVALID_OPERATION). A pass that never ran contributes 0 ns.
- ulong opaqueNs = 0, transNs = 0;
- bool anyRead = false, allAvailable = true;
- if (_gpuQueryOpaqueBegun[gpuQuerySlot])
- {
- _gl.GetQueryObject(_gpuQueryOpaque[gpuQuerySlot], QueryObjectParameterName.ResultAvailable, out int availO);
- if (availO != 0)
- {
- _gl.GetQueryObject(_gpuQueryOpaque[gpuQuerySlot], QueryObjectParameterName.Result, out opaqueNs);
- anyRead = true;
- }
- else allAvailable = false;
- }
- if (_gpuQueryTransparentBegun[gpuQuerySlot])
- {
- _gl.GetQueryObject(_gpuQueryTransparent[gpuQuerySlot], QueryObjectParameterName.ResultAvailable, out int availT);
- if (availT != 0)
- {
- _gl.GetQueryObject(_gpuQueryTransparent[gpuQuerySlot], QueryObjectParameterName.Result, out transNs);
- anyRead = true;
- }
- else allAvailable = false;
- }
- // If a begun query isn't available yet the sample is dropped
- // silently. MedianMicros computes over the non-zero subset, so
- // dropped samples don't poison the median.
- if (anyRead && allAvailable)
- {
- long gpuUs = (long)((opaqueNs + transNs) / 1000UL);
- _gpuSamples[_gpuSampleCursor] = gpuUs;
- _gpuSampleCursor = (_gpuSampleCursor + 1) % _gpuSamples.Length;
- }
- }
-
- // ── Phase 7: opaque pass ─────────────────────────────────────────────
- if (_opaqueDrawCount > 0)
- {
- _gl.Disable(EnableCap.Blend);
- _gl.DepthMask(true);
- // A.5 T20: enable A2C for ClipMap foliage — GPU derives sample mask
- // from the alpha written by mesh_modern.frag so foliage edges are
- // smooth under MSAA 4x. A no-op for fully-opaque (α=1) batches.
- // A.5 T22.5: gated by AlphaToCoverage property so Low/Medium presets
- // (no MSAA) skip the unnecessary GL state change.
- if (AlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage);
- _shader!.SetInt("uRenderPass", 0);
- // Phase Post-A.5 (ISSUE #52, 2026-05-10): opaque section of
- // Batches[] starts at index 0. See uDrawIDOffset comment in
- // mesh_modern.vert for why this is needed.
- _shader.SetInt("uDrawIDOffset", 0);
- _gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
- if (diag && _gpuQueriesInitialized)
- {
- _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryOpaque[gpuQuerySlot]);
- _gpuQueryOpaqueBegun[gpuQuerySlot] = true; // #125
- }
- DrawIndirectRange(0, _opaqueDrawCount);
- if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed);
- if (AlphaToCoverage) _gl.Disable(EnableCap.SampleAlphaToCoverage);
- }
-
- // ── Phase 8: transparent pass ────────────────────────────────────────
- if (_transparentDrawCount > 0)
- {
- _gl.Enable(EnableCap.Blend);
- _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
- _gl.DepthMask(false);
- // Phase Post-A.5 (ISSUE #52, 2026-05-10): transparent section of
- // Batches[] starts at index _opaqueDrawCount. Without this offset,
- // each transparent draw reads BatchData[0..transparentCount) — the
- // OPAQUE section — and the lifestone crystal's apparent texture
- // flickers to whatever opaque batch sorted first that frame. See
- // uDrawIDOffset comment in mesh_modern.vert.
- _shader!.SetInt("uDrawIDOffset", _opaqueDrawCount);
- // Closed-shell translucent meshes still need culling, but the
- // cull side must come from each dat batch just like the opaque
- // section. BuildIndirectArrays preserves CullMode in _drawCullModes.
- _gl.FrontFace(FrontFaceDirection.CW);
- _shader.SetInt("uRenderPass", 1);
- if (diag && _gpuQueriesInitialized)
- {
- _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryTransparent[gpuQuerySlot]);
- _gpuQueryTransparentBegun[gpuQuerySlot] = true; // #125
- }
- DrawIndirectRange(_opaqueDrawCount, _transparentDrawCount);
- if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed);
- _gl.DepthMask(true);
- _gl.Disable(EnableCap.Blend);
- }
-
- _gl.Disable(EnableCap.CullFace);
- _gl.BindVertexArray(0);
-
+ // Campaign V slice V11: every per-frame upload is a frame ring slice
+ // bound through the borrowed world pass, which retires the buffer-set
+ // pool structurally. See WbDrawDispatcher.Rhi.cs.
+ SubmitRhi(vp, immediateInstances, totalDraws, diag);
_cpuStopwatch.Stop();
-
if (diag)
{
- long cpuUs = _cpuStopwatch.ElapsedTicks * 1_000_000L / System.Diagnostics.Stopwatch.Frequency;
+ long cpuUs = _cpuStopwatch.ElapsedTicks * 1_000_000L
+ / System.Diagnostics.Stopwatch.Frequency;
_cpuSamples[_cpuSampleCursor] = cpuUs;
_cpuSampleCursor = (_cpuSampleCursor + 1) % _cpuSamples.Length;
-
- // GPU sample read happens BEFORE issuing the next frame's queries
- // (see step 1.3 above). Increment the frame counter here so the
- // next call computes a fresh slot.
- if (_gpuQueriesInitialized) _gpuQueryFrameIndex++;
-
- _drawsIssued += _opaqueDrawCount + _transparentDrawCount;
+ _drawsIssued += _opaqueDrawCount + _transparentDrawCount;
_instancesIssued += totalInstances;
MaybeFlushDiag();
}
@@ -3000,7 +2659,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
return;
GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer;
- if (global is null || !MeshSourceReady(global.VAO))
+ if (global is null || !MeshSourceReady())
return;
int count = tokens.Length;
@@ -3039,18 +2698,11 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
// One upload per source per sorted alpha scope. RetailAlphaQueue later
// draws contiguous ranges from this immutable prepared payload; it must
// never overwrite these buffers for every short mesh/particle run.
- if (_gl is null)
- {
- // A ring allocation cannot outlive its frame as a ref struct, but its
- // buffer, offset and size can be stored — so the payload is written
- // once here and bound many times below without recopying.
- PrepareRhiAlphaSections(count);
- return;
- }
-
- ActivateNextDynamicBufferSet();
- UploadDeferredAlphaBuffers(count);
- PersistActiveDynamicBufferCapacities();
+ //
+ // A ring allocation cannot outlive its frame as a ref struct, but its
+ // buffer, offset and size can be stored — so the payload is written
+ // once here and bound many times below without recopying.
+ PrepareRhiAlphaSections(count);
}
private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
@@ -3062,60 +2714,10 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
throw new ArgumentOutOfRangeException(nameof(firstPreparedDraw));
GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer;
- if (global is null || !MeshSourceReady(global.VAO))
+ if (global is null || !MeshSourceReady())
return;
- if (_gl is null)
- {
- DrawPreparedAlphaBatchRhi(global, firstPreparedDraw, drawCount);
- return;
- }
-
- _shader!.Use();
- _shader.SetMatrix4("uViewProjection", _deferredAlphaViewProjection);
- _shader.SetInt("uLightingMode", 0);
- _shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
- _shader.SetInt("uRenderPass", 1);
- _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 0, _instanceSsbo);
- _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 1, _batchSsbo);
- _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 3, _clipSlotSsbo);
- _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 4, _globalLightsSsbo);
- _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 5, _instLightSetSsbo);
- _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 6, _instIndoorSsbo);
- _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 7, _instAlphaSsbo);
- _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 8, _instSelectionLightingSsbo);
- // Campaign V slice V2: already flushed in UploadDeferredAlphaBuffers
- // (this is the same device table as the main draw path); just rebind.
- _gl.BindBufferBase(
- BufferTargetARB.ShaderStorageBuffer,
- AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
- WorldTextureTable.TextureTableGlName);
- BindClipRegionBinding2();
- _gl.BindVertexArray(global.VAO);
- _gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
- _gl.Enable(EnableCap.DepthTest);
- _gl.Enable(EnableCap.Blend);
- _gl.DepthMask(false);
- _gl.FrontFace(FrontFaceDirection.CW);
-
- int runStart = firstPreparedDraw;
- int preparedEnd = firstPreparedDraw + drawCount;
- while (runStart < preparedEnd)
- {
- TranslucencyKind blend = _deferredAlphaKinds[runStart];
- int runEnd = runStart + 1;
- while (runEnd < preparedEnd && _deferredAlphaKinds[runEnd] == blend)
- runEnd++;
-
- ApplyRetailBlend(blend);
- DrawIndirectRange(runStart, runEnd - runStart);
- runStart = runEnd;
- }
-
- _gl.DepthMask(true);
- _gl.Disable(EnableCap.Blend);
- _gl.Disable(EnableCap.CullFace);
- _gl.BindVertexArray(0);
+ DrawPreparedAlphaBatchRhi(global, firstPreparedDraw, drawCount);
}
private void EnsureDeferredAlphaCapacity(int count)
@@ -3201,60 +2803,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
_deferredAlpha.Capacity = targetCapacity;
}
- private void UploadDeferredAlphaBuffers(int count)
- {
- fixed (float* p = _instanceData)
- UploadSsbo(_instanceSsbo, 0, ref _instanceSsboCapacityBytes,
- p, count * 16 * sizeof(float));
- fixed (BatchData* p = _batchData)
- UploadSsbo(_batchSsbo, 1, ref _batchSsboCapacityBytes,
- p, count * sizeof(BatchData));
- fixed (uint* p = _clipSlotData)
- UploadSsbo(_clipSlotSsbo, 3, ref _clipSlotSsboCapacityBytes,
- p, count * sizeof(uint));
- fixed (int* p = _lightSetData)
- UploadSsbo(_instLightSetSsbo, 5, ref _instLightSetSsboCapacityBytes,
- p, count * LightManager.MaxLightsPerObject * sizeof(int));
- fixed (uint* p = _indoorData)
- UploadSsbo(_instIndoorSsbo, 6, ref _instIndoorSsboCapacityBytes,
- p, count * sizeof(uint));
- fixed (float* p = _alphaData)
- UploadSsbo(_instAlphaSsbo, 7, ref _instAlphaSsboCapacityBytes,
- p, count * sizeof(float));
- fixed (Vector2* p = _selectionLightingData)
- UploadSsbo(_instSelectionLightingSsbo, 8, ref _instSelectionLightingSsboCapacityBytes,
- p, count * sizeof(float) * 2);
- UploadGlobalLights();
- // Campaign V slice V2 (binding=9): flush/rebind the device texture table
- // before DrawPreparedAlphaBatch, which submits without going through
- // here again.
- FlushAndBindTextureTable();
-
- fixed (DrawElementsIndirectCommand* p = _indirectCommands)
- {
- UploadDynamicBuffer(
- BufferTargetARB.DrawIndirectBuffer,
- _indirectBuffer,
- ref _indirectBufferCapacityBytes,
- p,
- count * sizeof(DrawElementsIndirectCommand));
- }
- }
-
- private void ApplyRetailBlend(TranslucencyKind blend)
- {
- _gl!.BlendFunc(
- blend == TranslucencyKind.InvAlpha
- ? BlendingFactor.OneMinusSrcAlpha
- : BlendingFactor.SrcAlpha,
- blend switch
- {
- TranslucencyKind.Additive => BlendingFactor.One,
- TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha,
- _ => BlendingFactor.OneMinusSrcAlpha,
- });
- }
-
private static int CompareOpaqueSubmissionOrder(InstanceGroup a, InstanceGroup b)
{
int cull = ((int)a.CullMode).CompareTo((int)b.CullMode);
@@ -3284,282 +2832,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
return runs;
}
- private unsafe void DrawIndirectRange(int startCommand, int commandCount)
- {
- int end = startCommand + commandCount;
- int command = startCommand;
- while (command < end)
- {
- var cullMode = _drawCullModes[command];
- ApplyCullMode(cullMode);
-
- int runCount = 1;
- while (command + runCount < end && _drawCullModes[command + runCount] == cullMode)
- runCount++;
-
- // Each glMultiDrawElementsIndirect call restarts gl_DrawID at 0.
- // Because this method splits one logical opaque/transparent pass
- // into CullMode runs, the shader must receive the absolute command
- // index for this run or it will read BatchData[0] again and bind
- // the wrong texture for later runs.
- _shader!.SetInt("uDrawIDOffset", command);
- _gl!.MultiDrawElementsIndirect(
- PrimitiveType.Triangles,
- DrawElementsType.UnsignedShort,
- (void*)(command * DrawCommandStride),
- (uint)runCount,
- (uint)DrawCommandStride);
-
- command += runCount;
- }
- }
-
- private void ApplyCullMode(CullMode mode)
- {
- // WB BaseObjectRenderManager.cs:850-866 applies CullMode per MDI group.
- // WB GameScene.cs:843 sets FrontFace(CW) globally; SetCullMode then
- // only chooses front/back culling. Keep the same convention here so
- // splitting MDI commands by CullMode cannot resurrect stale CCW state.
- _gl!.FrontFace(FrontFaceDirection.CW);
- 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;
- }
- }
-
- private void ActivateNextDynamicBufferSet()
- {
- if (!_dynamicFrameStarted)
- throw new InvalidOperationException("BeginFrame must be called before drawing world entities.");
-
- List slotSets = _dynamicBufferSetsByFrame[_dynamicFrameSlot];
- if (_dynamicBufferSetCursor == slotSets.Count)
- slotSets.Add(CreateDynamicBufferSet());
-
- DynamicBufferSet set = slotSets[_dynamicBufferSetCursor++];
- _activeDynamicBufferSet = set;
- _instanceSsbo = set.InstanceSsbo;
- _batchSsbo = set.BatchSsbo;
- _indirectBuffer = set.IndirectBuffer;
- _clipSlotSsbo = set.ClipSlotSsbo;
- _globalLightsSsbo = set.GlobalLightsSsbo;
- _instLightSetSsbo = set.InstanceLightSetSsbo;
- _instIndoorSsbo = set.InstanceIndoorSsbo;
- _instAlphaSsbo = set.InstanceAlphaSsbo;
- _instSelectionLightingSsbo = set.InstanceSelectionLightingSsbo;
- _instanceSsboCapacityBytes = set.InstanceCapacityBytes;
- _batchSsboCapacityBytes = set.BatchCapacityBytes;
- _indirectBufferCapacityBytes = set.IndirectCapacityBytes;
- _clipSlotSsboCapacityBytes = set.ClipSlotCapacityBytes;
- _globalLightsSsboCapacityBytes = set.GlobalLightsCapacityBytes;
- _instLightSetSsboCapacityBytes = set.InstanceLightSetCapacityBytes;
- _instIndoorSsboCapacityBytes = set.InstanceIndoorCapacityBytes;
- _instAlphaSsboCapacityBytes = set.InstanceAlphaCapacityBytes;
- _instSelectionLightingSsboCapacityBytes = set.InstanceSelectionLightingCapacityBytes;
- }
-
- private DynamicBufferSet CreateDynamicBufferSet()
- {
- var set = new DynamicBufferSet();
- try
- {
- set.InstanceSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity instance SSBO");
- set.BatchSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity batch SSBO");
- set.IndirectBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating entity indirect buffer");
- set.ClipSlotSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity clip-slot SSBO");
- set.GlobalLightsSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity global-light SSBO");
- set.InstanceLightSetSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity light-set SSBO");
- set.InstanceIndoorSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity indoor SSBO");
- set.InstanceAlphaSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity alpha SSBO");
- set.InstanceSelectionLightingSsbo = TrackedGlResource.CreateBuffer(
- _gl!,
- "creating entity selection-lighting SSBO");
- return set;
- }
- catch (Exception creationFailure)
- {
- try { DeleteDynamicBufferSet(set); }
- catch (Exception cleanupFailure)
- {
- throw new AggregateException(
- "Entity dynamic-buffer creation and rollback failed.",
- creationFailure,
- cleanupFailure);
- }
- throw;
- }
- }
-
- private void DeleteDynamicBufferSet(DynamicBufferSet set)
- {
- List? failures = null;
- void Attempt(uint buffer, int bytes, string name)
- {
- try { TrackedGlResource.DeleteBuffer(_gl!, buffer, bytes, $"deleting {name}"); }
- catch (Exception ex) { (failures ??= []).Add(ex); }
- }
-
- Attempt(set.InstanceSsbo, set.InstanceCapacityBytes, "entity instance SSBO");
- Attempt(set.BatchSsbo, set.BatchCapacityBytes, "entity batch SSBO");
- Attempt(set.IndirectBuffer, set.IndirectCapacityBytes, "entity indirect buffer");
- Attempt(set.ClipSlotSsbo, set.ClipSlotCapacityBytes, "entity clip-slot SSBO");
- Attempt(set.GlobalLightsSsbo, set.GlobalLightsCapacityBytes, "entity global-light SSBO");
- Attempt(set.InstanceLightSetSsbo, set.InstanceLightSetCapacityBytes, "entity light-set SSBO");
- Attempt(set.InstanceIndoorSsbo, set.InstanceIndoorCapacityBytes, "entity indoor SSBO");
- Attempt(set.InstanceAlphaSsbo, set.InstanceAlphaCapacityBytes, "entity alpha SSBO");
- Attempt(
- set.InstanceSelectionLightingSsbo,
- set.InstanceSelectionLightingCapacityBytes,
- "entity selection-lighting SSBO");
-
- if (failures is not null)
- throw new AggregateException("One or more entity dynamic buffers failed to delete.", failures);
- }
-
- private void PersistActiveDynamicBufferCapacities()
- {
- DynamicBufferSet set = _activeDynamicBufferSet
- ?? throw new InvalidOperationException("No dynamic entity buffer set is active.");
- set.InstanceCapacityBytes = _instanceSsboCapacityBytes;
- set.BatchCapacityBytes = _batchSsboCapacityBytes;
- set.IndirectCapacityBytes = _indirectBufferCapacityBytes;
- set.ClipSlotCapacityBytes = _clipSlotSsboCapacityBytes;
- set.GlobalLightsCapacityBytes = _globalLightsSsboCapacityBytes;
- set.InstanceLightSetCapacityBytes = _instLightSetSsboCapacityBytes;
- set.InstanceIndoorCapacityBytes = _instIndoorSsboCapacityBytes;
- set.InstanceAlphaCapacityBytes = _instAlphaSsboCapacityBytes;
- set.InstanceSelectionLightingCapacityBytes = _instSelectionLightingSsboCapacityBytes;
- }
-
- private unsafe void UploadSsbo(
- uint ssbo,
- uint binding,
- ref int capacityBytes,
- void* data,
- int byteCount)
- {
- UploadDynamicBuffer(
- BufferTargetARB.ShaderStorageBuffer,
- ssbo,
- ref capacityBytes,
- data,
- byteCount);
- _gl!.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, binding, ssbo);
- }
-
- private unsafe void UploadDynamicBuffer(
- BufferTargetARB target,
- uint buffer,
- ref int capacityBytes,
- void* data,
- int byteCount)
- {
- if (byteCount < 0)
- throw new ArgumentOutOfRangeException(nameof(byteCount));
-
- _gl!.BindBuffer(target, buffer);
- // A render bucket can legitimately contain zero batches (for example the outdoor dynamic
- // bucket immediately after auto-entry). Keep the buffer bound for the corresponding SSBO
- // binding, but there is no active prefix to allocate or upload and no draw can read it.
- if (byteCount == 0)
- return;
-
- if (capacityBytes < byteCount)
- {
- int grownCapacity = DynamicBufferCapacity.Grow(capacityBytes, byteCount);
- TrackedGlResource.AllocateBufferStorage(
- _gl,
- (GLEnum)target,
- buffer,
- capacityBytes,
- grownCapacity,
- GLEnum.DynamicDraw,
- $"growing entity dynamic buffer {buffer} to {grownCapacity} bytes");
- capacityBytes = grownCapacity;
- }
-
- _gl.BufferSubData(target, 0, (nuint)byteCount, data);
- }
-
- ///
- /// Fix B: pack into the binding=4 global light
- /// buffer (one GlobalLight = 4 vec4 = 16 floats, std430 stride 64 bytes,
- /// matching mesh_modern.vert's GlobalLight). Always uploads ≥1 element
- /// so the shader never reads an unbound SSBO — on a no-lights frame index 0 is
- /// a zeroed dummy that no instance set references (all sets are -1).
- ///
- private unsafe void UploadGlobalLights()
- {
- int n = GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
- int count = n > 0 ? n : 1; // never zero-size
- // Pack guarantees _globalLightData holds at least max(n,1) * FloatsPerLight floats.
- fixed (float* gp = _globalLightData)
- UploadSsbo(_globalLightsSsbo, 4, ref _globalLightsSsboCapacityBytes, gp,
- count * GlobalLightPacker.FloatsPerLight * sizeof(float));
- }
-
- ///
- /// Campaign V slice V4t: drains the device texture table's dirty runs and
- /// (re)binds it at
- /// .
- /// The drain is normally a no-op — a genuinely new slot means a new dat
- /// surface or composite override, not a new frame — but the bind is
- /// unconditional, because GL storage-buffer binding points are global and
- /// another raw-GL renderer's binding 9 sits there between two of these
- /// draws. Deleted with the raw-GL world path once these draws go through the
- /// encoder, which binds the same table on every pipeline bind.
- ///
- private void FlushAndBindTextureTable()
- {
- AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable;
- device.FlushTextureTable();
- _gl!.BindBufferBase(
- BufferTargetARB.ShaderStorageBuffer,
- AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
- device.TextureTableGlName);
- }
-
- ///
- /// Phase U.3: bind the per-cell clip-region SSBO to binding=2. Prefers the
- /// shared buffer (set via );
- /// otherwise lazily creates + binds a one-slot no-clip fallback so the shader
- /// never reads an unbound SSBO. The fallback's single slot has count 0
- /// (pass-all), matching 's slot 0.
- ///
- private unsafe void BindClipRegionBinding2()
- {
- if (_sharedClipRegionSsbo != 0)
- {
- _gl!.BindBufferBase(BufferTargetARB.ShaderStorageBuffer,
- ClipFrame.MeshClipSsboBinding, _sharedClipRegionSsbo);
- return;
- }
-
- if (_fallbackClipRegionSsbo == 0)
- {
- _fallbackClipRegionSsbo = _gl!.GenBuffer();
- // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
- var zero = stackalloc byte[ClipFrame.CellClipStrideBytes];
- for (int i = 0; i < ClipFrame.CellClipStrideBytes; i++) zero[i] = 0;
- _gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, _fallbackClipRegionSsbo);
- _gl.BufferData(BufferTargetARB.ShaderStorageBuffer,
- (nuint)ClipFrame.CellClipStrideBytes, zero, BufferUsageARB.DynamicDraw);
- }
- _gl!.BindBufferBase(BufferTargetARB.ShaderStorageBuffer,
- ClipFrame.MeshClipSsboBinding, _fallbackClipRegionSsbo);
- }
-
private void MaybeFlushDiag()
{
long now = Environment.TickCount64;
@@ -4154,14 +3426,11 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
{
if (_disposeResources is null)
{
- var releases = new List<(string Name, Action Release)>();
- // Campaign V slice V6j: the encoder arm owns no GL names. Its
+ // Campaign V slice V11: the RHI arm is the only arm — its
// pipelines route their physical free through the device's
- // retirement queue, so the ledger below is empty there.
- if (_gl is null)
- DisposeRhiResources();
- else
- BuildDisposeReleases(releases);
+ // retirement queue, so the release ledger is always empty.
+ var releases = new List<(string Name, Action Release)>();
+ DisposeRhiResources();
_disposeResources = new RetryableResourceReleaseLedger(releases);
}
@@ -4188,127 +3457,9 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
}
}
- private void BuildDisposeReleases(List<(string Name, Action Release)> releases)
- {
- for (int frame = 0; frame < _dynamicBufferSetsByFrame.Length; frame++)
- {
- List frameSets = _dynamicBufferSetsByFrame[frame];
- for (int index = 0; index < frameSets.Count; index++)
- AddDynamicBufferSetReleases(releases, frameSets[index], frame, index);
- }
-
- AddRawGlRelease(
- releases,
- _fallbackClipRegionSsbo,
- "fallback-clip-region",
- "deleting entity fallback clip SSBO",
- _gl!.DeleteBuffer);
-
- if (!_gpuQueriesInitialized)
- return;
- for (int i = 0; i < GpuQueryRingDepth; i++)
- {
- AddRawGlRelease(
- releases,
- _gpuQueryOpaque[i],
- $"opaque-query-{i}",
- "deleting entity opaque timing query",
- _gl.DeleteQuery);
- AddRawGlRelease(
- releases,
- _gpuQueryTransparent[i],
- $"transparent-query-{i}",
- "deleting entity transparent timing query",
- _gl.DeleteQuery);
- }
- }
-
- private void AddDynamicBufferSetReleases(
- List<(string Name, Action Release)> releases,
- DynamicBufferSet set,
- int frame,
- int index)
- {
- AddTrackedBufferRelease(releases, set.InstanceSsbo, set.InstanceCapacityBytes,
- $"dynamic-{frame}-{index}-instances", "deleting entity instance SSBO");
- AddTrackedBufferRelease(releases, set.BatchSsbo, set.BatchCapacityBytes,
- $"dynamic-{frame}-{index}-batches", "deleting entity batch SSBO");
- AddTrackedBufferRelease(releases, set.IndirectBuffer, set.IndirectCapacityBytes,
- $"dynamic-{frame}-{index}-indirect", "deleting entity indirect buffer");
- AddTrackedBufferRelease(releases, set.ClipSlotSsbo, set.ClipSlotCapacityBytes,
- $"dynamic-{frame}-{index}-clip-slots", "deleting entity clip-slot SSBO");
- AddTrackedBufferRelease(releases, set.GlobalLightsSsbo, set.GlobalLightsCapacityBytes,
- $"dynamic-{frame}-{index}-global-lights", "deleting entity global-light SSBO");
- AddTrackedBufferRelease(releases, set.InstanceLightSetSsbo, set.InstanceLightSetCapacityBytes,
- $"dynamic-{frame}-{index}-light-sets", "deleting entity light-set SSBO");
- AddTrackedBufferRelease(releases, set.InstanceIndoorSsbo, set.InstanceIndoorCapacityBytes,
- $"dynamic-{frame}-{index}-indoor", "deleting entity indoor SSBO");
- AddTrackedBufferRelease(releases, set.InstanceAlphaSsbo, set.InstanceAlphaCapacityBytes,
- $"dynamic-{frame}-{index}-alpha", "deleting entity alpha SSBO");
- AddTrackedBufferRelease(
- releases,
- set.InstanceSelectionLightingSsbo,
- set.InstanceSelectionLightingCapacityBytes,
- $"dynamic-{frame}-{index}-selection-lighting",
- "deleting entity selection-lighting SSBO");
- }
-
- 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));
- }
-
- private void AddRawGlRelease(
- List<(string Name, Action Release)> releases,
- uint resource,
- string name,
- string context,
- Action delete)
- {
- if (resource == 0)
- return;
- var release = new RetryableGpuResourceRelease(
- () => GLHelpers.ThrowOnResourceError(_gl!, $"{context} (precondition)"),
- () =>
- {
- delete(resource);
- GLHelpers.ThrowOnResourceError(_gl!, context);
- });
- releases.Add((name, release.Run));
- }
-
private void CompleteDispose()
{
- foreach (List frameSets in _dynamicBufferSetsByFrame)
- frameSets.Clear();
- _activeDynamicBufferSet = null;
_dynamicFrameStarted = false;
- _instanceSsbo = 0;
- _batchSsbo = 0;
- _indirectBuffer = 0;
- _clipSlotSsbo = 0;
- _globalLightsSsbo = 0;
- _instLightSetSsbo = 0;
- _instIndoorSsbo = 0;
- _instAlphaSsbo = 0;
- _instSelectionLightingSsbo = 0;
- _fallbackClipRegionSsbo = 0;
- Array.Clear(_gpuQueryOpaque);
- Array.Clear(_gpuQueryTransparent);
- _gpuQueriesInitialized = false;
}
// ── Public types + helpers for BuildIndirectArrays (Task 9) ─────────────
diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
index 04ca35f3..abf93dba 100644
--- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
@@ -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;
}
- ///
- /// Campaign V slice V4t: the GL device whose texture table every mesh
- /// batch's GpuTextureSlot 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.
- ///
- 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);
diff --git a/src/AcDream.App/Rendering/Wb/WorldTextureArray.cs b/src/AcDream.App/Rendering/Wb/WorldTextureArray.cs
index a2207b05..9addda89 100644
--- a/src/AcDream.App/Rendering/Wb/WorldTextureArray.cs
+++ b/src/AcDream.App/Rendering/Wb/WorldTextureArray.cs
@@ -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
/// ITextureArray implementation over , not a
-/// codec." This is that interface. and
-/// implement it, and which one exists is
-/// decided once at composition by —
-/// never per call, so the GL path executes exactly the statements it executed
-/// before.
+/// codec." This is that interface. ManagedGLTextureArray used to be its
+/// GL implementation, alongside ; which one
+/// existed was decided once at composition by
+/// , never per call. Campaign V slice
+/// V11 deleted ManagedGLTextureArray along with the rest of the raw-GL
+/// arm, so is now the sole implementation.
///
-/// The slot, not the handle, is the seam. Before this slice
+/// The slot, not the handle, is the seam. Before V6i-2
/// ObjectMeshManager read BindlessWrapHandle/
/// BindlessClampHandle off the concrete GL array and interned them into
/// the device table itself. A 64-bit ARB_bindless_texture handle is
-/// unspellable on Vulkan, so the array now answers the question the caller was
-/// really asking — — 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.
+/// unspellable on Vulkan, so the array answers the question the caller was
+/// really asking — — instead: the RHI array
+/// registered its two (texture, sampler) pairs at construction and returns a
+/// field.
///
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);
}
/// The retirement queue array layers and images are released through.
@@ -132,36 +132,6 @@ internal interface IWorldTextureArrayFactory
IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers);
}
-///
-/// The GL arm. Delegates to the same OpenGLGraphicsDevice entry point
-/// called directly before this slice, so the
-/// shipping backend's construction is textually unchanged.
-///
-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);
-}
-
///
/// The backend-neutral arm. Creates through
/// 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;
+
+ ///
+ /// The expected byte count for one uploaded layer of
+ /// at x.
+ ///
+ 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}"),
+ };
+ }
+
+ ///
+ /// Validates an upload payload against the format's expected byte count and
+ /// rejects transfer overrides that contradict it.
+ ///
+ 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}.");
+ }
+ }
}
diff --git a/src/AcDream.App/Rendering/WorldPassSurface.cs b/src/AcDream.App/Rendering/WorldPassSurface.cs
index b4aee130..e7198b50 100644
--- a/src/AcDream.App/Rendering/WorldPassSurface.cs
+++ b/src/AcDream.App/Rendering/WorldPassSurface.cs
@@ -1,10 +1,20 @@
using System.Numerics;
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Wb;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
+///
+/// Restores the frame-global rendering convention shared by frame clear and
+/// exceptional world-pass rollback. The raw-GL implementation this contract
+/// used to have alongside it (RenderFrameGlStateController) was deleted
+/// at Campaign V slice V11; is the only
+/// implementation left.
+///
+internal interface IRenderFrameGlState
+{
+ void RestoreFrameDefaults();
+}
+
///
/// Campaign V slice V6j: the small graphics surface the two world pass executors
/// touch directly, expressed once so their ordering logic — which is retail's,
@@ -75,97 +85,11 @@ internal interface IWorldPassSurface
}
///
-/// The GL arm. Every statement below is the one the executor used to issue
-/// inline, in the same order, against the same objects.
-///
-internal sealed class GlWorldPassSurface : IWorldPassSurface
-{
- private readonly GL _gl;
- private readonly ClipFrame _clipFrame;
- private readonly IRetailPViewFramebufferSource _framebuffer;
- private readonly WbDrawDispatcher _entities;
- private readonly EnvCellRenderer _envCells;
- private readonly TerrainModernRenderer? _terrain;
-
- public GlWorldPassSurface(
- GL gl,
- ClipFrame clipFrame,
- IRetailPViewFramebufferSource framebuffer,
- WbDrawDispatcher entities,
- EnvCellRenderer envCells,
- TerrainModernRenderer? terrain)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
- _framebuffer = framebuffer ?? throw new ArgumentNullException(nameof(framebuffer));
- _entities = entities ?? throw new ArgumentNullException(nameof(entities));
- _envCells = envCells ?? throw new ArgumentNullException(nameof(envCells));
- _terrain = terrain;
- }
-
- public void PrepareClipFrame(int terrainUploadCount)
- {
- // Allocate every terrain record before issuing the first draw. BufferData
- // must not replace the arena while an earlier slice can reference it.
- _clipFrame.ReserveTerrainUploads(_gl, terrainUploadCount);
- _clipFrame.UploadRegions(_gl);
- _entities.SetClipRegionSsbo(_clipFrame.RegionSsbo);
- _envCells.SetClipRegionSsbo(_clipFrame.RegionSsbo);
- UploadTerrainClip();
- }
-
- public void SetTerrainClip(ReadOnlySpan planes)
- {
- _clipFrame.SetTerrainClip(planes);
- UploadTerrainClip();
- }
-
- public void BindTerrainClip() => _clipFrame.BindTerrainClip(_gl);
-
- public void EnableClipDistances()
- {
- for (int index = 0; index < ClipFrame.MaxPlanes; index++)
- _gl.Enable(EnableCap.ClipDistance0 + index);
- }
-
- public void DisableClipDistances()
- {
- for (int index = 0; index < ClipFrame.MaxPlanes; index++)
- _gl.Disable(EnableCap.ClipDistance0 + index);
- }
-
- public bool BeginScissor(Vector4 ndcAabb)
- {
- RetailPViewFramebufferSize framebuffer = _framebuffer.Capture();
- var box = NdcScissorRect.ToPixels(
- ndcAabb,
- framebuffer.Width,
- framebuffer.Height);
- _gl.Enable(EnableCap.ScissorTest);
- _gl.Scissor(box.X, box.Y, (uint)box.Width, (uint)box.Height);
- return true;
- }
-
- public void EndScissor() => _gl.Disable(EnableCap.ScissorTest);
-
- public void ClearInteriorDepth()
- {
- _gl.Disable(EnableCap.ScissorTest);
- _gl.DepthMask(true);
- _gl.Clear(ClearBufferMask.DepthBufferBit);
- }
-
- private void UploadTerrainClip()
- {
- TerrainClipBufferBinding binding = _clipFrame.UploadTerrainClip(_gl);
- _terrain?.SetClipUbo(binding);
- }
-}
-
-///
-/// The RHI arm. The clip tables become ring sections published on the world pass
-/// scope, the scissor becomes dynamic state on the borrowed encoder, and the
-/// depth clear becomes a scoped vkCmdClearAttachments.
+/// The clip tables are ring sections published on the world pass scope, the
+/// scissor is dynamic state on the borrowed encoder, and the depth clear is a
+/// scoped vkCmdClearAttachments. The raw-GL implementation this used to
+/// sit alongside (GlWorldPassSurface) was deleted at Campaign V slice
+/// V11.
///
internal sealed class RhiWorldPassSurface : IWorldPassSurface
{
diff --git a/src/AcDream.App/RuntimeOptions.cs b/src/AcDream.App/RuntimeOptions.cs
index 0468b63c..703a14a3 100644
--- a/src/AcDream.App/RuntimeOptions.cs
+++ b/src/AcDream.App/RuntimeOptions.cs
@@ -59,7 +59,6 @@ public sealed record RuntimeOptions(
float FogEndMultiplier,
ResidencyBudgetOptions ResidencyBudgets,
StreamingWorkBudgetOptions StreamingWorkBudgets,
- RenderBackendKind RenderBackend,
string? VulkanDeviceOverride,
string? VulkanForcedUnsupportedFeature,
bool VulkanCapabilityProbe,
@@ -147,10 +146,6 @@ public sealed record RuntimeOptions(
FogEndMultiplier: TryParseFloat(env("ACDREAM_FOG_END_MULT")) ?? 0.95f,
ResidencyBudgets: ResidencyBudgetOptions.Parse(env),
StreamingWorkBudgets: StreamingWorkBudgetOptions.Parse(env),
- // Campaign V slice V10 flipped this. Unset, empty, or any
- // unrecognised value now means Vulkan: it is the shipping backend,
- // and a typo must never silently start the backend V11 deletes.
- RenderBackend: ParseRenderBackend(env("ACDREAM_RENDER_BACKEND")),
// Physical-device override, matched as a decimal index first and then
// as a case-insensitive device-name substring. Recorded verbatim in
// graphical-capabilities-vulkan.json whether or not it matched.
@@ -178,29 +173,6 @@ public sealed record RuntimeOptions(
TryParseNonNegativeInt(env("ACDREAM_VULKAN_PROBE_FRAMES")) ?? 0);
}
- ///
- /// Startup backend request. Campaign V slice V10 inverted this: Vulkan is
- /// the default, and only the explicit escape-hatch tokens gl and
- /// opengl select OpenGL. Everything else — unset, vulkan, or a
- /// typo — is Vulkan.
- ///
- ///
- /// The polarity of the typo case is deliberate and it flipped with the
- /// default. Before V10 an unrecognised token had to land on GL, because
- /// Vulkan was dark and a typo must never silently start a backend that
- /// cannot draw. After V10 an unrecognised token must land on Vulkan for the
- /// same reason read the other way: GL is the retiring backend that slice V11
- /// deletes, so a typo must never silently pin a process to it. opengl
- /// is honoured alongside gl because the escape hatch exists to be
- /// found, and the failure mode of a near-miss spelling here is a process
- /// that quietly is not on the backend the operator asked to fall back to.
- ///
- private static RenderBackendKind ParseRenderBackend(string? value)
- => string.Equals(value, "gl", StringComparison.OrdinalIgnoreCase)
- || string.Equals(value, "opengl", StringComparison.OrdinalIgnoreCase)
- ? RenderBackendKind.Gl
- : RenderBackendKind.Vulkan;
-
/// True iff live-mode credentials are present and valid for connecting.
public bool HasLiveCredentials =>
LiveMode && !string.IsNullOrEmpty(LiveUser) && !string.IsNullOrEmpty(LivePass);
diff --git a/src/AcDream.App/UI/UiHost.cs b/src/AcDream.App/UI/UiHost.cs
index d2b0043b..254fe689 100644
--- a/src/AcDream.App/UI/UiHost.cs
+++ b/src/AcDream.App/UI/UiHost.cs
@@ -3,7 +3,6 @@ using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using Silk.NET.Input;
-using Silk.NET.OpenGL;
namespace AcDream.App.UI;
diff --git a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs
index bf69aeb9..ff2c6b0f 100644
--- a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs
+++ b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs
@@ -1,4 +1,4 @@
-using System.Reflection;
+using System.Reflection;
using System.Runtime.CompilerServices;
using AcDream.App.Audio;
using AcDream.App.Composition;
@@ -19,7 +19,6 @@ using AcDream.Runtime.Gameplay;
using DatReaderWriter.DBObjs;
using Silk.NET.Input;
using Silk.NET.OpenAL;
-using Silk.NET.OpenGL;
namespace AcDream.App.Tests.Composition;
@@ -253,7 +252,7 @@ public sealed class ContentEffectsAudioCompositionTests
Poses = new EntityEffectPoseRegistry();
Factory = new Factory();
Publication = new Publication();
- Platform = new GameWindowPlatformResult(TestGameWindowGraphics.OpenGl, null!);
+ Platform = new GameWindowPlatformResult(TestGameWindowGraphics.Instance, null!);
Host = (HostInputCameraResult)RuntimeHelpers.GetUninitializedObject(
typeof(HostInputCameraResult));
Dependencies = new ContentEffectsAudioDependencies(
diff --git a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs
index 9c517617..267bb3ee 100644
--- a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs
+++ b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Input;
@@ -8,7 +8,6 @@ using AcDream.App.Tests.Rendering.Gpu;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
using Silk.NET.Maths;
-using Silk.NET.OpenGL;
namespace AcDream.App.Tests.Composition;
@@ -95,7 +94,7 @@ public sealed class HostInputCameraCompositionTests
IKeyboard keyboard = DispatchProxy.Create();
IMouse mouse = DispatchProxy.Create();
Input = new InputContext(keyboard, mouse);
- Platform = new GameWindowPlatformResult(TestGameWindowGraphics.OpenGl, Input);
+ Platform = new GameWindowPlatformResult(TestGameWindowGraphics.Instance, Input);
ViewportAspect = new ViewportAspectState();
Framebuffer = new FramebufferResizeController(ViewportAspect);
Capture = new CaptureSource();
diff --git a/tests/AcDream.App.Tests/Composition/TestGameWindowGraphics.cs b/tests/AcDream.App.Tests/Composition/TestGameWindowGraphics.cs
index dc23169b..93962314 100644
--- a/tests/AcDream.App.Tests/Composition/TestGameWindowGraphics.cs
+++ b/tests/AcDream.App.Tests/Composition/TestGameWindowGraphics.cs
@@ -1,61 +1,30 @@
-using AcDream.App;
using AcDream.App.Composition;
-using Silk.NET.Core.Contexts;
-using Silk.NET.OpenGL;
+using AcDream.App.Rendering;
+using AcDream.App.Rendering.Gpu.Vk;
namespace AcDream.App.Tests.Composition;
///
-/// Campaign V slice V6h: the graphics handle composition tests hand to a phase.
+/// The graphics handle composition tests hand to a phase.
///
-/// The phases now select their backend arm from
-/// rather than from a bare GL reference,
-/// so a test that means "compose the OpenGL arm" has to say so. The GL instance
-/// is never called: every composition test supplies a stub factory that ignores
-/// its context argument, and the loader below would fault if anything did — which
-/// is the point. It is a token identifying the arm, not a driver.
+/// Campaign V slice V11 deleted the raw-GL arm — GameWindowGraphics
+/// no longer has a Backend or Gl member to select between, so this
+/// is now a single Vulkan-shaped token: a real
+/// (composition phases require one, and it needs no live surface — just a
+/// sample count), no live because tests never
+/// dereference it.
///
internal sealed class TestGameWindowGraphics : GameWindowGraphics
{
- private readonly GL? _gl;
+ public static TestGameWindowGraphics Instance { get; } = new();
- private TestGameWindowGraphics(RenderBackendKind backend, GL? gl)
+ private TestGameWindowGraphics()
{
- Backend = backend;
- _gl = gl;
}
- /// Selects the OpenGL arm, with a context token no test dereferences.
- public static TestGameWindowGraphics OpenGl { get; } =
- new(RenderBackendKind.Gl, new GL(new UnusableNativeContext()));
-
- /// Selects the Vulkan arm: no GL context exists.
- public static TestGameWindowGraphics Vulkan { get; } =
- new(RenderBackendKind.Vulkan, null);
-
- public override RenderBackendKind Backend { get; }
-
- public override GL? Gl => _gl;
+ public override IWorldPassScope? WorldPassScope { get; } = new VulkanWorldPassScope(sampleCount: 1);
public override void Dispose()
{
}
-
- private sealed class UnusableNativeContext : INativeContext
- {
- public nint GetProcAddress(string proc, int? slot = null) =>
- throw new InvalidOperationException(
- $"A composition test called GL entry point '{proc}'. " +
- "Test graphics are a backend token, not a driver.");
-
- public bool TryGetProcAddress(string proc, out nint addr, int? slot = null)
- {
- addr = 0;
- return false;
- }
-
- public void Dispose()
- {
- }
- }
}
diff --git a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs
index 5ff43550..e2ee6328 100644
--- a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs
+++ b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs
@@ -1,4 +1,4 @@
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using AcDream.App.Composition;
using AcDream.App.Rendering;
@@ -14,7 +14,6 @@ using AcDream.UI.Abstractions.Settings;
using DatReaderWriter.DBObjs;
using Silk.NET.Input;
using Silk.NET.OpenGL;
-using Shader = AcDream.App.Rendering.Shader;
namespace AcDream.App.Tests.Composition;
@@ -90,15 +89,12 @@ public sealed class WorldRenderCompositionTests
}
[Theory]
- [InlineData("terrain shader", "terrain shader")]
[InlineData("scene lighting", "scene lighting")]
[InlineData("debug lines", "debug lines")]
[InlineData("HUD", "text renderer|debug font")]
[InlineData("terrain", "terrain")]
- [InlineData("mesh shader", "mesh shader")]
[InlineData("WB mesh adapter", "WB mesh adapter")]
[InlineData("texture cache", "texture cache")]
- [InlineData("sampler cache", "sampler cache")]
public void FailedPublicationRollsBackOnlyItsUnpublishedResourcePrefix(
string publication,
string expectedReleaseOrder)
@@ -193,7 +189,7 @@ public sealed class WorldRenderCompositionTests
if (point == _failurePoint)
throw new InvalidOperationException($"fault at {point}");
}).Compose(
- new GameWindowPlatformResult(TestGameWindowGraphics.OpenGl, null!),
+ new GameWindowPlatformResult(TestGameWindowGraphics.Instance, null!),
Content,
new SettingsDevToolsResult(
QualitySettings.From(QualityPreset.High)));
@@ -224,8 +220,6 @@ public sealed class WorldRenderCompositionTests
public ResidencyBudgetOptions? TextureBudgets { get; private set; }
public ResidencyManager? RegisteredResidency { get; private set; }
- public void InitializeGlState(GL gl) { }
-
public WorldRegionData LoadRegion(IDatReaderWriter dats) =>
new(Stub(), new float[256]);
@@ -233,16 +227,6 @@ public sealed class WorldRenderCompositionTests
WorldEnvironmentController environment,
Region region) { }
- public BindlessSupport RequireBindless(GL gl, Action log) =>
- Stub();
-
- public TerrainAtlas AcquireTerrainAtlas(
- IGameRenderResourceLifetime lifetime,
- GL gl,
- IDatReaderWriter dats,
- BindlessSupport bindless) =>
- lifetime.AcquireTerrainAtlas(() => Atlas);
-
///
/// Campaign V slice V6i-2: the arm a backend with no GL context takes.
/// Returns the same stub atlas through the same lifetime owner, so the
@@ -258,7 +242,7 @@ public sealed class WorldRenderCompositionTests
/// Recorded rather than run: the exercise needs a real
/// to create images through. Its behaviour is
/// covered by RhiWorldTextureArrayTests and by the Vulkan
- /// composition-host run — see plan §5.5.13.
+ /// composition-host run  see plan §5.5.13.
///
public void ExerciseBackendNeutralWorldTextures(
IGpuDevice device,
@@ -270,12 +254,6 @@ public sealed class WorldRenderCompositionTests
public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) =>
AnisotropicLevel = level;
- public Shader CreateTerrainShader(GL gl, string shadersDirectory) =>
- Resource("terrain shader");
-
- public SceneLightingUboBinding CreateSceneLighting(GL gl) =>
- Resource("scene lighting");
-
public SceneLightingUboBinding CreateBackendNeutralSceneLighting(
ICurrentGpuFrameSource frameSource,
IWorldPassScope scope) =>
@@ -294,15 +272,6 @@ public sealed class WorldRenderCompositionTests
IGpuDevice device, ICurrentGpuFrameSource frameSource, string shadersDirectory) =>
Resource("text renderer");
- public TerrainModernRenderer CreateTerrain(
- GL gl,
- BindlessSupport bindless,
- Shader shader,
- TerrainAtlas atlas,
- IGpuDevice gpuDevice,
- IGpuResourceRetirementQueue retirement) =>
- Resource("terrain");
-
public TerrainModernRenderer CreateBackendNeutralTerrain(
IGpuDevice gpuDevice,
ICurrentGpuFrameSource frameSource,
@@ -323,9 +292,6 @@ public sealed class WorldRenderCompositionTests
Stub(),
new ConcurrentDictionary());
- public Shader CreateMeshShader(GL gl, string shadersDirectory) =>
- Resource("mesh shader");
-
public WbMeshAdapter CreateMeshAdapter(
GL? gl,
IGpuDevice device,
@@ -339,10 +305,8 @@ public sealed class WorldRenderCompositionTests
}
public TextureCache CreateTextureCache(
- GL gl,
IGpuDevice device,
IDatReaderWriter dats,
- BindlessSupport bindless,
IGpuResourceRetirementQueue retirement,
string diagnosticsDirectory,
ResidencyBudgetOptions budgets)
@@ -362,9 +326,6 @@ public sealed class WorldRenderCompositionTests
RegisteredResidency = manager;
}
- public SamplerCache CreateSamplerCache(GL gl) =>
- Resource("sampler cache");
-
public void Release(IDisposable resource)
{
Releases.Add(_names[resource]);
@@ -385,10 +346,6 @@ public sealed class WorldRenderCompositionTests
public WbMeshAdapter? MeshAdapter { get; private set; }
public TextureCache? TextureCache { get; private set; }
- public void PublishBindlessSupport(BindlessSupport value) =>
- Fail("bindless");
- public void PublishTerrainShader(Shader value) =>
- Fail("terrain shader");
public void PublishSceneLighting(SceneLightingUboBinding value) =>
Fail("scene lighting");
public void PublishDebugLines(DebugLineRenderer value) =>
@@ -407,7 +364,6 @@ public sealed class WorldRenderCompositionTests
TerrainBlendingContext blending,
ConcurrentDictionary surfaceCache) =>
Fail("terrain build state");
- public void PublishMeshShader(Shader value) => Fail("mesh shader");
public void PublishWbMeshAdapter(WbMeshAdapter value)
{
@@ -421,9 +377,6 @@ public sealed class WorldRenderCompositionTests
TextureCache = value;
}
- public void PublishSamplerCache(SamplerCache value) =>
- Fail("sampler cache");
-
private void Fail(string point)
{
if (string.Equals(failure, point, StringComparison.Ordinal))
diff --git a/tests/AcDream.App.Tests/Platform/GraphicalCapabilityRequirementsTests.cs b/tests/AcDream.App.Tests/Platform/GraphicalCapabilityRequirementsTests.cs
deleted file mode 100644
index 741686e1..00000000
--- a/tests/AcDream.App.Tests/Platform/GraphicalCapabilityRequirementsTests.cs
+++ /dev/null
@@ -1,261 +0,0 @@
-using System.Text.Json;
-using AcDream.App.Platform;
-
-namespace AcDream.App.Tests.Platform;
-
-public sealed class GraphicalCapabilityRequirementsTests
-{
- [Fact]
- public void SupportedModernContextPasses()
- {
- GraphicalCapabilityRecord capabilities = CreateSupported();
-
- Assert.Empty(GraphicalCapabilityRequirements.Evaluate(capabilities));
- }
-
- [Theory]
- [InlineData("bindless")]
- [InlineData("draw-parameters")]
- [InlineData("mdi")]
- [InlineData("ssbo")]
- [InlineData("timer")]
- [InlineData("depth")]
- [InlineData("stencil")]
- [InlineData("srgb")]
- [InlineData("keyboard")]
- [InlineData("mouse")]
- public void MissingMandatoryCapabilityIsRejected(string missing)
- {
- GraphicalCapabilityRecord capabilities = CreateSupported();
- capabilities = missing switch
- {
- "bindless" => capabilities with
- {
- HasBindlessTexture = false,
- },
- "draw-parameters" => capabilities with
- {
- HasShaderDrawParameters = false,
- },
- "mdi" => capabilities with
- {
- HasMultiDrawIndirect = false,
- },
- "ssbo" => capabilities with
- {
- HasShaderStorageBuffer = false,
- },
- "timer" => capabilities with
- {
- HasTimerQuery = false,
- },
- "depth" => capabilities with
- {
- Framebuffer = capabilities.Framebuffer with
- {
- DepthBits = 16,
- },
- },
- "stencil" => capabilities with
- {
- Framebuffer = capabilities.Framebuffer with
- {
- StencilBits = 0,
- },
- },
- "srgb" => capabilities with
- {
- Framebuffer = capabilities.Framebuffer with
- {
- FramebufferSrgbApi = false,
- },
- },
- "keyboard" => capabilities with
- {
- Input = capabilities.Input with
- {
- KeyboardCount = 0,
- },
- },
- "mouse" => capabilities with
- {
- Input = capabilities.Input with
- {
- MouseCount = 0,
- },
- },
- _ => throw new ArgumentOutOfRangeException(nameof(missing)),
- };
-
- Assert.NotEmpty(GraphicalCapabilityRequirements.Evaluate(capabilities));
- }
-
- [Fact]
- public void AdvertisedBufferStorageRequiresSuccessfulPersistentProbe()
- {
- GraphicalCapabilityRecord capabilities = CreateSupported() with
- {
- FunctionProbe = CreateSupported().FunctionProbe with
- {
- PersistentBufferStorage = false,
- },
- };
-
- Assert.Contains(
- GraphicalCapabilityRequirements.Evaluate(capabilities),
- failure => failure.Contains(
- "persistent mapping",
- StringComparison.Ordinal));
- }
-
- [Fact]
- public void MissingOptionalBufferStorageDoesNotRequireProbe()
- {
- GraphicalCapabilityRecord capabilities = CreateSupported() with
- {
- HasBufferStorage = false,
- FunctionProbe = CreateSupported().FunctionProbe with
- {
- PersistentBufferStorage = null,
- },
- };
-
- Assert.Empty(GraphicalCapabilityRequirements.Evaluate(capabilities));
- }
-
- [Fact]
- public void UnsupportedMessageNamesDriverProtocolFailureAndReport()
- {
- GraphicalCapabilityRecord capabilities = CreateSupported() with
- {
- SupportFailures = ["GL_ARB_bindless_texture is required."],
- };
-
- string message = GraphicalCapabilityGuard.FormatUnsupportedMessage(
- capabilities,
- "capabilities.json");
-
- Assert.Contains("Mesa", message);
- Assert.Contains("RadeonSI", message);
- Assert.Contains("Wayland", message);
- Assert.Contains("GL_ARB_bindless_texture", message);
- Assert.Contains(
- Path.GetFullPath("capabilities.json"),
- message);
- }
-
- [Fact]
- public void ReportWriterAtomicallyOverwritesJson()
- {
- string directory = Path.Combine(
- Path.GetTempPath(),
- "acdream-capability-tests",
- Guid.NewGuid().ToString("N"));
- string path = Path.Combine(directory, "capabilities.json");
- try
- {
- GraphicalCapabilityReportWriter.Write(path, CreateSupported());
- GraphicalCapabilityReportWriter.Write(
- path,
- CreateSupported() with
- {
- GlRenderer = "second renderer",
- });
-
- using JsonDocument report = JsonDocument.Parse(
- File.ReadAllText(path));
- Assert.Equal(
- "second renderer",
- report.RootElement
- .GetProperty(nameof(GraphicalCapabilityRecord.GlRenderer))
- .GetString());
- Assert.Equal(
- "Wayland",
- report.RootElement
- .GetProperty(nameof(
- GraphicalCapabilityRecord.ActiveDisplayProtocol))
- .GetString());
- Assert.False(File.Exists(path + ".tmp"));
- }
- finally
- {
- if (Directory.Exists(directory))
- Directory.Delete(directory, recursive: true);
- }
- }
-
- private static GraphicalCapabilityRecord CreateSupported() => new(
- DateTimeOffset.UnixEpoch,
- "linux-x64",
- GraphicalHostOperatingSystem.Linux,
- GraphicalDisplayProtocol.Wayland,
- GraphicalDisplayProtocol.Wayland,
- "test",
- "Silk.NET.Windowing.Glfw",
- "3.4.0",
- "Mesa",
- "RadeonSI",
- "4.6",
- "4.60",
- 4,
- 6,
- 1,
- 1,
- HasBindlessTexture: true,
- HasShaderDrawParameters: true,
- HasMultiDrawIndirect: true,
- HasShaderStorageBuffer: true,
- HasBufferStorage: true,
- HasTimerQuery: true,
- MaximumShaderStorageBufferBindings: 16,
- MaximumUniformBufferBindings: 72,
- MaximumTextureSize: 16_384,
- MaximumArrayTextureLayers: 2_048,
- MaximumCombinedTextureImageUnits: 192,
- new GraphicalFramebufferCapabilities(
- 8,
- 8,
- 8,
- 8,
- 24,
- 8,
- 1,
- 4,
- FramebufferSrgbApi: true),
- new GraphicalInputCapabilities(
- KeyboardCount: 1,
- MouseCount: 1,
- GamepadCount: 0,
- JoystickCount: 0),
- new GraphicalWindowCapabilities(
- 1280,
- 720,
- 2560,
- 1440,
- "test monitor",
- 144,
- VSync: false),
- new GraphicalAudioCapabilities(
- Requested: false,
- Available: false,
- PlaybackSubmitted: false,
- DisposalComplete: true,
- Backend: "not requested"),
- new GraphicalSmokeLifecycleCapabilities(
- OwnedWindowCount: 1,
- OwnedGlApiCount: 1,
- OwnedInputContextCount: 1,
- OwnedAudioEngineCount: 0,
- ShutdownComplete: false),
- [],
- new GraphicalFunctionProbeResult(
- BindlessTexture: true,
- ShaderDrawParameters: true,
- MultiDrawIndirect: true,
- ShaderStorageBuffer: true,
- TimerQuery: true,
- SrgbFramebuffer: true,
- PersistentBufferStorage: true,
- Failures: []),
- SupportFailures: []);
-}
diff --git a/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs b/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs
index 46090dd1..ccb0f221 100644
--- a/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using AcDream.App.Rendering;
+using AcDream.App.Rendering.Gpu;
using Xunit;
namespace AcDream.App.Tests.Rendering;
@@ -41,7 +42,11 @@ public class ClipFrameLayoutTests
Assert.Equal(8, ClipFrame.MaxPlanes);
Assert.Equal(144, ClipFrame.TerrainUboBytes);
// Binding contract: mesh clip regions on SSBO binding=2, terrain on UBO binding=2.
- Assert.Equal(2u, ClipFrame.MeshClipSsboBinding);
+ // The mesh side's binding index moved off ClipFrame at Campaign V slice
+ // V11 — the RHI arm addresses it through GpuBindingModel.StorageClipRegions
+ // instead of a raw GL binding constant (see ClipFrame's BeginFrame doc
+ // comment); the terrain UBO binding is still genuinely shared, so it stays.
+ Assert.Equal(2u, GpuBindingModel.StorageClipRegions);
Assert.Equal(2u, ClipFrame.TerrainClipUboBinding);
}
diff --git a/tests/AcDream.App.Tests/Rendering/ClipFrameUploadTests.cs b/tests/AcDream.App.Tests/Rendering/ClipFrameUploadTests.cs
deleted file mode 100644
index 24704a28..00000000
--- a/tests/AcDream.App.Tests/Rendering/ClipFrameUploadTests.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using AcDream.App.Rendering;
-using Xunit;
-
-namespace AcDream.App.Tests.Rendering;
-
-public sealed class ClipFrameUploadTests
-{
- [Theory]
- [InlineData(1, 144)]
- [InlineData(16, 144)]
- [InlineData(64, 192)]
- [InlineData(256, 256)]
- [InlineData(512, 512)]
- public void TerrainArena_RecordStride_RespectsDriverAlignment(
- int alignment,
- int expectedStride)
- {
- Assert.Equal(expectedStride, ClipFrameArenaLayout.RecordStride(alignment));
- }
-
- [Fact]
- public void TerrainArena_AssignsEverySliceAUniqueOrderedRange()
- {
- const int stride = 256;
- Assert.Equal(0, ClipFrameArenaLayout.RecordOffset(0, stride));
- Assert.Equal(256, ClipFrameArenaLayout.RecordOffset(1, stride));
- Assert.Equal(512, ClipFrameArenaLayout.RecordOffset(2, stride));
- Assert.Equal(1280, ClipFrameArenaLayout.RequiredBytes(5, stride));
- }
-
- [Fact]
- public void UploadState_RegionsOnce_AndTerrainRangesInSubmissionOrder()
- {
- var state = new ClipFrameUploadState();
- state.BeginFrame();
- state.ValidateRegionsNotUploaded();
- state.MarkRegionsUploaded();
- Assert.True(state.RegionsUploaded);
- Assert.Throws(state.ValidateRegionsNotUploaded);
-
- state.ValidateTerrainReservation(4);
- state.CommitTerrainReservation(4);
- Assert.Equal(new[] { 0, 1, 2, 3 }, new[]
- {
- state.NextTerrainRecord(),
- state.NextTerrainRecord(),
- state.NextTerrainRecord(),
- state.NextTerrainRecord(),
- });
- Assert.Equal(4, state.TerrainUploaded);
- Assert.Throws(() => state.NextTerrainRecord());
-
- state.BeginFrame();
- Assert.False(state.RegionsUploaded);
- Assert.Equal(0, state.TerrainReserved);
- Assert.Equal(0, state.TerrainUploaded);
- }
-
- [Fact]
- public void ResourceRing_RetainsAtMostOneResourcePerFencedFrameSlot()
- {
- var ring = new ClipFrameResourceRing