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;
|
||||
}
|
||||
|
|
|
|||
239
src/AcDream.App/Rendering/Gpu/Vk/VulkanCompositionFramePhases.cs
Normal file
239
src/AcDream.App/Rendering/Gpu/Vk/VulkanCompositionFramePhases.cs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6h: the Vulkan arm of the frame spine's clear phase.
|
||||
///
|
||||
/// <para>The GL arm establishes frame-global capability state and issues
|
||||
/// <c>glClear</c> against framebuffer 0. Vulkan has neither: a clear is a pass
|
||||
/// load-op, and there is no global state to restore. So this opens one
|
||||
/// backbuffer pass with <see cref="GpuLoadOp.Clear"/>, draws nothing, and closes
|
||||
/// it — which resolves the multisampled scratch image into the acquired
|
||||
/// swapchain image and leaves it in exactly the state the retained UI's own
|
||||
/// load/store pass expects.</para>
|
||||
///
|
||||
/// <para>It computes the same <see cref="RenderFrameFoundation"/> the GL arm
|
||||
/// does, from the same world clock and weather owners, so every consumer
|
||||
/// downstream — portal viewport visibility, sky keyframe, atmosphere — reads
|
||||
/// identical values on both backends.</para>
|
||||
/// </summary>
|
||||
internal sealed class VulkanRenderFrameClearPhase : IRenderFrameClearPhase
|
||||
{
|
||||
private readonly ICurrentGpuFrameSource _frames;
|
||||
private readonly WorldTimeService _worldTime;
|
||||
private readonly WeatherSystem _weather;
|
||||
private readonly IRenderFramePortalStateSource _portal;
|
||||
private readonly ParticleVisibilityController _particleVisibility;
|
||||
private readonly Func<int> _sampleCount;
|
||||
|
||||
public VulkanRenderFrameClearPhase(
|
||||
ICurrentGpuFrameSource frames,
|
||||
WorldTimeService worldTime,
|
||||
WeatherSystem weather,
|
||||
IRenderFramePortalStateSource portal,
|
||||
ParticleVisibilityController particleVisibility,
|
||||
Func<int> sampleCount)
|
||||
{
|
||||
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
|
||||
_worldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime));
|
||||
_weather = weather ?? throw new ArgumentNullException(nameof(weather));
|
||||
_portal = portal ?? throw new ArgumentNullException(nameof(portal));
|
||||
_particleVisibility = particleVisibility
|
||||
?? throw new ArgumentNullException(nameof(particleVisibility));
|
||||
_sampleCount = sampleCount ?? throw new ArgumentNullException(nameof(sampleCount));
|
||||
}
|
||||
|
||||
public RenderFrameFoundation Clear()
|
||||
{
|
||||
bool portalViewportVisible = _portal.IsPortalViewportVisible;
|
||||
if (portalViewportVisible)
|
||||
_particleVisibility.Reset();
|
||||
|
||||
SkyKeyframe sky = _worldTime.CurrentSky;
|
||||
AtmosphereSnapshot atmosphere = _weather.Snapshot(in sky);
|
||||
// SceneTool::BeginScene @ 0x0043DAD0 starts the replacement CreatureMode
|
||||
// frame with an opaque black target; otherwise the fog colour is the
|
||||
// frame's ground truth, exactly as on GL.
|
||||
Vector4 clear = portalViewportVisible
|
||||
? new Vector4(0f, 0f, 0f, 1f)
|
||||
: new Vector4(
|
||||
Math.Clamp(atmosphere.FogColor.X, 0f, 1f),
|
||||
Math.Clamp(atmosphere.FogColor.Y, 0f, 1f),
|
||||
Math.Clamp(atmosphere.FogColor.Z, 0f, 1f),
|
||||
1f);
|
||||
|
||||
if (_frames.CurrentFrame is { } frame)
|
||||
{
|
||||
using IGpuPassEncoder pass = frame.BeginPass(
|
||||
GpuPassDescription.BackbufferClear(
|
||||
"vk-frame-clear",
|
||||
clear,
|
||||
_sampleCount()));
|
||||
}
|
||||
|
||||
return new RenderFrameFoundation(portalViewportVisible, sky, atmosphere);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6h: the Vulkan arm's world-scene phase.
|
||||
///
|
||||
/// <para>There is nothing to draw. Every world renderer — terrain, statics,
|
||||
/// EnvCells, sky, particles, the portal depth mask — is still raw GL, and the
|
||||
/// slice that ports them is V4t plus the world arm behind it. The phase exists
|
||||
/// so the frame spine's contract is identical on both backends: the world phase
|
||||
/// runs, reports what it drew, and the private-presentation phase composites the
|
||||
/// retained UI over whatever it left behind.</para>
|
||||
///
|
||||
/// <para>Reporting <c>default</c> — zero visible, zero total, world not drawn —
|
||||
/// is the honest answer and is what the lifecycle artifacts record.</para>
|
||||
/// </summary>
|
||||
internal sealed class VulkanWorldScenePhase : IWorldSceneFramePhase
|
||||
{
|
||||
public static VulkanWorldScenePhase Instance { get; } = new();
|
||||
|
||||
private VulkanWorldScenePhase()
|
||||
{
|
||||
}
|
||||
|
||||
public WorldRenderFrameOutcome Render(RenderFrameInput input) => default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6h: no GPU-timer bracket on the Vulkan arm.
|
||||
///
|
||||
/// <para><see cref="FrameProfilerGpuMeasurement"/> drives
|
||||
/// <c>FrameProfiler</c>'s GL query ring, which is a GL-only instrument.
|
||||
/// <see cref="IGpuDevice.Timers"/> is its backend-neutral replacement and the
|
||||
/// frame spine adopts it at slice V4h; until then the Vulkan arm reports no GPU
|
||||
/// samples rather than reporting wrong ones.</para>
|
||||
/// </summary>
|
||||
internal sealed class NullRenderFrameGpuMeasurement : IRenderFrameGpuMeasurement
|
||||
{
|
||||
public static NullRenderFrameGpuMeasurement Instance { get; } = new();
|
||||
|
||||
private NullRenderFrameGpuMeasurement()
|
||||
{
|
||||
}
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
}
|
||||
|
||||
public void EndFrame()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6h: the mesh backend on a backend that has none.
|
||||
///
|
||||
/// <para><c>WbMeshAdapter</c> owns an <c>OpenGLGraphicsDevice</c>, so it is not
|
||||
/// constructible on Vulkan until slice V4t. The landblock spawn ledger and the
|
||||
/// world state that drives it are backend-neutral and must keep running — they
|
||||
/// are how streaming residence is tracked — so they register against this
|
||||
/// instead. Reference counting is a no-op because there is nothing to count,
|
||||
/// and <see cref="IsRenderDataReady"/> answers true because a mesh that is never
|
||||
/// going to be drawn is never pending.</para>
|
||||
/// </summary>
|
||||
internal sealed class NullWbMeshAdapter : AcDream.App.Rendering.Wb.IWbMeshAdapter
|
||||
{
|
||||
public static NullWbMeshAdapter Instance { get; } = new();
|
||||
|
||||
private NullWbMeshAdapter()
|
||||
{
|
||||
}
|
||||
|
||||
public void IncrementRefCount(ulong id)
|
||||
{
|
||||
}
|
||||
|
||||
public void DecrementRefCount(ulong id)
|
||||
{
|
||||
}
|
||||
|
||||
public void PinPreparedRenderData(ulong id)
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsRenderDataReady(ulong id) => true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6h: the portal viewport on a backend with no portal tunnel.
|
||||
///
|
||||
/// <para><c>PortalTunnelPresentation</c> is a raw-GL renderer, so the Vulkan arm
|
||||
/// composes none and the teleport controller drives this instead. Every state
|
||||
/// query answers "no tunnel is showing", which is true, and keeps the
|
||||
/// portal-space lifecycle's own invariants — reveal generation, destination
|
||||
/// latch, wait cue — running unchanged in Runtime.</para>
|
||||
/// </summary>
|
||||
internal sealed class NullLocalPlayerTeleportPresentation
|
||||
: ILocalPlayerTeleportPresentation
|
||||
{
|
||||
private readonly TeleportAnimSequencer _animation = new();
|
||||
private readonly TeleportViewPlaneController _viewPlane = new();
|
||||
|
||||
/// <summary>Always false: with no tunnel renderer there is no replacement viewport.</summary>
|
||||
public bool IsPortalViewportVisible => false;
|
||||
|
||||
public int CurrentTunnelFrame => 0;
|
||||
|
||||
public void Begin(Matrix4x4 projection)
|
||||
{
|
||||
_viewPlane.Begin(projection);
|
||||
_animation.Begin(TeleportEntryKind.Portal);
|
||||
}
|
||||
|
||||
public (TeleportAnimSnapshot Snapshot, IReadOnlyList<TeleportAnimEvent> Events)
|
||||
Tick(float deltaSeconds, bool worldReady)
|
||||
{
|
||||
var (snapshot, events) = _animation.Tick(
|
||||
deltaSeconds,
|
||||
worldReady,
|
||||
CurrentTunnelFrame);
|
||||
_viewPlane.Update(snapshot);
|
||||
return (snapshot, events);
|
||||
}
|
||||
|
||||
public void TickTunnel(float deltaSeconds)
|
||||
{
|
||||
}
|
||||
|
||||
public void EnterTunnel()
|
||||
{
|
||||
}
|
||||
|
||||
public void ExitTunnel()
|
||||
{
|
||||
}
|
||||
|
||||
public void SetWaitCue(bool visible)
|
||||
{
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_animation.Reset();
|
||||
_viewPlane.Reset();
|
||||
}
|
||||
|
||||
public Matrix4x4 ApplyViewPlane(Matrix4x4 projection) =>
|
||||
_viewPlane.Apply(projection);
|
||||
|
||||
public ICamera ApplyViewPlane(ICamera camera) => _viewPlane.ApplyTo(camera);
|
||||
|
||||
public void DrawPortalViewport(int width, int height, Matrix4x4 projection)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,24 @@ internal sealed unsafe class VulkanGpuPassEncoder : IGpuPassEncoder
|
|||
// draw, so the full-attachment default is the only safe starting point.
|
||||
SetViewport(0, 0, (int)attachmentWidth, (int)attachmentHeight);
|
||||
SetScissor(0, 0, (int)attachmentWidth, (int)attachmentHeight);
|
||||
|
||||
// Campaign V slice V6h: and for the same reason, the descriptor sets.
|
||||
//
|
||||
// Before this, sets 0/1/2 were bound only as a side effect of
|
||||
// BindStorageBuffer/BindUniformBuffer, so a pass whose pipeline reads the
|
||||
// texture table but binds no buffer — every retained-UI and debug-line
|
||||
// pass, because their per-draw data travels in push constants and a
|
||||
// vertex buffer — issued vkCmdDraw with set 2 unbound. That is
|
||||
// VUID-vkCmdDraw-None-08600 and, on the RX 9070 XT, an immediate
|
||||
// ErrorDeviceLost at submit.
|
||||
//
|
||||
// It went unseen through V6c–V6g because the bring-up host always drew
|
||||
// VulkanRhiScene first: its storage binds left all three sets bound in
|
||||
// the same command buffer, so the UI pass that followed inherited them.
|
||||
// The composition host has no 3-D scene, so its UI pass is the first
|
||||
// thing in the buffer and inherits nothing. Binding here makes a pass
|
||||
// self-contained rather than dependent on what preceded it in the frame.
|
||||
_bindings.Bind(_commands, _device);
|
||||
}
|
||||
|
||||
public GpuPassDescription Pass { get; }
|
||||
|
|
|
|||
507
src/AcDream.App/Rendering/Gpu/Vk/VulkanGraphicsContext.cs
Normal file
507
src/AcDream.App/Rendering/Gpu/Vk/VulkanGraphicsContext.cs
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
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 V6h: everything a Vulkan-backed host owns between a window
|
||||
/// and the RHI — instance, surface, physical-device choice, logical device,
|
||||
/// queues, swapchain, the capability gate, and <see cref="VulkanGpuDevice"/>.
|
||||
///
|
||||
/// <para>Extracted verbatim from <c>VulkanBringUpHost</c>, which was a second
|
||||
/// <c>main()</c>: the same sequence now serves the real composition host, and the
|
||||
/// bring-up harness consumes it too, so the acquisition order that was proven at
|
||||
/// V5/V6c is executed by exactly one piece of code rather than two.</para>
|
||||
///
|
||||
/// <para><b>The swapchain deliberately stays here rather than inside
|
||||
/// <see cref="VulkanGpuDevice"/></b>: format, extent, present-mode and the
|
||||
/// OUT_OF_DATE/SUBOPTIMAL policy are slice V5's pure, unit-tested decisions, and
|
||||
/// the RHI contract has nothing to say about presentation. The device borrows the
|
||||
/// swapchain through <see cref="IVulkanBackbuffer"/>.</para>
|
||||
/// </summary>
|
||||
internal sealed unsafe class VulkanGraphicsContext : IDisposable
|
||||
{
|
||||
private const ulong AcquireTimeoutNanoseconds = 1_000_000_000ul;
|
||||
|
||||
private readonly IWindow _window;
|
||||
private readonly RuntimeOptions _options;
|
||||
private readonly GraphicalHostPlatformServices _platform;
|
||||
private readonly FramePacingPolicy _pacing;
|
||||
private readonly Action<string> _log;
|
||||
|
||||
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 VulkanGpuDevice? _gpuDevice;
|
||||
private VulkanDebugNames _debugNames = VulkanDebugNames.Disabled;
|
||||
private VulkanDeviceFeatureSupport? _features;
|
||||
private VulkanDeviceLimitSupport? _limits;
|
||||
private VulkanFormatSupport? _formats;
|
||||
private IReadOnlyList<string> _instanceExtensions = [];
|
||||
private bool _recreateAtFrameBoundary;
|
||||
private bool _disposed;
|
||||
|
||||
private VulkanGraphicsContext(
|
||||
IWindow window,
|
||||
RuntimeOptions options,
|
||||
GraphicalHostPlatformServices platform,
|
||||
FramePacingPolicy pacing,
|
||||
Action<string> log)
|
||||
{
|
||||
_window = window ?? throw new ArgumentNullException(nameof(window));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_platform = platform ?? throw new ArgumentNullException(nameof(platform));
|
||||
_pacing = pacing;
|
||||
_log = log ?? throw new ArgumentNullException(nameof(log));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the whole stack against an already-initialised Vulkan window.
|
||||
/// Throws <see cref="NotSupportedException"/> when the capability gate
|
||||
/// rejects the device, which <c>Program.cs</c> turns into exit code 4 exactly
|
||||
/// as it does for GL. Any failure leaves zero Vulkan objects behind.
|
||||
/// </summary>
|
||||
/// <param name="requestedSampleCount">
|
||||
/// MSAA samples asked for by the quality preset. Clamped to what the device
|
||||
/// reports; 1 disables the multisampled scratch image entirely.
|
||||
/// </param>
|
||||
internal static VulkanGraphicsContext Acquire(
|
||||
IWindow window,
|
||||
RuntimeOptions options,
|
||||
GraphicalHostPlatformServices platform,
|
||||
FramePacingPolicy pacing,
|
||||
int requestedSampleCount,
|
||||
Action<string>? log = null)
|
||||
{
|
||||
var context = new VulkanGraphicsContext(
|
||||
window,
|
||||
options,
|
||||
platform,
|
||||
pacing,
|
||||
log ?? Console.WriteLine);
|
||||
try
|
||||
{
|
||||
context.CreateInstanceAndSurface();
|
||||
context.SelectDeviceAndGate();
|
||||
context.CreateDevice(requestedSampleCount);
|
||||
return context;
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The record the gate evaluated. Non-null once <see cref="Acquire"/> returns.</summary>
|
||||
internal VulkanCapabilityRecord? Capabilities { get; private set; }
|
||||
|
||||
/// <summary>The RHI device every renderer is constructed against.</summary>
|
||||
internal VulkanGpuDevice Device =>
|
||||
_gpuDevice ?? throw new InvalidOperationException(
|
||||
"The Vulkan RHI device has not been created.");
|
||||
|
||||
/// <summary>Samples the backbuffer pass renders with. 1 when MSAA is off or unsupported.</summary>
|
||||
internal int SampleCount { get; private set; } = 1;
|
||||
|
||||
internal uint Width => _swapchain?.Configuration?.Width ?? 0u;
|
||||
|
||||
internal uint Height => _swapchain?.Configuration?.Height ?? 0u;
|
||||
|
||||
/// <summary>Where the committed SPIR-V lives beside the binary.</summary>
|
||||
internal static string ShaderSpirvDirectory() =>
|
||||
Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders", "spv");
|
||||
|
||||
/// <summary>
|
||||
/// Brings the swapchain up to date with the current framebuffer size before a
|
||||
/// frame is opened. Returns false when the window is minimised — there is no
|
||||
/// zero-area swapchain to create, so the caller skips the frame.
|
||||
/// </summary>
|
||||
internal bool PrepareFrame()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_recreateAtFrameBoundary && _swapchain!.IsCreated)
|
||||
return true;
|
||||
|
||||
if (!RecreateSwapchain())
|
||||
return false;
|
||||
|
||||
VulkanSwapchainConfiguration resized = _swapchain!.Configuration!;
|
||||
Device.ConfigureBackbufferAttachments(
|
||||
resized.Width,
|
||||
resized.Height,
|
||||
resized.ImageFormat,
|
||||
SampleCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the outcome of the frame the caller just closed. A failed present
|
||||
/// (OUT_OF_DATE/SUBOPTIMAL) arms recreation at the next frame boundary rather
|
||||
/// than mid-frame, which is the only point at which it is safe.
|
||||
/// </summary>
|
||||
internal void NoteFrameClosed()
|
||||
{
|
||||
if (_gpuDevice is not null && !_gpuDevice.PresentSucceeded)
|
||||
_recreateAtFrameBoundary = true;
|
||||
}
|
||||
|
||||
/// <summary>Arms recreation, used when the acquire itself reported out-of-date.</summary>
|
||||
internal void RequestRecreate() => _recreateAtFrameBoundary = true;
|
||||
|
||||
private void CreateInstanceAndSurface()
|
||||
{
|
||||
_vk = Silk.NET.Vulkan.Vk.GetApi();
|
||||
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.");
|
||||
}
|
||||
|
||||
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 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 void CreateDevice(int requestedSampleCount)
|
||||
{
|
||||
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));
|
||||
|
||||
SampleCount = (int)Math.Min(
|
||||
(uint)Math.Max(1, requestedSampleCount),
|
||||
Math.Max(1u, _gpuDevice.Capabilities.MaxSampleCount));
|
||||
_gpuDevice.ConfigureBackbufferAttachments(
|
||||
configuration.Width,
|
||||
configuration.Height,
|
||||
configuration.ImageFormat,
|
||||
SampleCount);
|
||||
|
||||
_log(
|
||||
$"vulkan: RHI backend up — {_gpuDevice.Allocator.Describe()}, " +
|
||||
$"{SampleCount}x MSAA, pipeline cache " +
|
||||
(_gpuDevice.PipelineCacheLoadedFromDisk ? "reused" : "cold") +
|
||||
$", debug names {(_debugNames.IsEnabled ? "on" : "off")}");
|
||||
}
|
||||
|
||||
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>
|
||||
/// Adapts the 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>
|
||||
/// Teardown in strict reverse-construction order. Every handle is checked
|
||||
/// before destruction because acquisition 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.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
Silk.NET.Vulkan.Vk? vk = _vk;
|
||||
if (vk is not null && _device.Handle != 0)
|
||||
vk.DeviceWaitIdle(_device);
|
||||
|
||||
_gpuDevice?.Dispose();
|
||||
_gpuDevice = null;
|
||||
_swapchain?.Dispose();
|
||||
_swapchain = null;
|
||||
|
||||
if (vk is not null && _device.Handle != 0)
|
||||
{
|
||||
vk.DestroyDevice(_device, null);
|
||||
_device = default;
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue