feat(render): Campaign V slice V6h — the Vulkan composition host
ACDREAM_RENDER_BACKEND=vulkan now runs the real GameWindow composition rather
than a second main(). All nine phases execute: DAT load, streaming, camera,
entity table, session, and the real retained UiHost drawing through the RHI.
No world renderers — they are raw GL until V4t and the world arm behind it.
The offline log is the client's own (acdream.pak opened, 6266 spells, Region
0x13000000, "loading world view centered on 0xA9B4FFFF", fourteen retail
LayoutDesc lines, streaming radii), and the captured frame is the retail
retained UI: vitals, combat/spell bar with DAT scarab icons, the nine-slot
toolbar, chat with tabs and Send, radar/compass with dat-font glyphs. Sampled
against the GL capture the widgets agree — chat interior RGBA (25,24,27,158)
vs (22,21,23,158), vitals bar (117,1,0) and toolbar slot (0,11,17) identical.
Three seams, as §5.5.9 specified:
1. Platform acquisition — already generic — publishes GameWindowGraphics
instead of a bare GL. Phases that still speak raw GL read Graphics.Gl and
take their Vulkan arm when it is null; each branch names the slice that
removes it.
2. VulkanHostInputCameraCompositionFactory is a new file and the whole of the
Phase-1 fork: four graphics members differ, input/camera/pointer delegate.
The default factory is chosen inside the phase from the platform result.
HostInputCameraResult gained backend-neutral Retirement and FrameSlots.
3. The frame root forks on one condition. The GL world-scene assembly is
unchanged, wrapped in `if (gl is not null)`; the Vulkan arm's graph is one
backbuffer clear pass computing the same RenderFrameFoundation from the same
clock and weather owners, then private presentation over it.
§5.5.9's three TextureCache couplings are unpicked: the constructor takes GL?
and rejects bindless without one, world entry points route through a Gl
property that throws naming V4t, and the (GlGpuTexture) VRAM-accounting cast
became a backend test. That cast's stated reason — DrawSprite's texture-unit
binding — was already stale, deleted at V6d.
VulkanBringUpHost is reduced to the capability-probe harness it is named for:
the instance/surface/device/swapchain sequence moved into VulkanGraphicsContext,
which the composition host and the harness now share. It is reached only with
ACDREAM_VULKAN_PROBE=1.
One latent Vulkan defect surfaced and is fixed here. The first composition-host
frame died with ErrorDeviceLost; validation named VUID-vkCmdDraw-None-08600 —
descriptor set 2 never bound. VulkanGpuPassEncoder bound sets 0/1/2 only as a
side effect of BindStorageBuffer/BindUniformBuffer, so a pass sampling the
texture table while binding no buffer — every retained-UI and debug-line pass —
drew with the table unbound. It survived V6c-V6g because the bring-up host
always drew VulkanRhiScene first and the UI pass inherited its binds; the
composition host has no 3-D scene. The fix is one line in the encoder's
constructor beside the viewport and scissor defaults, which exist for exactly
the same reason: a pass opens with complete binding state rather than depending
on what preceded it.
Gates: strict GL offline pixel gate against 46d893f7 measures 1.24e-05 (7 of
563,200 pixels), inside the documented 15-23 px / 4.1e-05 band, so GL behaviour
did not move. App tests 4,075/3 skips; complete Release suite 9,138/5 skips.
One full Vulkan run with VK_LAYER_KHRONOS_validation: zero errors, zero
warnings. Both Vulkan runs converged the ownership ledger — no [shutdown]
diagnostic on either stream. The reduced probe harness presented 34,811
validation-clean frames.
No divergence-register row: GL is the shipping backend and the pixel gate proves
it unmoved; the Vulkan arm is not a retail deviation but a backend under
construction.
Next is V4t, the texture stack, which the world arm cannot be written without.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
46d893f7c3
commit
b16f820643
29 changed files with 2292 additions and 997 deletions
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.Composition;
|
|||
|
||||
internal interface IGameWindowHostInputCameraPublication
|
||||
{
|
||||
void PublishGpuFrameFlights(GpuFrameFlightController value);
|
||||
void PublishGpuFrameFlights(GpuFrameFlightController? value);
|
||||
void PublishGpuDevice(IGpuDevice value);
|
||||
void PublishGpuFrameLifetime(GpuDeviceFrameLifetime value);
|
||||
void PublishKeyboardSource(SilkKeyboardSource value);
|
||||
|
|
@ -21,11 +21,20 @@ internal interface IGameWindowHostInputCameraPublication
|
|||
void PublishCameraPointerInput(CameraPointerInputController value);
|
||||
}
|
||||
|
||||
/// <param name="GpuFrameFlights">
|
||||
/// The GL fence/slot ring, or null on a backend whose RHI device owns its own
|
||||
/// flight control. Campaign V slice V6h: <see cref="Retirement"/> and
|
||||
/// <see cref="FrameSlots"/> are the backend-neutral views every consumer should
|
||||
/// take; this field exists because GL's teardown ledger still names the
|
||||
/// controller itself.
|
||||
/// </param>
|
||||
internal sealed record HostInputCameraResult(
|
||||
GpuFrameFlightController GpuFrameFlights,
|
||||
GpuFrameFlightController? GpuFrameFlights,
|
||||
IGpuResourceRetirementQueue Retirement,
|
||||
IRenderFrameSlotSource FrameSlots,
|
||||
IGpuDevice GpuDevice,
|
||||
GpuDeviceFrameLifetime GpuFrameLifetime,
|
||||
WorldRenderDiagnostics WorldRenderDiagnostics,
|
||||
WorldRenderDiagnostics? WorldRenderDiagnostics,
|
||||
SilkKeyboardSource? KeyboardSource,
|
||||
SilkMouseSource? MouseSource,
|
||||
IMouseLookCursor? MouseLookCursor,
|
||||
|
|
@ -46,14 +55,40 @@ internal sealed record HostInputCameraDependencies(
|
|||
PointerPositionState PointerPosition,
|
||||
IRenderFrameDiagnosticLog RenderDiagnosticLog);
|
||||
|
||||
/// <summary>
|
||||
/// The construction seam every backend differs at. Campaign V slice V6h widened
|
||||
/// the first four members from <c>GL</c> to <see cref="GameWindowGraphics"/> —
|
||||
/// they are the whole of what a backend has to supply before the composition
|
||||
/// pipeline is identical again.
|
||||
/// </summary>
|
||||
internal interface IHostInputCameraCompositionFactory
|
||||
{
|
||||
IFramebufferViewportTarget CreateViewportTarget(GL gl);
|
||||
GpuFrameFlightController CreateGpuFrameFlights(GL gl);
|
||||
IGpuDevice CreateGpuDevice(GL gl, GpuFrameFlightController frameFlights);
|
||||
WorldRenderDiagnostics CreateWorldRenderDiagnostics(
|
||||
GL gl,
|
||||
IFramebufferViewportTarget CreateViewportTarget(GameWindowGraphics graphics);
|
||||
|
||||
/// <summary>The GL fence ring, or null when the backend's RHI device owns its flights.</summary>
|
||||
GpuFrameFlightController? CreateGpuFrameFlights(GameWindowGraphics graphics);
|
||||
|
||||
IGpuDevice CreateGpuDevice(
|
||||
GameWindowGraphics graphics,
|
||||
GpuFrameFlightController? frameFlights);
|
||||
|
||||
/// <summary>Where per-frame resource release is queued. GL's ring, or the RHI device's own.</summary>
|
||||
IGpuResourceRetirementQueue CreateRetirement(
|
||||
GameWindowGraphics graphics,
|
||||
GpuFrameFlightController? frameFlights,
|
||||
IGpuDevice device);
|
||||
|
||||
/// <summary>The ring slot renderers index their per-flight buffers by.</summary>
|
||||
IRenderFrameSlotSource CreateFrameSlots(
|
||||
GameWindowGraphics graphics,
|
||||
GpuFrameFlightController? frameFlights,
|
||||
IGpuDevice device);
|
||||
|
||||
/// <summary>The raw-GL state tripwire, or null on a backend that has no GL state.</summary>
|
||||
WorldRenderDiagnostics? CreateWorldRenderDiagnostics(
|
||||
GameWindowGraphics graphics,
|
||||
IRenderFrameDiagnosticLog log);
|
||||
|
||||
SilkKeyboardSource CreateKeyboardSource(
|
||||
IKeyboard keyboard,
|
||||
HostQuiescenceGate quiescence);
|
||||
|
|
@ -83,21 +118,39 @@ internal interface IHostInputCameraCompositionFactory
|
|||
internal sealed class RetailHostInputCameraCompositionFactory
|
||||
: IHostInputCameraCompositionFactory
|
||||
{
|
||||
public IFramebufferViewportTarget CreateViewportTarget(GL gl) =>
|
||||
new SilkFramebufferViewportTarget(gl);
|
||||
public IFramebufferViewportTarget CreateViewportTarget(GameWindowGraphics graphics) =>
|
||||
new SilkFramebufferViewportTarget(graphics.RequireGl("viewport target"));
|
||||
|
||||
public GpuFrameFlightController CreateGpuFrameFlights(GL gl) => new(gl);
|
||||
public GpuFrameFlightController? CreateGpuFrameFlights(GameWindowGraphics graphics) =>
|
||||
new(graphics.RequireGl("GPU frame flights"));
|
||||
|
||||
public IGpuDevice CreateGpuDevice(GL gl, GpuFrameFlightController frameFlights) =>
|
||||
public IGpuDevice CreateGpuDevice(
|
||||
GameWindowGraphics graphics,
|
||||
GpuFrameFlightController? frameFlights) =>
|
||||
new AcDream.App.Rendering.Gpu.Gl.GlGpuDevice(
|
||||
gl,
|
||||
frameFlights,
|
||||
graphics.RequireGl("GPU device (RHI)"),
|
||||
frameFlights ?? throw new ArgumentNullException(nameof(frameFlights)),
|
||||
Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders"));
|
||||
|
||||
public WorldRenderDiagnostics CreateWorldRenderDiagnostics(
|
||||
GL gl,
|
||||
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(gl), log);
|
||||
new(
|
||||
new SilkRenderGlStateReader(
|
||||
graphics.RequireGl("world render diagnostics")),
|
||||
log);
|
||||
|
||||
public SilkKeyboardSource CreateKeyboardSource(
|
||||
IKeyboard keyboard,
|
||||
|
|
@ -175,13 +228,15 @@ internal enum HostInputCameraCompositionPoint
|
|||
/// </summary>
|
||||
internal sealed class HostInputCameraCompositionPhase :
|
||||
IHostInputCameraCompositionPhase<
|
||||
GameWindowPlatformResult<GL, IInputContext>,
|
||||
GameWindowPlatformResult<GameWindowGraphics, IInputContext>,
|
||||
HostInputCameraResult>
|
||||
{
|
||||
private readonly HostInputCameraDependencies _dependencies;
|
||||
private readonly IGameWindowHostInputCameraPublication _publication;
|
||||
private readonly IHostInputCameraCompositionFactory _factory;
|
||||
private readonly IHostInputCameraCompositionFactory? _injectedFactory;
|
||||
private readonly Action<HostInputCameraCompositionPoint>? _faultInjection;
|
||||
private IHostInputCameraCompositionFactory _factory =
|
||||
new RetailHostInputCameraCompositionFactory();
|
||||
|
||||
public HostInputCameraCompositionPhase(
|
||||
HostInputCameraDependencies dependencies,
|
||||
|
|
@ -193,12 +248,25 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
?? throw new ArgumentNullException(nameof(dependencies));
|
||||
_publication = publication
|
||||
?? throw new ArgumentNullException(nameof(publication));
|
||||
_factory = factory ?? new RetailHostInputCameraCompositionFactory();
|
||||
_injectedFactory = factory;
|
||||
_faultInjection = faultInjection;
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// tests) still wins.
|
||||
/// </summary>
|
||||
private static IHostInputCameraCompositionFactory DefaultFactoryFor(
|
||||
GameWindowGraphics graphics) =>
|
||||
graphics.Backend == RenderBackendKind.Vulkan
|
||||
? new VulkanHostInputCameraCompositionFactory()
|
||||
: new RetailHostInputCameraCompositionFactory();
|
||||
|
||||
public HostInputCameraResult Compose(
|
||||
GameWindowPlatformResult<GL, IInputContext> platform)
|
||||
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(platform);
|
||||
var scope = new CompositionAcquisitionScope();
|
||||
|
|
@ -216,19 +284,23 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
}
|
||||
|
||||
private HostInputCameraResult ComposeCore(
|
||||
GameWindowPlatformResult<GL, IInputContext> platform,
|
||||
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform,
|
||||
CompositionAcquisitionScope scope)
|
||||
{
|
||||
GL gl = platform.Graphics;
|
||||
GameWindowGraphics graphics = platform.Graphics;
|
||||
IInputContext input = platform.Input;
|
||||
_factory = _injectedFactory ?? DefaultFactoryFor(graphics);
|
||||
|
||||
_dependencies.FramebufferResize.BindViewport(
|
||||
_factory.CreateViewportTarget(gl));
|
||||
_factory.CreateViewportTarget(graphics));
|
||||
Fault(HostInputCameraCompositionPoint.ViewportBound);
|
||||
|
||||
GpuFrameFlightController gpuFrames = scope.Acquire(
|
||||
// Null on a backend whose RHI device owns its own frame flights
|
||||
// (Vulkan's timeline semaphore). The publication still runs so the
|
||||
// teardown ledger records the same slot either way.
|
||||
GpuFrameFlightController? gpuFrames = scope.AcquireOptional(
|
||||
"GPU frame flights",
|
||||
() => _factory.CreateGpuFrameFlights(gl),
|
||||
() => _factory.CreateGpuFrameFlights(graphics),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishGpuFrameFlights);
|
||||
Fault(HostInputCameraCompositionPoint.GpuFrameFlightsPublished);
|
||||
|
|
@ -242,10 +314,14 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
// stack so later slices (starting at V4a) have somewhere to plug in.
|
||||
IGpuDevice gpuDevice = scope.Acquire(
|
||||
"GPU device (RHI)",
|
||||
() => _factory.CreateGpuDevice(gl, gpuFrames),
|
||||
() => _factory.CreateGpuDevice(graphics, gpuFrames),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishGpuDevice);
|
||||
Fault(HostInputCameraCompositionPoint.GpuDevicePublished);
|
||||
IGpuResourceRetirementQueue retirement =
|
||||
_factory.CreateRetirement(graphics, gpuFrames, gpuDevice);
|
||||
IRenderFrameSlotSource frameSlots =
|
||||
_factory.CreateFrameSlots(graphics, gpuFrames, gpuDevice);
|
||||
|
||||
// Campaign V slice V4a: drives IGpuDevice.BeginFrame()/IGpuFrame.End()
|
||||
// once per rendered frame, additively over the existing
|
||||
|
|
@ -258,9 +334,9 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice);
|
||||
_publication.PublishGpuFrameLifetime(gpuFrameLifetime);
|
||||
|
||||
WorldRenderDiagnostics diagnostics =
|
||||
WorldRenderDiagnostics? diagnostics =
|
||||
_factory.CreateWorldRenderDiagnostics(
|
||||
gl,
|
||||
graphics,
|
||||
_dependencies.RenderDiagnosticLog);
|
||||
|
||||
IKeyboard? firstKeyboard = input.Keyboards.FirstOrDefault();
|
||||
|
|
@ -357,6 +433,8 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
|
||||
return new HostInputCameraResult(
|
||||
gpuFrames,
|
||||
retirement,
|
||||
frameSlots,
|
||||
gpuDevice,
|
||||
gpuFrameLifetime,
|
||||
diagnostics,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue