feat(render): Campaign V slice V6c - SPIR-V, pipelines, passes, and a Vulkan frame that draws
The last of V6's three commits, and the one that makes the backend render. Plan sections: 4.5 (pipelines and the persisted cache), 4.6 (shaders and the committed .spv), 4.7 and 3.3 (clip space, the Y flip and winding), 4.9 and 4.10 (swapchain format and the scissor convention), 4.11 (the probe shader V5 deferred), 5.4 (Target: null means the swapchain image, literally). WHAT RUNS. ACDREAM_RENDER_BACKEND=vulkan now renders a real scene through the whole RHI on the RX 9070 XT: 60,000-plus frames per twelve-second run, 4x MSAA resolving into a B8G8R8A8_UNORM swapchain, GPU timer scopes resolving, a screenshot taken through IGpuDevice.CaptureBackbuffer, and a clean CloseMainWindow exit with the allocator reporting three device-memory objects. WHAT IT DRAWS, AND WHY IT IS NOT THE GAME. V6's milestone is "a full game frame on Vulkan" and on this branch that cannot be the game's own frame. V4c and V4d are parked by 5.5.5 so the world renderers are still raw GL; and the two renderers that DO speak the RHI - TextRenderer and DebugLineRenderer, ported at V4a - both throw for any device that is not a GlGpuDevice, because their loose uniforms and their classic texture-unit sprite binding have no home in the pinned contract yet. Converting them is a V4-class change with its own GL pixel gate, outside this slice's file list. So the backend is exercised through the contract by a scene of our own, and it is not a toy. It uses a device-local mesh arena filled through the staging ring, instance and batch data written straight into mapped ring memory, an offscreen render target whose colour is registered into the global texture table and sampled by a later pass, a BC1 texture with a CPU-built mip chain beside an uncompressed one with a vkCmdBlitImage chain, one multi-draw-indirect covering five quads with gl_DrawID selecting per-draw batch data, a second pipeline with line-list topology bound mid-pass, dynamic cull/front-face/depth-write, push constants, timer scopes, and an MSAA colour attachment resolving into the swapchain image. ORIENTATION, BY INSPECTION. Slice V5's screenshot was a uniform clear and its orientation was right "by construction" - which a uniform clear cannot show. The scene is therefore deliberately asymmetric in both axes: a quadrant card that is red top-left, green top-right, blue bottom-left and white bottom-right, four differently tinted markers at four different corners, and an open L of lines whose short stub rises at its right end. The captured PNG reads correctly in every one of those, including a miniature of the same card in the bottom-right whose own quadrants are also the right way up. The negative viewport height, the front-face inversion and the capture path agree. THE SHADER TOOLCHAIN, AND WHAT IT FOUND. tools/compile-shaders.ps1 drives tools/ShaderCompiler, a small out-of-solution .NET tool over Silk.NET.Shaderc - the same shaderc glslc is built on, through the already-pinned Silk 2.23.0 family. glslc is preferred when a Vulkan SDK is present and reported when it is; neither this machine nor CI has one, and requiring a 500 MB manual install between a contributor and a working checkout is not a reasonable price for a build step. The GLSL sources stay the single source of truth: the Vulkan dialect arrives as a preamble injected after the #version line - ACDREAM_UBO_SET becomes "set = 1,", the texture table becomes a set-2 descriptor array with a required nonuniformEXT accessor, and the shared 96-byte push block is declared with each loose uniform name defined onto its member. The only edits to a shader BODY are mechanical and dialect-level: dropping default-block uniform declarations, which Vulkan GLSL has no such thing as, and assigning explicit varying locations BY NAME across a pair, because ordinal assignment would look identical today and silently swap varyings the first time an author reordered a line. Run over the eight production pairs, exactly one thing happened: none of them compiled, and every failure is a specific source-level fact belonging to a renderer-port slice that has not landed. debug_line needs uView/uProjection converged into one uViewProjection - two matrices are 128 bytes and the shared block is 96. mesh_modern and particle still pass a uvec2 bindless handle as a varying, which is V4t's GpuTextureSlot retype. sky has ten loose uniforms and wants a UBO. ui_text needs uScreenSize/uUseTexture/uTex. particle_mesh needs uTextureIndex to become uTextureIndexA. terrain_modern needs V4d-1's matrix convergence. mesh is the legacy pair with no RHI consumer at all. That inventory is committed as shaders.manifest.json, with each source's SHA-256 and the compiler's own message, and a test re-hashes it so an edited shader that never got recompiled fails a build rather than shipping a stale binary. vk_probe is the pair that does compile, and it is the shader 4.11 already asked for: V5 recorded "build one real pipeline from the committed .spv" as its single deliberate deviation because no toolchain existed. It is Vulkan-dialect only and no GL renderer draws with it, so it forks nothing; it retires when the ported world renderers become the backend's own proof. DESCRIPTORS. Sets 0 and 1 are DYNAMIC buffer descriptors bound per flight slot, so a per-draw range change costs a dynamic offset in vkCmdBindDescriptorSets rather than a vkUpdateDescriptorSets in the hot path - which is what keeps 4.4's zero-writes-per-frame property true for buffers as well as for textures. Ten dynamic storage descriptors is above Vulkan's guaranteed minimum of four, so it is a real requirement rather than a free choice, it fails loudly at layout creation on a device that cannot serve it, and V9's lavapipe row must confirm it. Unused bindings point at a shared dummy range so there is ONE set layout and one pipeline layout; that is why binding a second pipeline mid-pass costs nothing and disturbs neither the descriptors nor the push constants. THE ONE MAPPING FUNCTION. VulkanViewportMapping holds the whole coordinate reconciliation: negative viewport height, the front-face inversion that pairs with it, and - separately - the scissor flip, which the viewport sign does NOT perform. The V3 audit flagged that as a concrete V6 acceptance item and it is the subtle one: vkCmdSetScissor is always top-left-origin, NdcScissorRect emits GL bottom-left rectangles, and getting it wrong clips a doorway aperture from the wrong edge in a scene that has one. Clip space needs nothing, as 4.7 concluded: the cameras already build [0,1]-convention projections. CONTRACT GAP, RECORDED NOT PAPERED OVER. GpuPipelineDescription cannot name its colour-attachment format, and Vulkan bakes that into a pipeline. Offscreen targets therefore adopt the swapchain's B8G8R8A8_UNORM rather than a literal RGBA order - invisible above the API, because an image is sampled through its format's component mapping and the one CPU readback swizzles explicitly. The honest fix is a colour-format field added in a reviewed contract commit, exactly as GpuBlendMode.InverseAlpha and GpuVertexFormat.UByte4UInt were added when V4c and V4d met the same wall. It is documented at VulkanTextureFormatMapping.CanonicalColorAttachmentFormat. The pipeline cache is persisted to the cache directory and validated by its 32-byte header against this device's vendor, device and cache UUID before use. Drivers are required to ignore incompatible blobs, but "required to" is a poor foundation for something that runs before anything else in the process, and the check costs 32 bytes of comparison. Two consecutive launches report "cold" then "reused". Gates: Release build clean; App suite 4056 passed / 3 skipped (4037 at V6b plus 19 new); offline pixel gate PASS at a differing fraction of 5.15e-05 with a same-commit control immediately after it at 2.84e-05 - 29 and 16 pixels of 563,200, the same class of ambient variation the campaign's 15-23 band records, and roughly 19x under the 0.001 threshold on a commit that changes no GL code path. Validation layers could not be run: this machine has no Vulkan SDK, no HKLM\SOFTWARE\Khronos\Vulkan\ExplicitLayers key, no VK_LAYER_PATH and no VkLayer_khronos_validation.json anywhere on disk. Plan 7 already requires one validation-clean run at V7; it needs the SDK installed first and is reported rather than assumed here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9eae496301
commit
234fe91d3b
24 changed files with 3937 additions and 319 deletions
|
|
@ -66,14 +66,19 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
private VulkanQueueFamilyChoice? _families;
|
||||
private VulkanSwapchain? _swapchain;
|
||||
|
||||
private CommandPool[] _commandPools = [];
|
||||
private CommandBuffer[] _commandBuffers = [];
|
||||
private Semaphore[] _imageAcquired = [];
|
||||
private Semaphore _timeline;
|
||||
|
||||
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 VulkanRhiScene? _scene;
|
||||
private VulkanDebugNames _debugNames = VulkanDebugNames.Disabled;
|
||||
private VulkanDeviceFeatureSupport? _features;
|
||||
private VulkanDeviceLimitSupport? _limits;
|
||||
private VulkanFormatSupport? _formats;
|
||||
|
||||
internal VulkanBringUpHost(
|
||||
RuntimeOptions options,
|
||||
GraphicalHostPlatformServices platform,
|
||||
|
|
@ -253,6 +258,13 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
_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,
|
||||
|
|
@ -278,12 +290,9 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
created.EnabledExtensions,
|
||||
families.GraphicsFamily,
|
||||
families.PresentFamily,
|
||||
VulkanPhysicalDeviceInspector.ReadFeatures(vk, _physicalDevice),
|
||||
VulkanPhysicalDeviceInspector.ReadLimits(vk, _physicalDevice),
|
||||
VulkanPhysicalDeviceInspector.ReadFormats(
|
||||
vk,
|
||||
_physicalDevice,
|
||||
VulkanSwapchainConfigurationFactory.OffersUnormFormat(formats)),
|
||||
_features,
|
||||
_limits,
|
||||
_formats,
|
||||
surfaceSupport,
|
||||
probe,
|
||||
SupportFailures: []);
|
||||
|
|
@ -310,66 +319,6 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
_log($"vulkan: device selection — {choice.Reason}");
|
||||
}
|
||||
|
||||
private void CreateFrameResources()
|
||||
{
|
||||
Silk.NET.Vulkan.Vk vk = _vk!;
|
||||
VulkanQueueFamilyChoice families = _families!;
|
||||
|
||||
_commandPools = new CommandPool[FlightCount];
|
||||
_commandBuffers = new CommandBuffer[FlightCount];
|
||||
_imageAcquired = new Semaphore[FlightCount];
|
||||
for (int i = 0; i < FlightCount; i++)
|
||||
{
|
||||
var poolCreate = new CommandPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.CommandPoolCreateInfo,
|
||||
QueueFamilyIndex = families.GraphicsFamily,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.CreateCommandPool(_device, &poolCreate, null, out CommandPool pool),
|
||||
"vkCreateCommandPool (flight slot)");
|
||||
_commandPools[i] = pool;
|
||||
|
||||
var allocate = new CommandBufferAllocateInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferAllocateInfo,
|
||||
CommandPool = pool,
|
||||
Level = CommandBufferLevel.Primary,
|
||||
CommandBufferCount = 1,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.AllocateCommandBuffers(_device, &allocate, out CommandBuffer commands),
|
||||
"vkAllocateCommandBuffers (flight slot)");
|
||||
_commandBuffers[i] = commands;
|
||||
|
||||
var semaphoreCreate = new SemaphoreCreateInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreCreateInfo,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.CreateSemaphore(_device, &semaphoreCreate, null, out Semaphore acquired),
|
||||
"vkCreateSemaphore (image acquired)");
|
||||
_imageAcquired[i] = acquired;
|
||||
}
|
||||
|
||||
var timelineType = new SemaphoreTypeCreateInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreTypeCreateInfo,
|
||||
SemaphoreType = SemaphoreType.Timeline,
|
||||
InitialValue = 0,
|
||||
};
|
||||
var timelineCreate = new SemaphoreCreateInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreCreateInfo,
|
||||
PNext = &timelineType,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.CreateSemaphore(_device, &timelineCreate, null, out _timeline),
|
||||
"vkCreateSemaphore (frame timeline)");
|
||||
|
||||
RecreateSwapchain();
|
||||
}
|
||||
|
||||
private bool RecreateSwapchain()
|
||||
{
|
||||
Vector2D<int> framebuffer = _window!.FramebufferSize;
|
||||
|
|
@ -386,11 +335,118 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
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);
|
||||
|
||||
// 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")}");
|
||||
}
|
||||
|
||||
/// <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,
|
||||
/// and capture one screenshot once the scene has settled.
|
||||
/// </summary>
|
||||
private void Present()
|
||||
{
|
||||
IWindow window = _window!;
|
||||
VulkanGpuDevice device = _gpuDevice!;
|
||||
VulkanRhiScene scene = _scene!;
|
||||
FrameScreenshotController? screenshots = CreateScreenshotController();
|
||||
bool screenshotRequested = false;
|
||||
DateTimeOffset started = DateTimeOffset.UtcNow;
|
||||
|
||||
while (!window.IsClosing)
|
||||
{
|
||||
|
|
@ -407,21 +463,44 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
Thread.Sleep(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
VulkanSwapchainConfiguration resized = _swapchain!.Configuration!;
|
||||
device.ConfigureBackbufferAttachments(
|
||||
resized.Width,
|
||||
resized.Height,
|
||||
resized.ImageFormat,
|
||||
scene.SampleCount);
|
||||
}
|
||||
|
||||
if (!RenderClearFrame(out uint imageIndex))
|
||||
if (!device.TryBeginFrame(out IGpuFrame? frame) || frame is null)
|
||||
{
|
||||
_recreateAtFrameBoundary = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (screenshots is not null && !screenshotRequested)
|
||||
VulkanSwapchainConfiguration configuration = _swapchain!.Configuration!;
|
||||
using (frame)
|
||||
{
|
||||
scene.Render(
|
||||
frame,
|
||||
configuration.Width,
|
||||
configuration.Height,
|
||||
(DateTimeOffset.UtcNow - started).TotalSeconds);
|
||||
}
|
||||
|
||||
_frameSerial = (ulong)frame.Serial;
|
||||
if (!device.PresentSucceeded)
|
||||
_recreateAtFrameBoundary = true;
|
||||
|
||||
// Capture after a few frames so the timer pool has resolved and the
|
||||
// ring has cycled through both flight slots at least once.
|
||||
if (screenshots is not null && !screenshotRequested && _frameSerial >= 4)
|
||||
{
|
||||
screenshotRequested = true;
|
||||
if (screenshots.TryRequest(ScreenshotName, out string error))
|
||||
{
|
||||
VulkanSwapchainConfiguration configuration = _swapchain!.Configuration!;
|
||||
_lastPresentedImage = imageIndex;
|
||||
screenshots.CapturePending(
|
||||
(int)configuration.Width,
|
||||
(int)configuration.Height);
|
||||
screenshots.CapturePending((int)configuration.Width, (int)configuration.Height);
|
||||
ReportTimings(device);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -431,10 +510,25 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
}
|
||||
|
||||
VulkanInterop.Check(_vk!.DeviceWaitIdle(_device), "vkDeviceWaitIdle (shutdown)");
|
||||
_log($"vulkan: presented {_frameSerial} clear-colour frame(s); shutting down.");
|
||||
_log($"vulkan: presented {_frameSerial} RHI frame(s); shutting down.");
|
||||
}
|
||||
|
||||
private uint _lastPresentedImage;
|
||||
private void ReportTimings(VulkanGpuDevice device)
|
||||
{
|
||||
if (!device.Timers.IsSupported)
|
||||
{
|
||||
_log("vulkan: GPU timestamps are unsupported on this device");
|
||||
return;
|
||||
}
|
||||
|
||||
string offscreen = device.Timers.TryResolve("offscreen", out double offscreenMs)
|
||||
? $"{offscreenMs:F3} ms"
|
||||
: "pending";
|
||||
string main = device.Timers.TryResolve("main", out double mainMs)
|
||||
? $"{mainMs:F3} ms"
|
||||
: "pending";
|
||||
_log($"vulkan: GPU timer scopes — offscreen {offscreen}, main {main}");
|
||||
}
|
||||
|
||||
private FrameScreenshotController? CreateScreenshotController()
|
||||
{
|
||||
|
|
@ -442,205 +536,20 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
return null;
|
||||
|
||||
return new FrameScreenshotController(
|
||||
(_, _) => _swapchain!.CaptureImage(
|
||||
_graphicsQueue,
|
||||
_families!.GraphicsFamily,
|
||||
_lastPresentedImage),
|
||||
// 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.
|
||||
(width, height) => FrameScreenshotController.FlipRows(
|
||||
_gpuDevice!.CaptureBackbuffer(width, height),
|
||||
width,
|
||||
height),
|
||||
_options.AutomationArtifactDirectory,
|
||||
_log);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One clear-colour frame: wait the timeline back to the flight window,
|
||||
/// acquire, record two barriers around a dynamic-rendering clear, submit, and
|
||||
/// present. This is the §4.8 frame skeleton with everything between the
|
||||
/// barriers removed.
|
||||
/// </summary>
|
||||
private bool RenderClearFrame(out uint imageIndex)
|
||||
{
|
||||
imageIndex = 0;
|
||||
Silk.NET.Vulkan.Vk vk = _vk!;
|
||||
VulkanSwapchain swapchain = _swapchain!;
|
||||
|
||||
ulong signalValue = _frameSerial + 1;
|
||||
if (signalValue > FlightCount)
|
||||
{
|
||||
ulong waitValue = signalValue - FlightCount;
|
||||
Semaphore timeline = _timeline;
|
||||
var wait = new SemaphoreWaitInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreWaitInfo,
|
||||
SemaphoreCount = 1,
|
||||
PSemaphores = &timeline,
|
||||
PValues = &waitValue,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.WaitSemaphores(_device, &wait, ulong.MaxValue),
|
||||
"vkWaitSemaphores (frame flight)");
|
||||
}
|
||||
|
||||
int slot = (int)((signalValue - 1) % FlightCount);
|
||||
VulkanSwapchainAction acquired = swapchain.TryAcquire(
|
||||
_imageAcquired[slot],
|
||||
AcquireTimeoutNanoseconds,
|
||||
out imageIndex);
|
||||
switch (acquired)
|
||||
{
|
||||
case VulkanSwapchainAction.RecreateNow:
|
||||
RecreateSwapchain();
|
||||
return false;
|
||||
case VulkanSwapchainAction.Idle:
|
||||
return false;
|
||||
case VulkanSwapchainAction.Fail:
|
||||
throw new InvalidOperationException(
|
||||
"vkAcquireNextImageKHR returned an unrecoverable result.");
|
||||
case VulkanSwapchainAction.RecreateAtFrameBoundary:
|
||||
_recreateAtFrameBoundary = true;
|
||||
break;
|
||||
}
|
||||
|
||||
VulkanInterop.Check(
|
||||
vk.ResetCommandPool(_device, _commandPools[slot], 0),
|
||||
"vkResetCommandPool");
|
||||
RecordClear(_commandBuffers[slot], swapchain, imageIndex);
|
||||
|
||||
var commandSubmit = new CommandBufferSubmitInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferSubmitInfo,
|
||||
CommandBuffer = _commandBuffers[slot],
|
||||
};
|
||||
var waitSemaphore = new SemaphoreSubmitInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreSubmitInfo,
|
||||
Semaphore = _imageAcquired[slot],
|
||||
StageMask = PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
};
|
||||
SemaphoreSubmitInfo* signals = stackalloc SemaphoreSubmitInfo[2];
|
||||
signals[0] = new SemaphoreSubmitInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreSubmitInfo,
|
||||
Semaphore = swapchain.RenderCompleteAt(imageIndex),
|
||||
StageMask = PipelineStageFlags2.AllCommandsBit,
|
||||
};
|
||||
signals[1] = new SemaphoreSubmitInfo
|
||||
{
|
||||
SType = StructureType.SemaphoreSubmitInfo,
|
||||
Semaphore = _timeline,
|
||||
Value = signalValue,
|
||||
StageMask = PipelineStageFlags2.AllCommandsBit,
|
||||
};
|
||||
var submit = new SubmitInfo2
|
||||
{
|
||||
SType = StructureType.SubmitInfo2,
|
||||
WaitSemaphoreInfoCount = 1,
|
||||
PWaitSemaphoreInfos = &waitSemaphore,
|
||||
CommandBufferInfoCount = 1,
|
||||
PCommandBufferInfos = &commandSubmit,
|
||||
SignalSemaphoreInfoCount = 2,
|
||||
PSignalSemaphoreInfos = signals,
|
||||
};
|
||||
VulkanInterop.Check(
|
||||
vk.QueueSubmit2(_graphicsQueue, 1, &submit, default),
|
||||
"vkQueueSubmit2 (clear frame)");
|
||||
_frameSerial = signalValue;
|
||||
|
||||
VulkanSwapchainAction presented = swapchain.Present(_presentQueue, imageIndex);
|
||||
switch (presented)
|
||||
{
|
||||
case VulkanSwapchainAction.RecreateNow:
|
||||
RecreateSwapchain();
|
||||
return false;
|
||||
case VulkanSwapchainAction.RecreateAtFrameBoundary:
|
||||
_recreateAtFrameBoundary = true;
|
||||
break;
|
||||
case VulkanSwapchainAction.Fail:
|
||||
throw new InvalidOperationException(
|
||||
"vkQueuePresentKHR returned an unrecoverable result.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RecordClear(
|
||||
CommandBuffer commands,
|
||||
VulkanSwapchain swapchain,
|
||||
uint imageIndex)
|
||||
{
|
||||
Silk.NET.Vulkan.Vk vk = _vk!;
|
||||
VulkanSwapchainConfiguration configuration = swapchain.Configuration!;
|
||||
|
||||
var begin = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
VulkanInterop.Check(vk.BeginCommandBuffer(commands, &begin), "vkBeginCommandBuffer");
|
||||
|
||||
var subresource = new ImageSubresourceRange
|
||||
{
|
||||
AspectMask = ImageAspectFlags.ColorBit,
|
||||
BaseMipLevel = 0,
|
||||
LevelCount = 1,
|
||||
BaseArrayLayer = 0,
|
||||
LayerCount = 1,
|
||||
};
|
||||
swapchain.TransitionImage(
|
||||
commands,
|
||||
swapchain.ImageAt(imageIndex),
|
||||
subresource,
|
||||
ImageLayout.Undefined,
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
PipelineStageFlags2.TopOfPipeBit,
|
||||
AccessFlags2.None,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit);
|
||||
|
||||
var clear = new ClearValue
|
||||
{
|
||||
Color = new ClearColorValue
|
||||
{
|
||||
Float32_0 = ClearColor[0],
|
||||
Float32_1 = ClearColor[1],
|
||||
Float32_2 = ClearColor[2],
|
||||
Float32_3 = ClearColor[3],
|
||||
},
|
||||
};
|
||||
var attachment = new RenderingAttachmentInfo
|
||||
{
|
||||
SType = StructureType.RenderingAttachmentInfo,
|
||||
ImageView = swapchain.ViewAt(imageIndex),
|
||||
ImageLayout = ImageLayout.ColorAttachmentOptimal,
|
||||
LoadOp = AttachmentLoadOp.Clear,
|
||||
StoreOp = AttachmentStoreOp.Store,
|
||||
ClearValue = clear,
|
||||
};
|
||||
var rendering = new RenderingInfo
|
||||
{
|
||||
SType = StructureType.RenderingInfo,
|
||||
RenderArea = new Rect2D(
|
||||
new Offset2D(0, 0),
|
||||
new Extent2D(configuration.Width, configuration.Height)),
|
||||
LayerCount = 1,
|
||||
ColorAttachmentCount = 1,
|
||||
PColorAttachments = &attachment,
|
||||
};
|
||||
vk.CmdBeginRendering(commands, &rendering);
|
||||
vk.CmdEndRendering(commands);
|
||||
|
||||
swapchain.TransitionImage(
|
||||
commands,
|
||||
swapchain.ImageAt(imageIndex),
|
||||
subresource,
|
||||
ImageLayout.ColorAttachmentOptimal,
|
||||
ImageLayout.PresentSrcKhr,
|
||||
PipelineStageFlags2.ColorAttachmentOutputBit,
|
||||
AccessFlags2.ColorAttachmentWriteBit,
|
||||
PipelineStageFlags2.BottomOfPipeBit,
|
||||
AccessFlags2.None);
|
||||
|
||||
VulkanInterop.Check(vk.EndCommandBuffer(commands), "vkEndCommandBuffer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Teardown in strict reverse-construction order. Every handle is checked
|
||||
/// before destruction because <see cref="Run"/> can throw at any stage — a
|
||||
|
|
@ -658,40 +567,33 @@ internal sealed unsafe class VulkanBringUpHost : IDisposable
|
|||
{
|
||||
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.
|
||||
_scene?.Dispose();
|
||||
_scene = null;
|
||||
_gpuDevice?.Dispose();
|
||||
_gpuDevice = null;
|
||||
|
||||
_swapchain?.Dispose();
|
||||
_swapchain = null;
|
||||
|
||||
if (_timeline.Handle != 0)
|
||||
{
|
||||
vk.DestroySemaphore(_device, _timeline, null);
|
||||
_timeline = default;
|
||||
}
|
||||
|
||||
foreach (Semaphore semaphore in _imageAcquired)
|
||||
{
|
||||
if (semaphore.Handle != 0)
|
||||
vk.DestroySemaphore(_device, semaphore, null);
|
||||
}
|
||||
|
||||
foreach (CommandPool pool in _commandPools)
|
||||
{
|
||||
if (pool.Handle != 0)
|
||||
vk.DestroyCommandPool(_device, pool, null);
|
||||
}
|
||||
|
||||
_imageAcquired = [];
|
||||
_commandBuffers = [];
|
||||
_commandPools = [];
|
||||
|
||||
vk.DestroyDevice(_device, null);
|
||||
_device = default;
|
||||
}
|
||||
else
|
||||
{
|
||||
_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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue