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
|
|
@ -1,38 +1,36 @@
|
|||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Platform;
|
||||
using AcDream.App.Rendering;
|
||||
using Silk.NET.Core.Native;
|
||||
using Silk.NET.Maths;
|
||||
using Silk.NET.Vulkan;
|
||||
using Silk.NET.Vulkan.Extensions.KHR;
|
||||
using Silk.NET.Windowing;
|
||||
using Semaphore = Silk.NET.Vulkan.Semaphore;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V5 — Vulkan bring-up, dark.
|
||||
/// The Vulkan capability-probe and bring-up harness. Reached only when
|
||||
/// <c>ACDREAM_RENDER_BACKEND=vulkan</c> <b>and</b> <c>ACDREAM_VULKAN_PROBE=1</c>.
|
||||
///
|
||||
/// Reached only when <c>ACDREAM_RENDER_BACKEND=vulkan</c>. It opens its own
|
||||
/// window, creates an instance, a surface, a device and a swapchain, runs the
|
||||
/// capability gate, and presents a flat clear colour until the window closes.
|
||||
/// <b>Nothing of the game renders through it</b> — the RHI backend lands at V6
|
||||
/// and the default flips at V10.
|
||||
/// <para><b>What it is for.</b> Answering two questions without starting the
|
||||
/// client: does this machine pass the Vulkan capability gate, and does the RHI
|
||||
/// backend actually draw? It opens a window, acquires a
|
||||
/// <see cref="VulkanGraphicsContext"/> — which runs the gate and writes the
|
||||
/// report — and presents the V6c/V6d verification scenes until the window
|
||||
/// closes. The scenes are deliberately synthetic and deliberately asymmetric:
|
||||
/// the one thing a symmetric layout could never prove is that the
|
||||
/// negative-viewport Y flip and the capture path agree.</para>
|
||||
///
|
||||
/// <para>Deliberately a self-contained host rather than a branch woven into
|
||||
/// <c>GameWindow</c>'s composition: the composition phases each own GL resources
|
||||
/// and would have to grow a backend switch apiece for a slice that draws one
|
||||
/// colour. <c>GameWindow.Run</c> delegates here in four lines and returns, so the
|
||||
/// GL path executes not one new statement.</para>
|
||||
/// <para><b>What it is no longer.</b> Through slice V6g this was the whole
|
||||
/// Vulkan path — a second <c>main()</c> beside <c>GameWindow</c>'s composition.
|
||||
/// Slice V6h moved production Vulkan onto the real composition host and lifted
|
||||
/// the instance/surface/device/swapchain sequence out into
|
||||
/// <see cref="VulkanGraphicsContext"/>, which both now share. What remains here
|
||||
/// is the harness role the class is named for.</para>
|
||||
///
|
||||
/// <para><b>Untested by the implementing slice.</b> Every line below needs a
|
||||
/// window and a driver. The pure decisions it depends on — device ranking,
|
||||
/// present-mode mapping, extent and image-count clamping, the capability
|
||||
/// accept/reject matrix, the report shape, the BGRA swizzle — are unit-tested
|
||||
/// beside it. The remaining gate is manual: "Vulkan boots to a clear
|
||||
/// colour."</para>
|
||||
/// window and a driver. The pure decisions it depends on are unit-tested beside
|
||||
/// it; the remaining gate is manual.</para>
|
||||
/// </summary>
|
||||
internal sealed unsafe class VulkanBringUpHost : IDisposable
|
||||
internal sealed class VulkanBringUpHost : IDisposable
|
||||
{
|
||||
/// <summary>Two frames in flight, matching plan §4.8 and the GL flight controller.</summary>
|
||||
internal const int FlightCount = 2;
|
||||
|
|
@ -46,7 +44,6 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
internal static readonly float[] ClearColor = [0.043f, 0.075f, 0.153f, 1f];
|
||||
|
||||
private const string ScreenshotName = "vulkan-bringup";
|
||||
private const ulong AcquireTimeoutNanoseconds = 1_000_000_000ul;
|
||||
|
||||
private readonly RuntimeOptions _options;
|
||||
private readonly GraphicalHostPlatformServices _platform;
|
||||
|
|
@ -54,31 +51,11 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
private readonly Action<string> _log;
|
||||
|
||||
private IWindow? _window;
|
||||
private Silk.NET.Vulkan.Vk? _vk;
|
||||
private Instance _instance;
|
||||
private KhrSurface? _surfaceApi;
|
||||
private SurfaceKHR _surface;
|
||||
private PhysicalDevice _physicalDevice;
|
||||
private Device _device;
|
||||
private KhrSwapchain? _swapchainApi;
|
||||
private Queue _graphicsQueue;
|
||||
private Queue _presentQueue;
|
||||
private VulkanQueueFamilyChoice? _families;
|
||||
private VulkanSwapchain? _swapchain;
|
||||
|
||||
|
||||
private ulong _frameSerial;
|
||||
private bool _recreateAtFrameBoundary;
|
||||
private bool _disposed;
|
||||
|
||||
// ── Campaign V slice V6c: the RHI backend and the scene that proves it ──
|
||||
private VulkanGpuDevice? _gpuDevice;
|
||||
private VulkanGraphicsContext? _graphics;
|
||||
private VulkanRhiScene? _scene;
|
||||
private VulkanRetainedUiScene? _ui;
|
||||
private VulkanDebugNames _debugNames = VulkanDebugNames.Disabled;
|
||||
private VulkanDeviceFeatureSupport? _features;
|
||||
private VulkanDeviceLimitSupport? _limits;
|
||||
private VulkanFormatSupport? _formats;
|
||||
private ulong _frameSerial;
|
||||
private bool _disposed;
|
||||
|
||||
internal VulkanBringUpHost(
|
||||
RuntimeOptions options,
|
||||
|
|
@ -91,9 +68,8 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
_log = log ?? Console.WriteLine;
|
||||
// Resolved from the same pure policy the GL path uses, so "VSync on means
|
||||
// FIFO" is one decision expressed once. The monitor refresh is left
|
||||
// unknown because slice V5 consumes only UseVSync: there is no software
|
||||
// pacer here to feed a limit to, and inventing one would be a number
|
||||
// nothing reads. V6 wires FramePacingController and supplies it.
|
||||
// unknown because the harness consumes only UseVSync: there is no
|
||||
// software pacer here to feed a limit to.
|
||||
_pacing = FramePacingPolicy.Resolve(
|
||||
requestedVSync,
|
||||
_options.UncappedRendering,
|
||||
|
|
@ -101,22 +77,32 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>The record the gate evaluated, available after <see cref="Run"/> starts.</summary>
|
||||
internal VulkanCapabilityRecord? Capabilities { get; private set; }
|
||||
internal VulkanCapabilityRecord? Capabilities => _graphics?.Capabilities;
|
||||
|
||||
/// <summary>
|
||||
/// Open the window, pass the capability gate, and present the clear colour
|
||||
/// until the window closes. Throws <see cref="NotSupportedException"/> when
|
||||
/// the gate rejects the device, which <c>Program.cs</c> turns into exit code
|
||||
/// 4 exactly as it does for the GL gate.
|
||||
/// Open the window, pass the capability gate, and present the verification
|
||||
/// scenes until the window closes. Throws <see cref="NotSupportedException"/>
|
||||
/// when the gate rejects the device, which <c>Program.cs</c> turns into exit
|
||||
/// code 4 exactly as it does for the GL gate.
|
||||
/// </summary>
|
||||
internal void Run()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
CreateWindow();
|
||||
CreateInstanceAndSurface();
|
||||
SelectDeviceAndGate();
|
||||
CreateFrameResources();
|
||||
_graphics = VulkanGraphicsContext.Acquire(
|
||||
_window!,
|
||||
_options,
|
||||
_platform,
|
||||
_pacing,
|
||||
// Four samples where the device allows it, so the backbuffer pass
|
||||
// really resolves rather than rendering straight into the swapchain
|
||||
// image. Plan §4.10 records that the V7 differential must force MSAA
|
||||
// off; this is not that gate, and a resolve path that is never
|
||||
// exercised is a resolve path that does not work.
|
||||
requestedSampleCount: 4,
|
||||
_log);
|
||||
CreateScenes();
|
||||
Present();
|
||||
}
|
||||
|
||||
|
|
@ -125,341 +111,38 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
var options = WindowOptions.DefaultVulkan with
|
||||
{
|
||||
Size = new Vector2D<int>(1280, 720),
|
||||
Title = "acdream — Vulkan bring-up (Campaign V slice V5)",
|
||||
Title = "acdream — Vulkan capability probe",
|
||||
VSync = _pacing.UseVSync,
|
||||
};
|
||||
_window = Window.Create(options);
|
||||
_window.Initialize();
|
||||
if (_window.VkSurface is null)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"The windowing backend did not expose a Vulkan surface. " +
|
||||
"acdream requires GLFW 3.4 built with Vulkan support.");
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateInstanceAndSurface()
|
||||
private void CreateScenes()
|
||||
{
|
||||
IWindow window = _window!;
|
||||
_vk = Silk.NET.Vulkan.Vk.GetApi();
|
||||
|
||||
byte** requiredNames = window.VkSurface!.GetRequiredExtensions(out uint requiredCount);
|
||||
var required = new List<string>((int)requiredCount);
|
||||
for (uint i = 0; i < requiredCount; i++)
|
||||
required.Add(VulkanInterop.ReadString(requiredNames[i]));
|
||||
|
||||
VulkanInstanceFactory.Created instance = VulkanInstanceFactory.Create(
|
||||
_vk,
|
||||
required,
|
||||
enableOptionalExtensions: _options.DevTools);
|
||||
_instance = instance.Instance;
|
||||
InstanceExtensions = instance.EnabledExtensions;
|
||||
|
||||
if (!_vk.TryGetInstanceExtension(_instance, out KhrSurface surfaceApi))
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"VK_KHR_surface is required but its entry points could not be loaded.");
|
||||
}
|
||||
|
||||
_surfaceApi = surfaceApi;
|
||||
_surface = window.VkSurface.Create<AllocationCallbacks>(
|
||||
_instance.ToHandle(),
|
||||
null).ToSurface();
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> InstanceExtensions { get; set; } = [];
|
||||
|
||||
private void SelectDeviceAndGate()
|
||||
{
|
||||
Silk.NET.Vulkan.Vk vk = _vk!;
|
||||
IReadOnlyList<VulkanPhysicalDeviceCandidate> candidates =
|
||||
VulkanPhysicalDeviceInspector.Enumerate(vk, _instance, out PhysicalDevice[] handles);
|
||||
VulkanPhysicalDeviceChoice? choice = VulkanPhysicalDeviceSelection.Choose(
|
||||
candidates,
|
||||
_options.VulkanDeviceOverride);
|
||||
if (choice is null)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"No Vulkan physical device was enumerated. Install or update a " +
|
||||
"Vulkan 1.3 driver for this GPU.");
|
||||
}
|
||||
|
||||
_physicalDevice = handles[choice.Device.Index];
|
||||
|
||||
IReadOnlyList<VulkanQueueFamilyCandidate> queueFamilies =
|
||||
VulkanPhysicalDeviceInspector.ReadQueueFamilies(
|
||||
vk,
|
||||
_physicalDevice,
|
||||
_surfaceApi,
|
||||
_surface);
|
||||
VulkanQueueFamilyChoice? families = VulkanQueueFamilySelection.Choose(queueFamilies);
|
||||
if (families is null)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"'{choice.Device.DeviceName}' exposes no queue family that can both " +
|
||||
"render and present to the window surface.");
|
||||
}
|
||||
|
||||
_families = families;
|
||||
VulkanLogicalDeviceFactory.Created created = VulkanLogicalDeviceFactory.Create(
|
||||
vk,
|
||||
_physicalDevice,
|
||||
families,
|
||||
requireSwapchain: true);
|
||||
_device = created.Device;
|
||||
_graphicsQueue = created.GraphicsQueue;
|
||||
_presentQueue = created.PresentQueue;
|
||||
|
||||
if (!vk.TryGetDeviceExtension(_instance, _device, out KhrSwapchain swapchainApi))
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"VK_KHR_swapchain is required but its entry points could not be loaded.");
|
||||
}
|
||||
|
||||
_swapchainApi = swapchainApi;
|
||||
_swapchain = new VulkanSwapchain(
|
||||
vk,
|
||||
_surfaceApi!,
|
||||
swapchainApi,
|
||||
_physicalDevice,
|
||||
_device,
|
||||
_surface,
|
||||
families);
|
||||
|
||||
(SurfaceCapabilitiesKHR surfaceCapabilities,
|
||||
IReadOnlyList<SurfaceFormatKHR> formats,
|
||||
IReadOnlyList<PresentModeKHR> presentModes) = _swapchain.QuerySurface();
|
||||
|
||||
Vector2D<int> framebuffer = _window!.FramebufferSize;
|
||||
VulkanSwapchainConfiguration planned = VulkanSwapchainConfigurationFactory.Create(
|
||||
surfaceCapabilities,
|
||||
formats,
|
||||
presentModes,
|
||||
_pacing,
|
||||
(uint)Math.Max(0, framebuffer.X),
|
||||
(uint)Math.Max(0, framebuffer.Y));
|
||||
|
||||
var surfaceSupport = new VulkanSurfaceSupport(
|
||||
PresentSupported: true,
|
||||
SelectedFormat: planned.ImageFormat,
|
||||
SelectedColorSpace: planned.ColorSpace,
|
||||
SelectedPresentMode: planned.PresentMode,
|
||||
SelectedImageCount: planned.ImageCount,
|
||||
SelectedWidth: planned.Width,
|
||||
SelectedHeight: planned.Height,
|
||||
SupportsTransferSource:
|
||||
VulkanSwapchainConfigurationFactory.SupportsTransferSource(surfaceCapabilities),
|
||||
AvailableFormats: [.. formats.Select(format => format.Format).Distinct()],
|
||||
AvailablePresentModes: [.. presentModes]);
|
||||
|
||||
VulkanFunctionProbeResult probe = VulkanActiveDeviceProbe.Run(
|
||||
vk,
|
||||
_physicalDevice,
|
||||
_device,
|
||||
_graphicsQueue,
|
||||
families.GraphicsFamily);
|
||||
|
||||
_features = VulkanPhysicalDeviceInspector.ReadFeatures(vk, _physicalDevice);
|
||||
_limits = VulkanPhysicalDeviceInspector.ReadLimits(vk, _physicalDevice);
|
||||
_formats = VulkanPhysicalDeviceInspector.ReadFormats(
|
||||
vk,
|
||||
_physicalDevice,
|
||||
VulkanSwapchainConfigurationFactory.OffersUnormFormat(formats));
|
||||
|
||||
var record = new VulkanCapabilityRecord(
|
||||
DateTimeOffset.UtcNow,
|
||||
_platform.RuntimeIdentifier,
|
||||
_platform.OperatingSystem,
|
||||
_platform.WindowBackend.RequestedProtocol,
|
||||
GlfwNativePlatformProbe.GetActiveProtocol(_platform.OperatingSystem),
|
||||
VulkanApiVersion.Describe(
|
||||
VulkanApiVersion.Make(
|
||||
VulkanCapabilityRequirements.RequiredApiMajor,
|
||||
VulkanCapabilityRequirements.RequiredApiMinor,
|
||||
0)),
|
||||
VulkanApiVersion.Describe(choice.Device.ApiVersion),
|
||||
choice.Device.ApiVersion,
|
||||
choice.Device.DeviceName,
|
||||
VulkanPhysicalDeviceInspector.DescribeDriver(choice.Device),
|
||||
choice.Device.DeviceType,
|
||||
choice.Device.Index,
|
||||
choice.Reason,
|
||||
_options.VulkanDeviceOverride,
|
||||
ForcedUnsupportedFeature: null,
|
||||
candidates,
|
||||
InstanceExtensions,
|
||||
created.EnabledExtensions,
|
||||
families.GraphicsFamily,
|
||||
families.PresentFamily,
|
||||
_features,
|
||||
_limits,
|
||||
_formats,
|
||||
surfaceSupport,
|
||||
probe,
|
||||
SupportFailures: []);
|
||||
|
||||
record = VulkanCapabilityRequirements.Reevaluate(record);
|
||||
record = VulkanCapabilityRequirements.ApplyForcedUnsupported(
|
||||
record,
|
||||
_options.VulkanForcedUnsupportedFeature);
|
||||
Capabilities = record;
|
||||
|
||||
string reportPath = Path.Combine(
|
||||
_platform.Paths.DiagnosticsDirectory,
|
||||
VulkanCapabilityGuard.ReportFileName);
|
||||
VulkanCapabilityReportWriter.Write(reportPath, record);
|
||||
VulkanCapabilityGuard.ThrowIfUnsupported(record, reportPath);
|
||||
|
||||
_log(
|
||||
"vulkan: capability gate passed " +
|
||||
$"({record.ActiveDisplayProtocol}, {record.DeviceName}, " +
|
||||
$"{record.DeviceApiVersion}, {record.DriverInfo}); " +
|
||||
$"swapchain {planned.ImageFormat}/{planned.PresentMode} " +
|
||||
$"{planned.Width}x{planned.Height} x{planned.ImageCount}; " +
|
||||
$"report={reportPath}");
|
||||
_log($"vulkan: device selection — {choice.Reason}");
|
||||
}
|
||||
|
||||
private bool RecreateSwapchain()
|
||||
{
|
||||
Vector2D<int> framebuffer = _window!.FramebufferSize;
|
||||
uint width = (uint)Math.Max(0, framebuffer.X);
|
||||
uint height = (uint)Math.Max(0, framebuffer.Y);
|
||||
if (VulkanSwapchainRecreationPolicy.OnFramebufferSize(width, height)
|
||||
== VulkanSwapchainAction.Idle)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
VulkanInterop.Check(_vk!.DeviceWaitIdle(_device), "vkDeviceWaitIdle (recreate)");
|
||||
_recreateAtFrameBoundary = false;
|
||||
return _swapchain!.Recreate(_pacing, width, height);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6c: build the RHI backend and the scene that proves it.
|
||||
///
|
||||
/// <para>The host's own command pools, acquire semaphores and timeline are
|
||||
/// gone — <see cref="VulkanGpuDevice"/> owns all three now, because a frame
|
||||
/// recorded through the contract has to be the same frame that presents. The
|
||||
/// host keeps exactly what the contract deliberately does not cover:
|
||||
/// swapchain configuration and the OUT_OF_DATE/SUBOPTIMAL policy, both of
|
||||
/// which are slice V5's pure, unit-tested decisions.</para>
|
||||
/// </summary>
|
||||
private void CreateFrameResources()
|
||||
{
|
||||
Silk.NET.Vulkan.Vk vk = _vk!;
|
||||
_debugNames = VulkanDebugNames.Create(vk, _instance, _device, [.. InstanceExtensions]);
|
||||
|
||||
if (!RecreateSwapchain())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The swapchain could not be created for the initial framebuffer size.");
|
||||
}
|
||||
|
||||
VulkanSwapchainConfiguration configuration = _swapchain!.Configuration!;
|
||||
_gpuDevice = new VulkanGpuDevice(
|
||||
vk,
|
||||
_physicalDevice,
|
||||
_device,
|
||||
_graphicsQueue,
|
||||
_presentQueue,
|
||||
_families!.GraphicsFamily,
|
||||
_features!,
|
||||
_limits!,
|
||||
_formats!,
|
||||
Capabilities!.DeviceName,
|
||||
Capabilities.DriverInfo,
|
||||
Capabilities.DeviceApiVersion,
|
||||
_debugNames,
|
||||
new SwapchainBackbuffer(_swapchain!, _presentQueue),
|
||||
ShaderSpirvDirectory(),
|
||||
_platform.Paths.CacheDirectory,
|
||||
// Slice V6g: a frame can only be read back while it still owns its
|
||||
// swapchain image, so retention has to be armed before the first
|
||||
// frame rather than at the moment a screenshot is asked for. Armed
|
||||
// exactly when an artifact directory exists, which is what a gate
|
||||
// run has and a player run does not.
|
||||
retainBackbufferCapture: !string.IsNullOrWhiteSpace(_options.AutomationArtifactDirectory));
|
||||
|
||||
// Four samples where the device allows it, so the backbuffer pass really
|
||||
// resolves rather than rendering straight into the swapchain image.
|
||||
// Plan §4.10 records that the V7 differential must force MSAA off; this
|
||||
// is not that gate, and a resolve path that is never exercised is a
|
||||
// resolve path that does not work.
|
||||
int sampleCount = (int)Math.Min(4u, Math.Max(1u, _gpuDevice.Capabilities.MaxSampleCount));
|
||||
_gpuDevice.ConfigureBackbufferAttachments(
|
||||
configuration.Width,
|
||||
configuration.Height,
|
||||
configuration.ImageFormat,
|
||||
sampleCount);
|
||||
|
||||
_scene = new VulkanRhiScene(_gpuDevice, sampleCount);
|
||||
_log(
|
||||
$"vulkan: RHI backend up — {_gpuDevice.Allocator.Describe()}, " +
|
||||
$"{sampleCount}x MSAA, pipeline cache " +
|
||||
(_gpuDevice.PipelineCacheLoadedFromDisk ? "reused" : "cold") +
|
||||
$", debug names {(_debugNames.IsEnabled ? "on" : "off")}");
|
||||
|
||||
// Campaign V slice V6d: the first production renderers on Vulkan. The
|
||||
// retained UI and the debug lines draw here through exactly the classes
|
||||
// the GL client uses — the scene only supplies a widget tree and its
|
||||
// sprites, because the retail tree's chrome still comes from a GL-only
|
||||
// TextureCache until V4t.
|
||||
_ui = new VulkanRetainedUiScene(_gpuDevice, ShaderSpirvDirectory());
|
||||
VulkanGraphicsContext graphics = _graphics!;
|
||||
_scene = new VulkanRhiScene(graphics.Device, graphics.SampleCount);
|
||||
// Campaign V slice V6d: the retained UI and the debug lines draw here
|
||||
// through exactly the classes the client uses, with a generated widget
|
||||
// tree. Slice V6h's composition host draws the client's own tree; this
|
||||
// keeps the isolated, session-free version reproducible.
|
||||
_ui = new VulkanRetainedUiScene(
|
||||
graphics.Device,
|
||||
VulkanGraphicsContext.ShaderSpirvDirectory());
|
||||
_log(
|
||||
"vulkan: retained UI up — TextRenderer and DebugLineRenderer on the Vulkan device" +
|
||||
(_ui.HasFont ? string.Empty : " (no system font found; glyph draws are skipped)"));
|
||||
}
|
||||
|
||||
/// <summary>Where the committed SPIR-V lives beside the binary.</summary>
|
||||
private static string ShaderSpirvDirectory() =>
|
||||
Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders", "spv");
|
||||
|
||||
/// <summary>
|
||||
/// Adapts slice V5's swapchain to the narrow surface the RHI device needs.
|
||||
/// The device deliberately does not own presentation: format, extent,
|
||||
/// present-mode and the recreation policy are pure decisions that are
|
||||
/// already unit-tested, and duplicating that judgement inside the backend
|
||||
/// would fork it.
|
||||
/// </summary>
|
||||
private sealed class SwapchainBackbuffer(VulkanSwapchain swapchain, Queue presentQueue) : IVulkanBackbuffer
|
||||
{
|
||||
public Format ImageFormat => swapchain.Configuration!.ImageFormat;
|
||||
|
||||
public uint Width => swapchain.Configuration!.Width;
|
||||
|
||||
public uint Height => swapchain.Configuration!.Height;
|
||||
|
||||
public bool TryAcquire(Semaphore acquired, out uint imageIndex)
|
||||
{
|
||||
VulkanSwapchainAction action = swapchain.TryAcquire(
|
||||
acquired,
|
||||
AcquireTimeoutNanoseconds,
|
||||
out imageIndex);
|
||||
return action is VulkanSwapchainAction.Continue
|
||||
or VulkanSwapchainAction.RecreateAtFrameBoundary;
|
||||
}
|
||||
|
||||
public Image ImageAt(uint imageIndex) => swapchain.ImageAt(imageIndex);
|
||||
|
||||
public ImageView ViewAt(uint imageIndex) => swapchain.ViewAt(imageIndex);
|
||||
|
||||
public Semaphore RenderCompleteAt(uint imageIndex) => swapchain.RenderCompleteAt(imageIndex);
|
||||
|
||||
public bool Present(uint imageIndex) =>
|
||||
swapchain.Present(presentQueue, imageIndex) is VulkanSwapchainAction.Continue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The frame loop: record the verification scene through the RHI, present,
|
||||
/// The frame loop: record the verification scenes through the RHI, present,
|
||||
/// and capture one screenshot once the scene has settled.
|
||||
/// </summary>
|
||||
private void Present()
|
||||
{
|
||||
IWindow window = _window!;
|
||||
VulkanGpuDevice device = _gpuDevice!;
|
||||
VulkanGraphicsContext graphics = _graphics!;
|
||||
VulkanGpuDevice device = graphics.Device;
|
||||
VulkanRhiScene scene = _scene!;
|
||||
FrameScreenshotController? screenshots = CreateScreenshotController();
|
||||
bool screenshotRequested = false;
|
||||
|
|
@ -471,45 +154,33 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
if (window.IsClosing)
|
||||
break;
|
||||
|
||||
if (_recreateAtFrameBoundary || !_swapchain!.IsCreated)
|
||||
if (!graphics.PrepareFrame())
|
||||
{
|
||||
if (!RecreateSwapchain())
|
||||
{
|
||||
// Minimised: idle without burning a core, and without
|
||||
// pretending a zero-area swapchain can be created.
|
||||
Thread.Sleep(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
VulkanSwapchainConfiguration resized = _swapchain!.Configuration!;
|
||||
device.ConfigureBackbufferAttachments(
|
||||
resized.Width,
|
||||
resized.Height,
|
||||
resized.ImageFormat,
|
||||
scene.SampleCount);
|
||||
// Minimised: idle without burning a core, and without pretending
|
||||
// a zero-area swapchain can be created.
|
||||
Thread.Sleep(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!device.TryBeginFrame(out IGpuFrame? frame) || frame is null)
|
||||
{
|
||||
_recreateAtFrameBoundary = true;
|
||||
graphics.RequestRecreate();
|
||||
continue;
|
||||
}
|
||||
|
||||
VulkanSwapchainConfiguration configuration = _swapchain!.Configuration!;
|
||||
double elapsed = (DateTimeOffset.UtcNow - started).TotalSeconds;
|
||||
using (frame)
|
||||
{
|
||||
scene.Render(frame, configuration.Width, configuration.Height, elapsed);
|
||||
scene.Render(frame, graphics.Width, graphics.Height, elapsed);
|
||||
// After the 3-D scene, in its own single-sampled load/store pass
|
||||
// against the backbuffer — the same shape the GL client's HUD
|
||||
// phase has, and the reason the multisampled pass must resolve
|
||||
// rather than store.
|
||||
_ui?.Render(frame, configuration.Width, configuration.Height, elapsed);
|
||||
// against the backbuffer — the same shape the client's HUD phase
|
||||
// has, and the reason the multisampled pass must resolve rather
|
||||
// than store.
|
||||
_ui?.Render(frame, graphics.Width, graphics.Height, elapsed);
|
||||
}
|
||||
|
||||
_frameSerial = (ulong)frame.Serial;
|
||||
if (!device.PresentSucceeded)
|
||||
_recreateAtFrameBoundary = true;
|
||||
graphics.NoteFrameClosed();
|
||||
|
||||
// Capture after a few frames so the timer pool has resolved and the
|
||||
// ring has cycled through both flight slots at least once.
|
||||
|
|
@ -518,7 +189,7 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
screenshotRequested = true;
|
||||
if (screenshots.TryRequest(ScreenshotName, out string error))
|
||||
{
|
||||
screenshots.CapturePending((int)configuration.Width, (int)configuration.Height);
|
||||
screenshots.CapturePending((int)graphics.Width, (int)graphics.Height);
|
||||
ReportTimings(device);
|
||||
}
|
||||
else
|
||||
|
|
@ -528,7 +199,7 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
VulkanInterop.Check(_vk!.DeviceWaitIdle(_device), "vkDeviceWaitIdle (shutdown)");
|
||||
device.WaitIdle();
|
||||
_log($"vulkan: presented {_frameSerial} RHI frame(s); shutting down.");
|
||||
}
|
||||
|
||||
|
|
@ -558,11 +229,9 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
// IGpuDevice.CaptureBackbuffer is documented top-left-origin and a
|
||||
// Vulkan image already is; FrameScreenshotController flips what it
|
||||
// receives because glReadPixels hands back bottom-up rows. Flipping
|
||||
// here makes the two cancel, so the PNG is right-side-up — and it
|
||||
// routes the screenshot through the RHI capture path, which is the
|
||||
// thing slice V6c has to prove rather than assume.
|
||||
// here makes the two cancel, so the PNG is right-side-up.
|
||||
(width, height) => FrameScreenshotController.FlipRows(
|
||||
_gpuDevice!.CaptureBackbuffer(width, height),
|
||||
_graphics!.Device.CaptureBackbuffer(width, height),
|
||||
width,
|
||||
height),
|
||||
_options.AutomationArtifactDirectory,
|
||||
|
|
@ -570,10 +239,10 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Teardown in strict reverse-construction order. Every handle is checked
|
||||
/// before destruction because <see cref="Run"/> can throw at any stage — a
|
||||
/// rejected capability gate is a normal, expected exit, not a crash, and it
|
||||
/// must still leave zero Vulkan objects behind.
|
||||
/// Teardown in strict reverse-construction order. The scenes go before the
|
||||
/// context: they own buffers, textures, render targets and pipelines whose
|
||||
/// release routes through the device's retirement queue, so the device has to
|
||||
/// still be alive to drain it.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -581,62 +250,12 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
return;
|
||||
_disposed = true;
|
||||
|
||||
Silk.NET.Vulkan.Vk? vk = _vk;
|
||||
if (vk is not null && _device.Handle != 0)
|
||||
{
|
||||
vk.DeviceWaitIdle(_device);
|
||||
|
||||
// Scene before device: the scene owns buffers, textures, render
|
||||
// targets and pipelines whose release routes through the device's
|
||||
// retirement queue, so the device has to still be alive to drain it.
|
||||
_ui?.Dispose();
|
||||
_ui = null;
|
||||
_scene?.Dispose();
|
||||
_scene = null;
|
||||
_gpuDevice?.Dispose();
|
||||
_gpuDevice = null;
|
||||
|
||||
_swapchain?.Dispose();
|
||||
_swapchain = null;
|
||||
|
||||
vk.DestroyDevice(_device, null);
|
||||
_device = default;
|
||||
}
|
||||
else
|
||||
{
|
||||
_ui?.Dispose();
|
||||
_ui = null;
|
||||
_scene?.Dispose();
|
||||
_scene = null;
|
||||
_gpuDevice?.Dispose();
|
||||
_gpuDevice = null;
|
||||
_swapchain?.Dispose();
|
||||
_swapchain = null;
|
||||
}
|
||||
|
||||
_debugNames.Dispose();
|
||||
_debugNames = VulkanDebugNames.Disabled;
|
||||
|
||||
if (vk is not null && _surfaceApi is not null && _surface.Handle != 0)
|
||||
{
|
||||
_surfaceApi.DestroySurface(_instance, _surface, null);
|
||||
_surface = default;
|
||||
}
|
||||
|
||||
_swapchainApi?.Dispose();
|
||||
_swapchainApi = null;
|
||||
_surfaceApi?.Dispose();
|
||||
_surfaceApi = null;
|
||||
|
||||
if (vk is not null && _instance.Handle != 0)
|
||||
{
|
||||
vk.DestroyInstance(_instance, null);
|
||||
_instance = default;
|
||||
}
|
||||
|
||||
vk?.Dispose();
|
||||
_vk = null;
|
||||
|
||||
_ui?.Dispose();
|
||||
_ui = null;
|
||||
_scene?.Dispose();
|
||||
_scene = null;
|
||||
_graphics?.Dispose();
|
||||
_graphics = null;
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue