164 lines
6.9 KiB
C#
164 lines
6.9 KiB
C#
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Rendering.Gpu.Vk;
|
|
using AcDream.UI.Abstractions.Input;
|
|
using Silk.NET.Input;
|
|
|
|
namespace AcDream.App.Composition;
|
|
|
|
/// <summary>
|
|
/// 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 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
|
|
{
|
|
public IFramebufferViewportTarget CreateViewportTarget(
|
|
GameWindowGraphics graphics) =>
|
|
// The viewport itself is a pipeline dynamic state on Vulkan, set per
|
|
// pass by the encoder from the pass extent — but the swapchain still
|
|
// has to be told the framebuffer changed. Relying on the driver's
|
|
// OUT_OF_DATE/SUBOPTIMAL results alone is spec-insufficient: a
|
|
// conformant driver (observed: Windows AMD, 2026-08-13 user gate —
|
|
// "just stretching the window, not changing the pixel count") may keep
|
|
// presenting the stale-extent swapchain scaled to the new window
|
|
// forever. The V11 null target assumed the driver signal; this target
|
|
// arms the context's existing frame-boundary recreation instead, so
|
|
// the next PrepareFrame recreates at the real FramebufferSize.
|
|
new SwapchainRecreateViewportTarget(RequireContext(graphics).RequestRecreate);
|
|
|
|
public GpuFrameFlightController? CreateGpuFrameFlights(
|
|
GameWindowGraphics graphics) => null;
|
|
|
|
public IGpuDevice CreateGpuDevice(
|
|
GameWindowGraphics graphics,
|
|
GpuFrameFlightController? frameFlights) =>
|
|
RequireContext(graphics).Device;
|
|
|
|
public IGpuResourceRetirementQueue CreateRetirement(
|
|
GameWindowGraphics graphics,
|
|
GpuFrameFlightController? frameFlights,
|
|
IGpuDevice device) => device.Retirement;
|
|
|
|
public IRenderFrameSlotSource CreateFrameSlots(
|
|
GameWindowGraphics graphics,
|
|
GpuFrameFlightController? frameFlights,
|
|
IGpuDevice device) =>
|
|
new VulkanRenderFrameSlotSource(RequireContext(graphics).Device);
|
|
|
|
public WorldRenderDiagnostics? CreateWorldRenderDiagnostics(
|
|
GameWindowGraphics graphics,
|
|
IRenderFrameDiagnosticLog log) => null;
|
|
|
|
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(
|
|
float? initialOrbitDistanceMeters,
|
|
float? initialOrbitYawDegrees,
|
|
float? initialOrbitPitchDegrees)
|
|
{
|
|
var orbit = new OrbitCamera();
|
|
if (initialOrbitDistanceMeters is { } distance)
|
|
orbit.Distance = distance;
|
|
if (initialOrbitYawDegrees is { } yaw)
|
|
orbit.Yaw = DegreesToRadians(yaw);
|
|
if (initialOrbitPitchDegrees is { } pitch)
|
|
orbit.Pitch = DegreesToRadians(pitch);
|
|
return new CameraController(orbit, new FlyCamera());
|
|
}
|
|
|
|
private static float DegreesToRadians(float degrees) =>
|
|
degrees * (MathF.PI / 180f);
|
|
|
|
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());
|
|
|
|
private static VulkanGraphicsContext RequireContext(
|
|
GameWindowGraphics graphics) =>
|
|
graphics.Vulkan
|
|
?? throw new InvalidOperationException(
|
|
"The Vulkan host factory was composed against a backend with no " +
|
|
"Vulkan context.");
|
|
|
|
/// <summary>
|
|
/// The Vulkan implementation of the resize seam (#387): a window resize
|
|
/// arms <see cref="VulkanGraphicsContext.RequestRecreate"/> so the next
|
|
/// <c>PrepareFrame</c> rebuilds the swapchain at the current
|
|
/// FramebufferSize. The width/height arguments are deliberately unused —
|
|
/// the context re-reads the live framebuffer size at the frame boundary,
|
|
/// so a burst of resize events collapses into one recreation and a stale
|
|
/// event can never install a stale extent. Zero-area (minimised) sizes
|
|
/// never reach this target (<see cref="FramebufferResizeController.Resize(int,int)"/>
|
|
/// gates them), and the armed flag survives a minimised skip until a
|
|
/// non-zero frame runs, which is the context's own existing behavior.
|
|
/// </summary>
|
|
internal sealed class SwapchainRecreateViewportTarget(Action requestRecreate)
|
|
: IFramebufferViewportTarget
|
|
{
|
|
private readonly Action _requestRecreate = requestRecreate
|
|
?? throw new ArgumentNullException(nameof(requestRecreate));
|
|
|
|
public void ResizeViewport(int width, int height) => _requestRecreate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The flight slot per-frame buffers index by. Identical in role to the GL
|
|
/// ring's <see cref="GpuFrameFlightController.CurrentSlot"/>; the count comes
|
|
/// from the device's timeline flight controller rather than a fence array.
|
|
/// </summary>
|
|
private sealed class VulkanRenderFrameSlotSource(VulkanGpuDevice device)
|
|
: IRenderFrameSlotSource
|
|
{
|
|
public int CurrentSlot => device.Flights.CurrentSlot;
|
|
}
|
|
}
|