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

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

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

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

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

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

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

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

View file

@ -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;

View file

@ -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,

View file

@ -1,79 +1,32 @@
using AcDream.App.Rendering.Gpu.Vk;
using Silk.NET.OpenGL;
namespace AcDream.App.Composition;
/// <summary>
/// Campaign V slice V6h: the graphics ownership that platform acquisition
/// publishes, once per backend.
/// The graphics ownership that platform acquisition publishes.
///
/// <para><see cref="GameWindowPlatformAcquisition"/> was already generic over its
/// graphics type; only the call sites pinned <c>GL</c>. 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.</para>
///
/// <para>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.</para>
/// <para>Vulkan is the only backend as of Campaign V slice V11: the raw-GL
/// implementation (<c>OpenGlGameWindowGraphics</c>) and the members that only
/// existed to branch on it (<c>Backend</c>, <c>Gl</c>, <c>RequireGl</c>) were
/// deleted along with it. This class stays as the seam
/// <see cref="GameWindowPlatformAcquisition"/> 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.</para>
/// </summary>
internal abstract class GameWindowGraphics : IDisposable
{
/// <summary>Which backend this handle owns.</summary>
public abstract RenderBackendKind Backend { get; }
/// <summary>
/// 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.
/// </summary>
public virtual GL? Gl => null;
/// <summary>The live Vulkan context, or null on any other backend.</summary>
/// <summary>The live Vulkan context.</summary>
public virtual VulkanGraphicsContext? Vulkan => null;
/// <summary>
/// Campaign V slice V6j: the backend's world-pass seam, or null where the
/// world renderers open their own passes.
///
/// <para>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.</para>
/// The backend's world-pass seam. Every remaining renderer records into the
/// pass this publishes rather than opening its own.
/// </summary>
public virtual AcDream.App.Rendering.IWorldPassScope? WorldPassScope => null;
/// <summary>
/// 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.
/// </summary>
public GL RequireGl(string owner) =>
Gl ?? throw new InvalidOperationException(
$"'{owner}' requires the OpenGL context, but the {Backend} backend is active.");
public abstract void Dispose();
}
/// <summary>OpenGL ownership: the Silk.NET <see cref="GL"/> context itself.</summary>
internal sealed class OpenGlGameWindowGraphics : GameWindowGraphics
{
public OpenGlGameWindowGraphics(GL gl) =>
Context = gl ?? throw new ArgumentNullException(nameof(gl));
/// <summary>The owned context. <see cref="GameWindowGraphics.Gl"/> is the borrowed view.</summary>
public GL Context { get; }
public override RenderBackendKind Backend => RenderBackendKind.Gl;
public override GL? Gl => Context;
public override void Dispose() => Context.Dispose();
}
/// <summary>
/// Vulkan ownership: the instance, surface, device, swapchain and RHI device
/// that <see cref="VulkanGraphicsContext"/> 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;
/// <summary>The concrete scope, for the phase that publishes the encoder on it.</summary>

View file

@ -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<IMouse> 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<HostInputCameraCompositionPoint>? _faultInjection;
private IHostInputCameraCompositionFactory _factory =
new RetailHostInputCameraCompositionFactory();
new VulkanHostInputCameraCompositionFactory();
public HostInputCameraCompositionPhase(
HostInputCameraDependencies dependencies,
@ -253,17 +167,13 @@ internal sealed class HostInputCameraCompositionPhase :
}
/// <summary>
/// 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.
/// </summary>
private static IHostInputCameraCompositionFactory DefaultFactoryFor(
GameWindowGraphics graphics) =>
graphics.Backend == RenderBackendKind.Vulkan
? new VulkanHostInputCameraCompositionFactory()
: new RetailHostInputCameraCompositionFactory();
new VulkanHostInputCameraCompositionFactory();
public HostInputCameraResult Compose(
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform)

View file

@ -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;

View file

@ -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);

View file

@ -8,28 +8,22 @@ using Silk.NET.Input;
namespace AcDream.App.Composition;
/// <summary>
/// Campaign V slice V6h: the Vulkan arm of the host phase.
///
/// <para>This is the whole of the backend fork at composition Phase 1. Only the
/// four graphics members differ from
/// <see cref="RetailHostInputCameraCompositionFactory"/>; 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.</para>
/// Campaign V slice V6h: the Vulkan arm of the host phase — now the only arm,
/// the raw-GL <c>RetailHostInputCameraCompositionFactory</c> 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.
///
/// <para><b>What is absent, and why.</b> 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
/// <see cref="WorldRenderDiagnostics"/>: it is a raw-GL state tripwire, and
/// <see cref="WorldRenderDiagnostics"/>: 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.</para>
/// </summary>
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<IMouse> 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.");
/// <summary>
/// Vulkan sets the viewport per pass from the pass extent, so there is no

View file

@ -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(
/// <summary>
/// The render foundation the later phases build on.
///
/// <para>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.</para>
/// <para>Every member here is backend-neutral. The raw-GL-only members this
/// record used to carry (<c>Bindless</c>, <c>TerrainShader</c>, <c>MeshShader</c>,
/// <c>Samplers</c>) were deleted at Campaign V slice V11 along with the GL arm
/// that was their only producer.</para>
/// </summary>
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<uint, SurfaceInfo> 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<string> log);
TerrainAtlas AcquireTerrainAtlas(
IGameRenderResourceLifetime lifetime,
GL gl,
IDatReaderWriter dats,
BindlessSupport bindless);
/// <summary>
/// Campaign V slice V6i-2: the terrain atlas built through
/// <see cref="AcDream.App.Rendering.Gpu.IGpuDevice"/> rather than raw GL.
/// Same DATs, same decode, same layer ordering — only the upload differs.
/// <see cref="AcDream.App.Rendering.Gpu.IGpuDevice"/>. The raw-GL arm this
/// used to fork from (<c>AcquireTerrainAtlas</c>) 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.
/// </summary>
TerrainAtlas AcquireBackendNeutralTerrainAtlas(
IGameRenderResourceLifetime lifetime,
@ -120,12 +104,11 @@ internal interface IWorldRenderCompositionFactory
Action<string> log);
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
Shader CreateTerrainShader(GL gl, string shadersDirectory);
SceneLightingUboBinding CreateSceneLighting(GL gl);
/// <summary>
/// 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 (<c>CreateSceneLighting</c>) was deleted at slice V11.
/// </summary>
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);
/// <summary>
/// Campaign V slice V6j: terrain's RHI arm. No GL context, no linked
/// program — the pipeline compiles <c>terrain_modern</c> 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 (<c>CreateTerrain</c>) was deleted at slice V11.
/// </summary>
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<string> 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<uint, SurfaceInfo>());
}
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,
}
/// <summary>
@ -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;
}
/// <summary>
/// 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.
/// </summary>
private T? AcquireAndPublishIf<T>(
bool supported,
CompositionAcquisitionScope scope,
string name,
Func<T> factory,
Action<T> 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);
}