fix(window): size the swapchain for its present mode, and key frames in flight by frame instead of image
Window - CreateSwapchain asked for exactly numFrames (3) images whatever the surface said, read them into a three-element array, and keyed the command buffers, the fences and the descriptor-heap copy holding "its" swapchain image by the acquired image index. A swapchain of another size fell back to FIFO (asked for MAILBOX or IMMEDIATE) or threw, and a surface whose minimum is above three got a create info below it (#160). Mesa's MAILBOX needs four images: Turnip on phoc, llvmpipe on any Wayland compositor. - The present mode is picked first, then the swapchain is sized by SwapchainCapabilities: the surface's counts for that mode where the instance has surface_maintenance1. With swapchain_maintenance1 the swapchain names its mode (VkSwapchainPresentModesCreateInfoKHR), which is what makes the mode's own count the one that applies, at creation and to the forward progress of a UINT64_MAX acquire; without it the stricter of the mode's and the surface's counts for all modes. It asks for numFrames or that minimum where more, never past the maximum (SwapchainMinImageCount). Turnip's FIFO keeps its three images and MAILBOX gets four. The FIFO fallback and presentModeUnfit are gone. - images, imageViewInfos (was imageViews: they are create-infos), the made imageViews that descriptor sets take, imageInitialised and the render-finished semaphores are per image, as many as the driver made. The views are made once per swapchain. The semaphores only grow, since a present of the old swapchain may still wait on one. - Frames in flight stay numFrames, keyed by currentFrame, frameCounter % numFrames: the command buffer, the fence, the acquire semaphore, the heap copy (or set), RenderingElement3D::tlases, the frameIdx a pass is recorded with. currentImage is the acquired image. currentBuffer is gone, so code that used it stops compiling instead of picking the wrong one; RenderPass::Record's frameIdx keeps its name and is now the frame slot, as RenderPass.cppm says. - The frame's fence is waited before the acquire. The acquire semaphores need no spare then, and a frame waits for exactly the one numFrames before it, which the deletion queue and the staging rings count on and fences keyed by image did not promise once images come back out of order. The swapchain slots are written before the fence is reset. - From the acquire to the present the window is recording: a Resize or SetPresentMode from a listener (a VSync key ticked in onUpdate) takes effect after the present, through RebuildSwapchain, together with a present that found the swapchain out of date. Rebuilding mid-frame used to destroy the image the frame was drawing into; with images sized per swapchain it would also index past the new one's end. DescriptorHeapVulkan - Frames and images pair up differently from frame to frame, so no heap copy can keep one swapchain image. AddSwapchainSlot(slot, window) lists an image slot as that window's swapchain image, and Window::Render writes the image a frame acquired into its window's listed slots in that frame's copy, or its set, before onUpdate, skipping a slot that holds it already. A slot is one window's, so two windows can share a heap without writing each other's slots under each other's frames in flight. A slot listed during a frame its window records is written at once. A slot past the heap's image slots, an empty ImageSlot's 0xFFFF among them, is refused. Freeing a slot unlists it, as does initialising the heap in either binding model. WriteImageSet, the one use of which this replaces, is gone, and with it the per-frame dimension of setImageViews. Device - VK_KHR_surface_maintenance1 (or EXT) and VK_KHR_get_surface_capabilities2 are enabled where the loader has them (surfaceMaintenance1Supported, vkGetPhysicalDeviceSurfaceCapabilities2KHR), and the matching swapchain_maintenance1 with its feature where the device has it (swapchainMaintenance1Supported). - DistinctGpus groups the physical devices by GPU as CRAFTER_GRAPHICS_DEVICE numbers them, for ChooseGpu and tests. UI - UIRenderer lists its output slot instead of writing every copy itself, and again on every resize; the resize listener is gone on both backends. The headers' frameIdx is currentFrame. Examples - VulkanTriangle, Sponza, SponzaBench and RTVolume list their output slot and write only the per-frame TLASes. They wrote imageViews[f] into copy f once, which a resize left pointing at destroyed images. Tests - SwapchainImageCount: SwapchainMinImageCount on the numbers drivers report; every offered mode keeps its mode and gets what the window asks for; every frame presents the colour it drew, read back a texel per frame; a switch and a resize asked for during a frame wait for its present; a slot listed during a frame holds its image at once; two windows on one heap draw their own colours. The frame checks run again in child processes, through CRAFTER_GRAPHICS_DEVICE, on a GPU whose swapchains have more images than frames (this one, else llvmpipe), in each binding model the parent didn't cover; a child skips only for a GPU that can't run the library, and checks it runs the model it was asked for. - FrameLoopSync checks the frame slot follows the frame counter and counts the swapchain's own images. HeapASRead, HeapDynamicIndexHit and RTPassPushData list their swapchain slot and drop their resize rewrites. Docs - README, AGENTS.md and AGENTS.consumer.md: frames and images, the swapchain slots, rebuilds held to the present, the new optional groups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6f471ca6b1
commit
234fcce099
29 changed files with 1378 additions and 550 deletions
|
|
@ -29,16 +29,16 @@ window.StartSync(); // the event loop
|
||||||
|
|
||||||
- Include `vulkan/vulkan.h` yourself, before the imports (the examples wrap it in `#ifndef CRAFTER_GRAPHICS_WINDOW_DOM`): the library includes it only in its own global module fragments, and neither Vulkan's types nor its macros (`VK_TRUE`, the `VK_*` flags) cross an `import`. On the DOM build `Device::Initialize()` takes no arguments and `DeviceFeatures` does not exist, which is why the dual-target examples keep a separate DOM `main`.
|
- Include `vulkan/vulkan.h` yourself, before the imports (the examples wrap it in `#ifndef CRAFTER_GRAPHICS_WINDOW_DOM`): the library includes it only in its own global module fragments, and neither Vulkan's types nor its macros (`VK_TRUE`, the `VK_*` flags) cross an `import`. On the DOM build `Device::Initialize()` takes no arguments and `DeviceFeatures` does not exist, which is why the dual-target examples keep a separate DOM `main`.
|
||||||
- `StartSync` runs the platform loop until `open` is false (`onClose` fires first). Each pass of the loop dispatches platform events into the `Window` events, runs `Gamepad::Tick()` and `onBeforeUpdate`, and runs `Update()` for a window that is due. On Wayland a window on the compositor's clock updates from its frame callback during that event dispatch, before the pass's `onBeforeUpdate`, and the loop wakes for every event, so `onBeforeUpdate` fires several times per frame; on Win32 and in the browser `onBeforeUpdate` precedes the same pass's `Update`. `updateInterval`, set before `StartUpdate`, makes a window update every so often whether visible or not; pair it with `SetPresentMode(PresentMode::Mailbox)` so a present never waits for the display. `AddPollFd(fd, callback)` (Wayland) wakes the loop for a socket or an eventfd.
|
- `StartSync` runs the platform loop until `open` is false (`onClose` fires first). Each pass of the loop dispatches platform events into the `Window` events, runs `Gamepad::Tick()` and `onBeforeUpdate`, and runs `Update()` for a window that is due. On Wayland a window on the compositor's clock updates from its frame callback during that event dispatch, before the pass's `onBeforeUpdate`, and the loop wakes for every event, so `onBeforeUpdate` fires several times per frame; on Win32 and in the browser `onBeforeUpdate` precedes the same pass's `Update`. `updateInterval`, set before `StartUpdate`, makes a window update every so often whether visible or not; pair it with `SetPresentMode(PresentMode::Mailbox)` so a present never waits for the display. `AddPollFd(fd, callback)` (Wayland) wakes the loop for a socket or an eventfd.
|
||||||
- `Update()` computes `mouseDelta` and calls `Render()`, which acquires a swapchain image, waits for that image's fence (three frames in flight, `Window::numFrames`), reclaims deferred deletions, begins `drawCmdBuffers[currentBuffer]`, binds the heap (or the descriptor set), fires `onUpdate(FrameTime)`, records every pass in `passes` in order with a barrier on the swapchain image between them, transitions the image to present, submits and presents. Per-frame GPU work of your own (a TLAS rebuild, a physics dispatch) is recorded from `onUpdate` into `window.drawCmdBuffers[window.currentBuffer]`; the heaps are bound by then and the passes come after. `onResize` fires after a swapchain rebuild; anything holding descriptors of `window.imageViews[]` rewrites them there.
|
- `Update()` computes `mouseDelta` and calls `Render()`, which waits for the fence of its frame slot (three frames in flight, `Window::numFrames`; the slot is `window.currentFrame`), acquires a swapchain image (`window.currentImage`; the swapchain has as many images as its present mode needs, not necessarily three), writes it into this window's swapchain slots of the heap, reclaims deferred deletions, begins `drawCmdBuffers[currentFrame]`, binds the heap (or the descriptor set), fires `onUpdate(FrameTime)`, records every pass in `passes` in order with a barrier on the swapchain image between them, transitions the image to present, submits and presents. Per-frame GPU work of your own (a TLAS rebuild, a physics dispatch) is recorded from `onUpdate` into `window.drawCmdBuffers[window.currentFrame]`; the heaps are bound by then and the passes come after. A `Resize` or `SetPresentMode` from a listener of the frame (an input action ticked in `onUpdate`, say) takes effect after its present. `onResize` fires after a swapchain rebuild (a new size or present mode); the swapchain slots follow by themselves, and anything else that holds the old images or is sized after the window is made again there.
|
||||||
- A `RenderPass` is `Record(cmd, frameIdx, window)` plus `SwapchainStage()`, which must return the stage that touches the swapchain image (`COMPUTE_SHADER`, `RAY_TRACING_SHADER` or `TRANSFER`): the frame's barriers take their stage and access masks from it. The image stays in `VK_IMAGE_LAYOUT_GENERAL` through the whole pass list, with `STORAGE | TRANSFER_SRC` usage and `TRANSFER_DST` where the surface allows it (`window.swapchainUsage`). Nothing clears it: draw a background quad, or clear from a transfer pass where `TRANSFER_DST` is there.
|
- A `RenderPass` is `Record(cmd, frameIdx, window)` plus `SwapchainStage()`, which must return the stage that touches the swapchain image (`COMPUTE_SHADER`, `RAY_TRACING_SHADER` or `TRANSFER`): the frame's barriers take their stage and access masks from it. `frameIdx` is the frame slot, `window.currentFrame`, not the image: the frame's image is `window.images[window.currentImage]`. The image stays in `VK_IMAGE_LAYOUT_GENERAL` through the whole pass list, with `STORAGE | TRANSFER_SRC` usage and `TRANSFER_DST` where the surface allows it (`window.swapchainUsage`). Nothing clears it: draw a background quad, or clear from a transfer pass where `TRANSFER_DST` is there.
|
||||||
- `window.width` and `height` are physical pixels, as is `currentMousePos`. `CaptureNextFrame(path)` saves the next frame as a PNG. `SetTitle`, `SetAppId`, `SetCursorImage`, `SetDefaultCursor`; `SetTextInput(active, x, y, w, h, purpose)` tells the compositor's input method a text field has focus, which opens an on-screen keyboard on a phone.
|
- `window.width` and `height` are physical pixels, as is `currentMousePos`. `CaptureNextFrame(path)` saves the next frame as a PNG. `SetTitle`, `SetAppId`, `SetCursorImage`, `SetDefaultCursor`; `SetTextInput(active, x, y, w, h, purpose)` tells the compositor's input method a text field has focus, which opens an on-screen keyboard on a phone.
|
||||||
- Several windows can exist (on Wayland `Device::windows` lists them; the other backends keep no list), but they share one frame clock: one rendering window is the assumption behind the deletion queue and the staging rings.
|
- Several windows can exist (on Wayland `Device::windows` lists them; the other backends keep no list), and may share a heap, each writing only its own swapchain slots, but they share one frame clock: one rendering window is the assumption behind the deletion queue and the staging rings.
|
||||||
|
|
||||||
### Device and features
|
### Device and features
|
||||||
|
|
||||||
- `Device::Initialize(required = {}, optional = {})` creates the instance, weighs every GPU and creates the device. `DeviceFeatures` holds the five core feature structs, `vulkan10` to `vulkan14`; only their `VkBool32`s are read, so `{.vulkan12 = {.shaderFloat16 = VK_TRUE}}` is a complete argument. The library enables what its own shaders use and nothing more: a `uint16_t` spec constant needs `.vulkan10.shaderInt16`, fp16 arithmetic `.vulkan12.shaderFloat16`, 16-bit storage `.vulkan11.storageBuffer16BitAccess`, scalar layout `.vulkan12.scalarBlockLayout`. A GPU lacking a required feature is skipped; when none is left the call throws naming each GPU's gaps, and may be called again asking for less. Optional features are enabled where the GPU has them; `Device::enabledFeatures` says which, `supportedFeatures` what the GPU has, `apiVersion` the version in use (`vulkan14` features need a 1.4 GPU; `maintenance5` is always on). `CRAFTER_GRAPHICS_DEVICE=<n>` picks GPU `n`.
|
- `Device::Initialize(required = {}, optional = {})` creates the instance, weighs every GPU and creates the device. `DeviceFeatures` holds the five core feature structs, `vulkan10` to `vulkan14`; only their `VkBool32`s are read, so `{.vulkan12 = {.shaderFloat16 = VK_TRUE}}` is a complete argument. The library enables what its own shaders use and nothing more: a `uint16_t` spec constant needs `.vulkan10.shaderInt16`, fp16 arithmetic `.vulkan12.shaderFloat16`, 16-bit storage `.vulkan11.storageBuffer16BitAccess`, scalar layout `.vulkan12.scalarBlockLayout`. A GPU lacking a required feature is skipped; when none is left the call throws naming each GPU's gaps, and may be called again asking for less. Optional features are enabled where the GPU has them; `Device::enabledFeatures` says which, `supportedFeatures` what the GPU has, `apiVersion` the version in use (`vulkan14` features need a 1.4 GPU; `maintenance5` is always on). `CRAFTER_GRAPHICS_DEVICE=<n>` picks GPU `n`.
|
||||||
- `VulkanShader` checks every module's SPIR-V capabilities against what was enabled and throws naming the file and the field to pass (`vulkan10.shaderInt16`), or saying the GPU lacks it. Most drivers accept such a module silently, so this is the error you get here instead of a wrong frame on another machine.
|
- `VulkanShader` checks every module's SPIR-V capabilities against what was enabled and throws naming the file and the field to pass (`vulkan10.shaderInt16`), or saying the GPU lacks it. Most drivers accept such a module silently, so this is the error you get here instead of a wrong frame on another machine.
|
||||||
- Optional groups, decided once at `Initialize`: `Device::descriptorHeapSupported` (`VK_EXT_descriptor_heap`), `rayTracingSupported` (needs the heap too), `memoryDecompressionSupported` (`VK_EXT_memory_decompression`: `.cmesh` and `.ctex` decompress on the GPU; otherwise the library decodes them on the CPU), `deviceFaultSupported`. The RT entry points throw through `Device::RequireRayTracing` without ray tracing; the compute/UI path runs everywhere.
|
- Optional groups, decided once at `Initialize`: `Device::descriptorHeapSupported` (`VK_EXT_descriptor_heap`), `rayTracingSupported` (needs the heap too), `memoryDecompressionSupported` (`VK_EXT_memory_decompression`: `.cmesh` and `.ctex` decompress on the GPU; otherwise the library decodes them on the CPU), `deviceFaultSupported`, `swapchainMaintenance1Supported` (`VK_KHR_swapchain_maintenance1`, with the instance's `surface_maintenance1`, `surfaceMaintenance1Supported`: a swapchain names its present mode and gets that mode's own image count; otherwise the stricter of the mode's and the surface's). The RT entry points throw through `Device::RequireRayTracing` without ray tracing; the compute/UI path runs everywhere.
|
||||||
- `Device::device`, `queue`, `commandPool`, `physDevice`, `instance`, `memoryProperties`, `deviceProperties`, `rayTracingProperties` and `descriptorHeapProperties` are public statics for raw Vulkan. `Device::GetMemoryType(typeBits, required, preferred = 0)` and `PreferDirectDeviceWrite(bytes)` are the allocation helpers.
|
- `Device::device`, `queue`, `commandPool`, `physDevice`, `instance`, `memoryProperties`, `deviceProperties`, `rayTracingProperties` and `descriptorHeapProperties` are public statics for raw Vulkan. `Device::GetMemoryType(typeBits, required, preferred = 0)` and `PreferDirectDeviceWrite(bytes)` are the allocation helpers.
|
||||||
- `Device::pipelineCache` is shared by every pipeline and written to `Device::pipelineCachePath` (`pipeline_cache.bin` in the working directory) at exit. Set the path before `Initialize`, and gitignore the file.
|
- `Device::pipelineCache` is shared by every pipeline and written to `Device::pipelineCachePath` (`pipeline_cache.bin` in the working directory) at exit. Set the path before `Initialize`, and gitignore the file.
|
||||||
|
|
||||||
|
|
@ -55,10 +55,10 @@ Frames are pipelined three deep and nothing waits for the GPU at steady state, s
|
||||||
|
|
||||||
### The bindless heap
|
### The bindless heap
|
||||||
|
|
||||||
`DescriptorHeapVulkan::Initialize(images, buffers, samplers)` sizes one resource heap and one sampler heap per frame (`resourceHeap[Window::numFrames]`, `samplerHeap[...]`, mapped `VulkanBuffer`s) and the slot allocators. `AllocateImageSlots(n)`, `AllocateBufferSlots(n)` and `AllocateSamplerSlots(n)` return `{firstElement, count}` ranges (contiguous; freed runs are reused), `Free*Slots` returns them, and running out throws naming the pool and the counts. `Initialize` throws too when its buffer slots would need shader-visible indices past 16 bits.
|
`DescriptorHeapVulkan::Initialize(images, buffers, samplers)` sizes one resource heap and one sampler heap per frame in flight (`resourceHeap[Window::numFrames]`, `samplerHeap[...]`, mapped `VulkanBuffer`s) and the slot allocators. `AllocateImageSlots(n)`, `AllocateBufferSlots(n)` and `AllocateSamplerSlots(n)` return `{firstElement, count}` ranges (contiguous; freed runs are reused), `Free*Slots` returns them, and running out throws naming the pool and the counts. `Initialize` throws too when its buffer slots would need shader-visible indices past 16 bits.
|
||||||
|
|
||||||
- **Buffer slot indices are relative.** `AllocateBufferSlots` indexes the heap's buffer region, which starts after the image region, while a GLSL `descriptor_heap` SSBO array indexes buffer descriptors from heap byte 0. The shader-visible index is `heap.bufferStartElement + raw`. The RAII `BufferSlot` converts to exactly that (its `operator std::uint16_t()`), `UIRenderer::RegisterBuffer` returns one, and raw code passes `heap.bufferStartElement` as a spec constant: the RT examples declare `layout(constant_id = 0) const uint16_t bufferStart = 0us;` and index `topLevelAS[bufferStart]`, which is why they ask for `shaderInt16`. Image slots index the heap as they are, in image-descriptor units; samplers live in their own heap. A wrong buffer index reads zeros, which looks like every field of every item being zero. In descriptor-set mode `bufferStartElement` is 0, so code that goes through the conversion is right in both.
|
- **Buffer slot indices are relative.** `AllocateBufferSlots` indexes the heap's buffer region, which starts after the image region, while a GLSL `descriptor_heap` SSBO array indexes buffer descriptors from heap byte 0. The shader-visible index is `heap.bufferStartElement + raw`. The RAII `BufferSlot` converts to exactly that (its `operator std::uint16_t()`), `UIRenderer::RegisterBuffer` returns one, and raw code passes `heap.bufferStartElement` as a spec constant: the RT examples declare `layout(constant_id = 0) const uint16_t bufferStart = 0us;` and index `topLevelAS[bufferStart]`, which is why they ask for `shaderInt16`. Image slots index the heap as they are, in image-descriptor units; samplers live in their own heap. A wrong buffer index reads zeros, which looks like every field of every item being zero. In descriptor-set mode `bufferStartElement` is 0, so code that goes through the conversion is right in both.
|
||||||
- Writing a descriptor by hand: `Device::vkWriteResourceDescriptorsEXT` into `heap.resourceHeap[f].value + heap.ImageByteOffset(imageRange.firstElement)` or `heap.BufferByteOffset(bufferRange.firstElement)`, `Device::descriptorHeapProperties.imageDescriptorSize` or `bufferDescriptorSize` bytes long, for each frame `f`, then `heap.resourceHeap[f].FlushDevice()`. `BufferByteOffset` takes the raw index `AllocateBufferSlots` returned and adds the region start itself; handing it a `BufferSlot`, which converts to the absolute index, adds the start twice and puts the descriptor in the wrong place. `ImageByteOffset` takes an image slot as it is. Per-frame resources (the swapchain image `window.imageViews[f]`, `tlases[f]`) are written into frame `f`'s heap; a shared resource into every frame's. `examples/VulkanTriangle/main.cpp` is the pattern. The `UIRenderer::Register*` helpers do this for you in both binding models.
|
- Writing a descriptor by hand: `Device::vkWriteResourceDescriptorsEXT` into `heap.resourceHeap[f].value + heap.ImageByteOffset(imageRange.firstElement)` or `heap.BufferByteOffset(bufferRange.firstElement)`, `Device::descriptorHeapProperties.imageDescriptorSize` or `bufferDescriptorSize` bytes long, for each frame `f`, then `heap.resourceHeap[f].FlushDevice()`. `BufferByteOffset` takes the raw index `AllocateBufferSlots` returned and adds the region start itself; handing it a `BufferSlot`, which converts to the absolute index, adds the start twice and puts the descriptor in the wrong place. `ImageByteOffset` takes an image slot as it is. Per-frame resources (`tlases[f]`) are written into frame `f`'s heap; a shared resource into every frame's. The swapchain image is neither, since frames and images pair up differently from frame to frame: list its image slot once with `heap.AddSwapchainSlot(slot, window)`, and the window writes the image each frame draws into there, a storage image in `GENERAL`, in both binding models and across resizes (a slot listed during a frame is written for that frame). `examples/VulkanTriangle/main.cpp` is the pattern. The `UIRenderer::Register*` helpers do this for you in both binding models.
|
||||||
- Binding the heap outside the frame loop: `Window::Render` binds `window.descriptorHeap` in every frame it records. A command buffer it does not record that dispatches against the heap (`window.StartInit()`'s, say) calls `heap.Bind(cmd, frame)`, which binds that frame's heaps with their reserved ranges, or in descriptor-set mode its descriptor set. Don't build the `VkBindHeapInfoEXT` by hand: where the reserved range sits is the heap's to decide.
|
- Binding the heap outside the frame loop: `Window::Render` binds `window.descriptorHeap` in every frame it records. A command buffer it does not record that dispatches against the heap (`window.StartInit()`'s, say) calls `heap.Bind(cmd, frame)`, which binds that frame's heaps with their reserved ranges, or in descriptor-set mode its descriptor set. Don't build the `VkBindHeapInfoEXT` by hand: where the reserved range sits is the heap's to decide.
|
||||||
- **Descriptor-set fallback.** On a driver without `VK_EXT_descriptor_heap` (Mesa Turnip on Adreno) `Device::descriptorHeapSupported` is false and the same slots live in one bindless descriptor set per frame; slot handles, item structs, `UIDispatchHeader` and the `UIRenderer` API are unchanged, so application code does not branch. Ray tracing is unavailable there. `CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP` forces this path on a driver that has the heap, to test it. Raw `vkWriteResourceDescriptorsEXT` code is heap-only; a consumer that must run on such a driver stays on the `UIRenderer::Register*` helpers.
|
- **Descriptor-set fallback.** On a driver without `VK_EXT_descriptor_heap` (Mesa Turnip on Adreno) `Device::descriptorHeapSupported` is false and the same slots live in one bindless descriptor set per frame; slot handles, item structs, `UIDispatchHeader` and the `UIRenderer` API are unchanged, so application code does not branch. Ray tracing is unavailable there. `CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP` forces this path on a driver that has the heap, to test it. Raw `vkWriteResourceDescriptorsEXT` code is heap-only; a consumer that must run on such a driver stays on the `UIRenderer::Register*` helpers.
|
||||||
|
|
||||||
|
|
@ -119,9 +119,9 @@ Heap-only: check `Device::rayTracingSupported`, or expect `std::runtime_error` f
|
||||||
|
|
||||||
- Shaders: `VulkanShader shader("raygen.spv", "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR, &specInfo);` (`nullptr` for no specialisation); `ShaderBindingTableVulkan table; table.Init(shaders);`; `PipelineRTVulkan pipeline; pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, table, maxRecursionDepth = 1);` with spans of `VkRayTracingShaderGroupCreateInfoKHR` indexing the shader array. **`maxRecursionDepth` counts the raygen as 1**: a closest-hit that traces a shadow ray needs 2, and tracing past the pipeline's depth loses the device.
|
- Shaders: `VulkanShader shader("raygen.spv", "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR, &specInfo);` (`nullptr` for no specialisation); `ShaderBindingTableVulkan table; table.Init(shaders);`; `PipelineRTVulkan pipeline; pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, table, maxRecursionDepth = 1);` with spans of `VkRayTracingShaderGroupCreateInfoKHR` indexing the shader array. **`maxRecursionDepth` counts the raygen as 1**: a closest-hit that traces a shadow ray needs 2, and tracing past the pipeline's depth loses the device.
|
||||||
- Geometry: `Mesh mesh; mesh.Build(std::span<Vector<float, 3, 3>> vertices, std::span<std::uint32_t> indices, cmd, RTBuildOptions{});` builds a BLAS and sets `mesh.blasAddr`; `Build(const CompressedMeshAsset&, cmd)` builds from a `.cmesh`, decompressed on the GPU where supported; `BuildProcedural(aabbs, opaque, cmd)` builds from `RTAabb` boxes for a `VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR` with an intersection shader, and has an overload taking a device address a compute pass wrote. `RTBuildOptions` is `.preference` (`FastTrace`, `FastBuild`), `.allowUpdate` and `.noDuplicateAnyHit`; with `allowUpdate`, `Refit(vertices, indices, cmd)` and its dirty-range overload update in place and keep `blasAddr`. A rebuild into storage that still fits keeps `blasAddr` too; read it again after a rebuild that grows.
|
- Geometry: `Mesh mesh; mesh.Build(std::span<Vector<float, 3, 3>> vertices, std::span<std::uint32_t> indices, cmd, RTBuildOptions{});` builds a BLAS and sets `mesh.blasAddr`; `Build(const CompressedMeshAsset&, cmd)` builds from a `.cmesh`, decompressed on the GPU where supported; `BuildProcedural(aabbs, opaque, cmd)` builds from `RTAabb` boxes for a `VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR` with an intersection shader, and has an overload taking a device address a compute pass wrote. `RTBuildOptions` is `.preference` (`FastTrace`, `FastBuild`), `.allowUpdate` and `.noDuplicateAnyHit`; with `allowUpdate`, `Refit(vertices, indices, cmd)` and its dirty-range overload update in place and keep `blasAddr`. A rebuild into storage that still fits keeps `blasAddr` too; read it again after a rebuild that grows.
|
||||||
- Instances: a `RenderingElement3D` holds a `VkAccelerationStructureInstanceKHR instance` (`transform`, `mask`, `instanceCustomIndex`, `instanceShaderBindingTableRecordOffset`, `flags`, `accelerationStructureReference = mesh.blasAddr`; Crafter.Math's `MatrixRowMajor<float, 4, 3, 1>::Store(reinterpret_cast<float*>(instance.transform.matrix))` writes the transform), `userMetadata` (copied per instance id into `tlases[f].metadataBuffer`) and `transformOwnedByGpu`. `RenderingElement3D::Add(&e)` and `Remove(&e)` register it. `RenderingElement3D::BuildTLAS(cmd, frameIndex, preference = FastTrace)` builds or refits `tlases[frameIndex]` from every registered element: once per frame slot at setup (`0`, `1`, `2`), and per frame from `onUpdate` for `window.currentBuffer` when instances move. `e.MarkHostDataDirty()` after a change to an element's host fields lets the per-frame copy skip unchanged slots. `tlases[f].address` goes into a `VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR` descriptor of frame `f` (see "The bindless heap") and changes only when the TLAS outgrows its storage.
|
- Instances: a `RenderingElement3D` holds a `VkAccelerationStructureInstanceKHR instance` (`transform`, `mask`, `instanceCustomIndex`, `instanceShaderBindingTableRecordOffset`, `flags`, `accelerationStructureReference = mesh.blasAddr`; Crafter.Math's `MatrixRowMajor<float, 4, 3, 1>::Store(reinterpret_cast<float*>(instance.transform.matrix))` writes the transform), `userMetadata` (copied per instance id into `tlases[f].metadataBuffer`) and `transformOwnedByGpu`. `RenderingElement3D::Add(&e)` and `Remove(&e)` register it. `RenderingElement3D::BuildTLAS(cmd, frameIndex, preference = FastTrace)` builds or refits `tlases[frameIndex]` from every registered element: once per frame slot at setup (`0`, `1`, `2`), and per frame from `onUpdate` for `window.currentFrame` when instances move. `e.MarkHostDataDirty()` after a change to an element's host fields lets the per-frame copy skip unchanged slots. `tlases[f].address` goes into a `VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR` descriptor of frame `f` (see "The bindless heap") and changes only when the TLAS outgrows its storage.
|
||||||
- The pass: `RTPass rtPass(&pipeline); rtPass.pushPtr = &camera; rtPass.pushBytes = sizeof(camera);` pushes your data at offset 0 of the shaders' push block before every trace (the bytes are read at record time, so updating `camera` before the frame is enough); `width` and `height` trace at a size other than the window's. `window.passes.push_back(&rtPass);`.
|
- The pass: `RTPass rtPass(&pipeline); rtPass.pushPtr = &camera; rtPass.pushBytes = sizeof(camera);` pushes your data at offset 0 of the shaders' push block before every trace (the bytes are read at record time, so updating `camera` before the frame is enough); `width` and `height` trace at a size other than the window's. `window.passes.push_back(&rtPass);`.
|
||||||
- In GLSL, with the extensions the examples enable (`GL_EXT_ray_tracing`, `GL_EXT_descriptor_heap`, `GL_EXT_nonuniform_qualifier` for a non-constant heap index, `GL_EXT_shader_explicit_arithmetic_types_int16` for a `uint16_t` spec constant, and `GL_EXT_shader_image_load_formatted` or a `writeonly` image): `layout(descriptor_heap) uniform accelerationStructureEXT topLevelAS[];`, `layout(descriptor_heap) uniform writeonly image2D image[];`, indices from spec constants or push data. The output image is the swapchain image at its image slot, in `VK_IMAGE_LAYOUT_GENERAL`. Start from `examples/VulkanTriangle/raygen.glsl`.
|
- In GLSL, with the extensions the examples enable (`GL_EXT_ray_tracing`, `GL_EXT_descriptor_heap`, `GL_EXT_nonuniform_qualifier` for a non-constant heap index, `GL_EXT_shader_explicit_arithmetic_types_int16` for a `uint16_t` spec constant, and `GL_EXT_shader_image_load_formatted` or a `writeonly` image): `layout(descriptor_heap) uniform accelerationStructureEXT topLevelAS[];`, `layout(descriptor_heap) uniform writeonly image2D image[];`, indices from spec constants or push data. The output image is the swapchain image at an image slot listed with `AddSwapchainSlot`, in `VK_IMAGE_LAYOUT_GENERAL`. Start from `examples/VulkanTriangle/raygen.glsl`.
|
||||||
- **NVIDIA.** On the proprietary driver (still on `615.78.08`) reading an acceleration structure through the heap loses the device. The library works around it transparently: `VulkanShader` rewrites the SPIR-V so the heap AS read becomes a TLAS-address load from the push block plus `OpConvertUToAccelerationStructureKHR`, and `RTPass` and `ComputeShader` push the address. Nothing changes in your shaders or code, except that the rewrite appends an 8-byte member after your push block, and that it turns `shaderInt64` on for itself only: a shader of yours that declares `Int64` still has to ask for `shaderInt64`, or it runs on NVIDIA alone. `CRAFTER_GRAPHICS_NO_AS_WORKAROUND` disables the rewrite, to probe a new driver. Second note: on `610.43.02`, indexing a `descriptor_heap` array with a runtime value inside a hit shader faulted (fixed in `615.78.08`). The portable pattern, which the Sponza example uses, is one descriptor at a spec-constant slot and dynamic indexing inside it: all geometry in one SSBO indexed by element offset, materials in one `texture2DArray` indexed by layer. `examples/Sponza/README.md` has the GLSL.
|
- **NVIDIA.** On the proprietary driver (still on `615.78.08`) reading an acceleration structure through the heap loses the device. The library works around it transparently: `VulkanShader` rewrites the SPIR-V so the heap AS read becomes a TLAS-address load from the push block plus `OpConvertUToAccelerationStructureKHR`, and `RTPass` and `ComputeShader` push the address. Nothing changes in your shaders or code, except that the rewrite appends an 8-byte member after your push block, and that it turns `shaderInt64` on for itself only: a shader of yours that declares `Int64` still has to ask for `shaderInt64`, or it runs on NVIDIA alone. `CRAFTER_GRAPHICS_NO_AS_WORKAROUND` disables the rewrite, to probe a new driver. Second note: on `610.43.02`, indexing a `descriptor_heap` array with a runtime value inside a hit shader faulted (fixed in `615.78.08`). The portable pattern, which the Sponza example uses, is one descriptor at a spec-constant slot and dynamic indexing inside it: all geometry in one SSBO indexed by element offset, materials in one `texture2DArray` indexed by layer. `examples/Sponza/README.md` has the GLSL.
|
||||||
|
|
||||||
### Input
|
### Input
|
||||||
|
|
|
||||||
12
AGENTS.md
12
AGENTS.md
|
|
@ -31,7 +31,7 @@ Partitions, by what they are for. Every partition file exists on every target; o
|
||||||
|
|
||||||
crafter-build is the only build tool: the Flatpak (`alias crafter-build='flatpak run net.catcrafts.CrafterBuild'`; a locally built one runs with `--branch=local`) or a native build of the sibling checkout. `crafter-build --version` prints the compiler it resolved. Loading `project.cpp` clones Crafter.Event, Crafter.Math and Crafter.Asset into the crafter-build cache and, since they are unpinned, `git pull`s them on every invocation, and clones the two Khronos header repositories, so a build needs the network. `--local` takes the sibling checkouts (`../Crafter.Event`, `../Crafter.Math`, `../Crafter.Asset`) instead and hands the flag on, so Crafter.Asset picks the sibling Crafter.Math too; two copies of one module on the path do not link.
|
crafter-build is the only build tool: the Flatpak (`alias crafter-build='flatpak run net.catcrafts.CrafterBuild'`; a locally built one runs with `--branch=local`) or a native build of the sibling checkout. `crafter-build --version` prints the compiler it resolved. Loading `project.cpp` clones Crafter.Event, Crafter.Math and Crafter.Asset into the crafter-build cache and, since they are unpinned, `git pull`s them on every invocation, and clones the two Khronos header repositories, so a build needs the network. `--local` takes the sibling checkouts (`../Crafter.Event`, `../Crafter.Math`, `../Crafter.Asset`) instead and hands the flag on, so Crafter.Asset picks the sibling Crafter.Math too; two copies of one module on the path do not link.
|
||||||
|
|
||||||
The host is Linux on Wayland. The native Linux backend is Wayland only (no X11), so every test that calls `Device::Initialize` needs `WAYLAND_DISPLAY`: it connects to the display right after creating the Vulkan instance, window or no window. The build links `wayland-client`, `xkbcommon`, `udev` and the Vulkan loader from the host or the sysroot; libevdev is vendored and built as its own static library, `Crafter.Graphics-libevdev`, once per build. A Vulkan 1.3 driver is the floor; `VK_EXT_descriptor_heap` (with `shaderUntypedPointers`) and ray tracing are optional groups detected at start-up. Recent Mesa's llvmpipe advertises the heap, so a machine without a GPU runs the UI path. The Khronos validation layer is optional and off by default.
|
The host is Linux on Wayland. The native Linux backend is Wayland only (no X11), so every test that calls `Device::Initialize` needs `WAYLAND_DISPLAY`: it connects to the display right after creating the Vulkan instance, window or no window. The build links `wayland-client`, `xkbcommon`, `udev` and the Vulkan loader from the host or the sysroot; libevdev is vendored and built as its own static library, `Crafter.Graphics-libevdev`, once per build. A Vulkan 1.3 driver is the floor; `VK_EXT_descriptor_heap` (with `shaderUntypedPointers`), ray tracing and `swapchain_maintenance1` (with the instance's `surface_maintenance1`, for per-present-mode image counts) are optional groups detected at start-up. Recent Mesa's llvmpipe advertises the heap, so a machine without a GPU runs the UI path. The Khronos validation layer is optional and off by default.
|
||||||
|
|
||||||
Targets, picked with `--target=` and passed down to the dependencies:
|
Targets, picked with `--target=` and passed down to the dependencies:
|
||||||
|
|
||||||
|
|
@ -70,9 +70,9 @@ Tests are declared in `project.cpp`, not discovered, through the `LibraryTest(na
|
||||||
|
|
||||||
A test is an `int main()`: exit 0 passes, nonzero fails, 77 skips (for a missing capability, never for a failed check). Each runs in its own process, in parallel, with a 60 s default timeout, and the output of a failed, crashed or timed-out test is in `build/test-logs/<name>.log`; a later pass does not delete the log, so check its timestamp. What the tests need, in three groups:
|
A test is an `int main()`: exit 0 passes, nonzero fails, 77 skips (for a missing capability, never for a failed check). Each runs in its own process, in parallel, with a 60 s default timeout, and the output of a failed, crashed or timed-out test is in `build/test-logs/<name>.log`; a later pass does not delete the log, so check its timestamp. What the tests need, in three groups:
|
||||||
|
|
||||||
- Pure logic, no GPU: they stage `Device`'s statics or feed synthetic data to the decision functions that were factored out for them (`Device::ResolveFeatures`, `ChooseDevice`, `CheckShaderCapabilities`, `GetMemoryType`, `PreferDirectDeviceWrite`, `PipelineCacheDataCompatible`, `AlignMappedFlushRange`, `BuildMipChainFinalBarriers`, `SwapchainStageUnion`, `BuildTlasWriteBarrier`, `UIRenderer::ClipFlags`, `WorkaroundNvidiaAS::Patch`, `DescriptorHeapVulkan::ComputeLayout`, the font and input-field helpers). `PushConstantRewrite` and `UIFusedShader` compile GLSL with glslang and check the SPIR-V with spirv-val at runtime, and `ExampleShaders` compiles every GLSL shader under `examples/` the way crafter-build would, the descriptor-set build included, so a shader contract change that breaks an example fails here without building the example; `MouseScroll` and `PointerEnterLeave` drive the Wayland listener callbacks directly.
|
- Pure logic, no GPU: they stage `Device`'s statics or feed synthetic data to the decision functions that were factored out for them (`Device::ResolveFeatures`, `ChooseDevice`, `CheckShaderCapabilities`, `GetMemoryType`, `PreferDirectDeviceWrite`, `PipelineCacheDataCompatible`, `AlignMappedFlushRange`, `BuildMipChainFinalBarriers`, `SwapchainStageUnion`, `BuildTlasWriteBarrier`, `UIRenderer::ClipFlags`, `Window::SwapchainMinImageCount`, `WorkaroundNvidiaAS::Patch`, `DescriptorHeapVulkan::ComputeLayout`, the font and input-field helpers). `PushConstantRewrite` and `UIFusedShader` compile GLSL with glslang and check the SPIR-V with spirv-val at runtime, and `ExampleShaders` compiles every GLSL shader under `examples/` the way crafter-build would, the descriptor-set build included, so a shader contract change that breaks an example fails here without building the example; `MouseScroll` and `PointerEnterLeave` drive the Wayland listener callbacks directly.
|
||||||
- A headless device: `Device::Initialize()` and no `Window` (`HeapSlotReuse`, `DeferredDeletion`, `MeshLifetime`, `ImageStagingRelease`, `BLASBuildOptions`, the `TLAS*` tests, ...). Headless still means a Wayland display on Linux. Most of the RT ones check `Device::rayTracingSupported` and return 77 without it; `BLASBuildOptions`, `TLASInstanceDirtyTracking` and `MeshDecompressStagingRelease` do not, and crash on a GPU without ray tracing (`Device::RequireRayTracing` throws out of `main`). `DeviceSelection` skips with one GPU.
|
- A headless device: `Device::Initialize()` and no `Window` (`HeapSlotReuse`, `DeferredDeletion`, `MeshLifetime`, `ImageStagingRelease`, `BLASBuildOptions`, the `TLAS*` tests, ...). Headless still means a Wayland display on Linux. Most of the RT ones check `Device::rayTracingSupported` and return 77 without it; `BLASBuildOptions`, `TLASInstanceDirtyTracking` and `MeshDecompressStagingRelease` do not, and crash on a GPU without ray tracing (`Device::RequireRayTracing` throws out of `main`). `DeviceSelection` skips with one GPU.
|
||||||
- A window and a live compositor: `FrameLoopSync`, `SetupCmdBufferReuse`, `UIFallbackParity`, `SwapchainTransferDst`, `WindowUpdateClock`, `HeapASRead`, `HeapDynamicIndexHit`, `RTPassPushData`, `WindowOptionalGlobals` (hides xdg-decoration, viewporter and fractional scale from the window in turn), and `FeatureRequests` for its retry case. They render, read the frame back with `Window::CaptureNextFrame` (a PNG, decoded with a private `STB_IMAGE_STATIC` copy of stb_image) and check pixels.
|
- A window and a live compositor: `FrameLoopSync`, `SetupCmdBufferReuse`, `UIFallbackParity`, `SwapchainTransferDst`, `WindowUpdateClock`, `HeapASRead`, `HeapDynamicIndexHit`, `RTPassPushData`, `WindowOptionalGlobals` (hides xdg-decoration, viewporter and fractional scale from the window in turn), `SwapchainImageCount` (every present mode, and swapchains with more images than frames in flight, in child processes on llvmpipe where the host's GPU never makes one), and `FeatureRequests` for its retry case. They render, read the frame back with `Window::CaptureNextFrame` (a PNG, decoded with a private `STB_IMAGE_STATIC` copy of stb_image) or a transfer pass of their own, and check pixels.
|
||||||
|
|
||||||
Conventions the existing tests follow:
|
Conventions the existing tests follow:
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ These were decided deliberately. Do not undo them without the maintainer's say-s
|
||||||
4. **The UI contracts are frozen.** `UIDispatchHeader` is 48 bytes and starts every UI push block; `QuadItem` is 64, `CircleItem` 48, `ImageItem` 64 and `GlyphItem` 48 bytes, byte for byte with `ui-shared.glsl` under `std430`. They grow only through the reserved `flags` bits and `_pad`. `UIFusedHeader` (128 bytes) is a separate contract for `DispatchFused`.
|
4. **The UI contracts are frozen.** `UIDispatchHeader` is 48 bytes and starts every UI push block; `QuadItem` is 64, `CircleItem` 48, `ImageItem` 64 and `GlyphItem` 48 bytes, byte for byte with `ui-shared.glsl` under `std430`. They grow only through the reserved `flags` bits and `_pad`. `UIFusedHeader` (128 bytes) is a separate contract for `DispatchFused`.
|
||||||
5. **Three tiers, and the source is the customisation API.** Tier 1 is `ComputeShader` and `UIRenderer::Dispatch`; Tier 2 the standard kernels, item structs and `UIRenderer` helpers; Tier 3 stateless functions that append items to user-owned buffers and never dispatch. No library `Theme` type (colours come as `*Colors` aggregates), no widget tree, no layout engine beyond `Rect::SubRect` and `Inset`, no hit-testing, no focus management; state lives in user-owned PODs (`InputField` is the model). A component that does not fit is copied and edited, not hooked: no virtual overrides, no extension points. Additive changes (another kernel, another stateless component) are fine.
|
5. **Three tiers, and the source is the customisation API.** Tier 1 is `ComputeShader` and `UIRenderer::Dispatch`; Tier 2 the standard kernels, item structs and `UIRenderer` helpers; Tier 3 stateless functions that append items to user-owned buffers and never dispatch. No library `Theme` type (colours come as `*Colors` aggregates), no widget tree, no layout engine beyond `Rect::SubRect` and `Inset`, no hit-testing, no focus management; state lives in user-owned PODs (`InputField` is the model). A component that does not fit is copied and edited, not hooked: no virtual overrides, no extension points. Additive changes (another kernel, another stateless component) are fine.
|
||||||
6. **Standard kernels dispatch one workgroup per 8x8 screen tile**, each thread owning one pixel and iterating every item in array order into a local accumulator, stored once. Array order is draw order and there is no inter-workgroup race. Never go back to a workgroup per item: Vulkan orders nothing between the workgroups of one dispatch, and overlapping items then race on the image (striped fills, stale pixels). The cost is `O(W*H*N)` and documented as such; the shared-memory tile cull in `ui-shared.glsl` must stay a strict superset of the per-pixel test.
|
6. **Standard kernels dispatch one workgroup per 8x8 screen tile**, each thread owning one pixel and iterating every item in array order into a local accumulator, stored once. Array order is draw order and there is no inter-workgroup race. Never go back to a workgroup per item: Vulkan orders nothing between the workgroups of one dispatch, and overlapping items then race on the image (striped fills, stale pixels). The cost is `O(W*H*N)` and documented as such; the shared-memory tile cull in `ui-shared.glsl` must stay a strict superset of the per-pixel test.
|
||||||
7. **Three frames in flight, a fence per image, no steady-state wait-idle.** The owners of buffers, acceleration structures, shader modules and pipelines (`VulkanBuffer`, `Mesh`, `TlasWithBuffer`, `VulkanShader`, `ComputeShader`, `PipelineRTVulkan`) release them through `Device::EnqueueDeletion`, which has overloads for exactly those handle kinds; `ReclaimDeletions` frees an entry `framesInFlight` frames later and `DrainDeletions` after a wait-idle. Owners of handles the queue has no overload for wait for the GPU first and then destroy directly: `~DescriptorHeapVulkan` in descriptor-set mode, `~Window`, `~FontAtlas` through `ImageVulkan::Destroy`. A destructor never destroys a handle without one or the other: a frame in flight may still use it, and a static owner of a queued kind is destroyed after the driver has gone, which only the queue survives. `Clear()` is for a GPU known to be idle. The deletion queue, its mutex and `RenderingElement3D::tlases` are leaked on purpose for the same reason. Staged re-uploads go through the per-frame staging ring in `VulkanBuffer::Stage`.
|
7. **Three frames in flight, a fence per frame, no steady-state wait-idle.** Frames in flight are not swapchain images: the swapchain has as many images as its present mode needs, what lives per frame (command buffer, fence, acquire semaphore, heap copy, TLAS) is keyed by `Window::currentFrame` and what lives per image by `currentImage`, and the image a frame draws into reaches shaders through the heap's swapchain slots (`DescriptorHeapVulkan::AddSwapchainSlot`, #160). The owners of buffers, acceleration structures, shader modules and pipelines (`VulkanBuffer`, `Mesh`, `TlasWithBuffer`, `VulkanShader`, `ComputeShader`, `PipelineRTVulkan`) release them through `Device::EnqueueDeletion`, which has overloads for exactly those handle kinds; `ReclaimDeletions` frees an entry `framesInFlight` frames later and `DrainDeletions` after a wait-idle. Owners of handles the queue has no overload for wait for the GPU first and then destroy directly: `~DescriptorHeapVulkan` in descriptor-set mode, `~Window`, `~FontAtlas` through `ImageVulkan::Destroy`. A destructor never destroys a handle without one or the other: a frame in flight may still use it, and a static owner of a queued kind is destroyed after the driver has gone, which only the queue survives. `Clear()` is for a GPU known to be idle. The deletion queue, its mutex and `RenderingElement3D::tlases` are leaked on purpose for the same reason. Staged re-uploads go through the per-frame staging ring in `VulkanBuffer::Stage`.
|
||||||
8. **The library enables only the device features its own shaders use.** Everything an application's shaders need (`shaderInt16` for a `uint16_t` spec constant, fp16, 16-bit storage, `scalarBlockLayout`) is the application's to ask for through `Device::Initialize(required, optional)`, and `VulkanShader` refuses a module whose capabilities nothing enabled, naming the field. Do not add a feature to the `library` set for an example's convenience; have the example ask.
|
8. **The library enables only the device features its own shaders use.** Everything an application's shaders need (`shaderInt16` for a `uint16_t` spec constant, fp16, 16-bit storage, `scalarBlockLayout`) is the application's to ask for through `Device::Initialize(required, optional)`, and `VulkanShader` refuses a module whose capabilities nothing enabled, naming the field. Do not add a feature to the `library` set for an example's convenience; have the example ask.
|
||||||
9. **Barriers are scoped and derived.** A `RenderPass` reports its `SwapchainStage()`; the frame loop's acquire, inter-pass and present barriers take their stage and access masks from the passes (`SwapchainStageUnion`, `SwapchainWriterAccess`, `BuildSwapchainInterPassBarrier`), masked by `Device::stageMask` so the RT stage never appears on a device without it. In the frame loop there is no `ALL_COMMANDS` and no queue-wide memory barrier, and a ranged flush rounds to `nonCoherentAtomSize`. The acceleration-structure builds and the GPU decompression do use global `VkMemoryBarrier`s on purpose (`BuildTlasWriteBarrier`, `BuildTlasReadBarrier`, `Decompress::DecompressOnGPU`): execution order alone never makes a build's writes visible to the shaders that read them. The window never clears the swapchain image (`TRANSFER_DST` is not on every surface); passes draw their own background.
|
9. **Barriers are scoped and derived.** A `RenderPass` reports its `SwapchainStage()`; the frame loop's acquire, inter-pass and present barriers take their stage and access masks from the passes (`SwapchainStageUnion`, `SwapchainWriterAccess`, `BuildSwapchainInterPassBarrier`), masked by `Device::stageMask` so the RT stage never appears on a device without it. In the frame loop there is no `ALL_COMMANDS` and no queue-wide memory barrier, and a ranged flush rounds to `nonCoherentAtomSize`. The acceleration-structure builds and the GPU decompression do use global `VkMemoryBarrier`s on purpose (`BuildTlasWriteBarrier`, `BuildTlasReadBarrier`, `Decompress::DecompressOnGPU`): execution order alone never makes a build's writes visible to the shaders that read them. The window never clears the swapchain image (`TRANSFER_DST` is not on every surface); passes draw their own background.
|
||||||
10. **Driver workarounds are fenced and written to be deleted.** The NVIDIA descriptor-heap AS-read rewrite is one block in `ShaderVulkan.cppm` gated on `Device::workaroundDescriptorHeapAS`, with its push-data hook in `RTPass` and `ComputeShader`, a kill switch (`CRAFTER_GRAPHICS_NO_AS_WORKAROUND`), a probe test and a status note in the README and `examples/VulkanTriangle/README.md`. A new workaround gets the same: a `Device` flag, one fenced block, a probe, a note. A fault the library cannot hide (dynamic descriptor indexing in hit shaders) is documented with the consumer-side pattern instead (`examples/Sponza/README.md`).
|
10. **Driver workarounds are fenced and written to be deleted.** The NVIDIA descriptor-heap AS-read rewrite is one block in `ShaderVulkan.cppm` gated on `Device::workaroundDescriptorHeapAS`, with its push-data hook in `RTPass` and `ComputeShader`, a kill switch (`CRAFTER_GRAPHICS_NO_AS_WORKAROUND`), a probe test and a status note in the README and `examples/VulkanTriangle/README.md`. A new workaround gets the same: a `Device` flag, one fenced block, a probe, a note. A fault the library cannot hide (dynamic descriptor indexing in hit shaders) is documented with the consumer-side pattern instead (`examples/Sponza/README.md`).
|
||||||
|
|
@ -141,11 +141,11 @@ These were decided deliberately. Do not undo them without the maintainer's say-s
|
||||||
- `Device::Initialize` on the Wayland backend connects to the display and throws without one; "headless" tests still need a compositor.
|
- `Device::Initialize` on the Wayland backend connects to the display and throws without one; "headless" tests still need a compositor.
|
||||||
- Buffer slot indices: `AllocateBufferSlots` returns an index relative to the heap's buffer region, while GLSL `descriptor_heap` SSBO arrays index in buffer-descriptor units from heap byte 0. `BufferSlot` converts to `bufferStartElement + raw`; raw code (the RT examples) passes `heap.bufferStartElement` as a `uint16_t` spec constant, which needs `shaderInt16`. In descriptor-set mode `bufferStartElement` is 0. Image and sampler slots are used as they are. A wrong index reads zeros, which looks like "every item field is zero".
|
- Buffer slot indices: `AllocateBufferSlots` returns an index relative to the heap's buffer region, while GLSL `descriptor_heap` SSBO arrays index in buffer-descriptor units from heap byte 0. `BufferSlot` converts to `bufferStartElement + raw`; raw code (the RT examples) passes `heap.bufferStartElement` as a `uint16_t` spec constant, which needs `shaderInt16`. In descriptor-set mode `bufferStartElement` is 0. Image and sampler slots are used as they are. A wrong index reads zeros, which looks like "every item field is zero".
|
||||||
- `UIRenderer::RegisterBuffer` refuses a buffer created without `VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT`. Freed slots are handed out again at once, so a slot may only be freed when no frame in flight reads it (after `vkQueueWaitIdle`, or at a scene switch). In descriptor-set mode a slot write destroys the previous view immediately.
|
- `UIRenderer::RegisterBuffer` refuses a buffer created without `VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT`. Freed slots are handed out again at once, so a slot may only be freed when no frame in flight reads it (after `vkQueueWaitIdle`, or at a scene switch). In descriptor-set mode a slot write destroys the previous view immediately.
|
||||||
- The swapchain image stays in `VK_IMAGE_LAYOUT_GENERAL` across the pass list and is never cleared; `Window::clearColor` is unused. `swapchainUsage` says whether `TRANSFER_DST` was available. `onResize` fires after a swapchain rebuild; anything holding descriptors of `window.imageViews[]` rewrites them there (`UIRenderer` does its own).
|
- The swapchain image stays in `VK_IMAGE_LAYOUT_GENERAL` across the pass list and is never cleared; `Window::clearColor` is unused. `swapchainUsage` says whether `TRANSFER_DST` was available. `onResize` fires after a swapchain rebuild; slots listed with `AddSwapchainSlot` follow by themselves, and anything else holding the old images is made again there. A rebuild asked for during a frame (`Resize` or `SetPresentMode` from a listener) waits for that frame's present, since the rest of the frame indexes the swapchain it acquired from. `RenderPass::Record`'s `frameIdx` is the frame slot, not the image.
|
||||||
- `Device::frameCounter` is one clock for every window, so the deletion queue and the staging rings assume one rendering window.
|
- `Device::frameCounter` is one clock for every window, so the deletion queue and the staging rings assume one rendering window.
|
||||||
- `ImageVulkan::Create`'s default `consumerStage` is the RT stage, corrected to compute on a device without ray tracing; the font atlas passes compute explicitly. `VulkanTransition`'s `GENERAL` mapping does the same.
|
- `ImageVulkan::Create`'s default `consumerStage` is the RT stage, corrected to compute on a device without ray tracing; the font atlas passes compute explicitly. `VulkanTransition`'s `GENERAL` mapping does the same.
|
||||||
- `UIRenderer::ShapeText` and `FontAtlas` key their caches on the `Font*`: call `UIRenderer::InvalidateFont` before destroying a `Font`, or a new one at the same address aliases stale runs.
|
- `UIRenderer::ShapeText` and `FontAtlas` key their caches on the `Font*`: call `UIRenderer::InvalidateFont` before destroying a `Font`, or a new one at the same address aliases stale runs.
|
||||||
- `RenderingElement3D::elements` stores raw pointers; a `std::vector<RenderingElement3D>` that grows after `Add` dangles them (Sponza reserves up front). `BuildTLAS` on an index whose earlier submission has not completed overwrites a TLAS in use; building `tlases[window.currentBuffer]` from `onUpdate` is safe, and `tlases[i].address` changes when a TLAS outgrows its storage.
|
- `RenderingElement3D::elements` stores raw pointers; a `std::vector<RenderingElement3D>` that grows after `Add` dangles them (Sponza reserves up front). `BuildTLAS` on an index whose earlier submission has not completed overwrites a TLAS in use; building `tlases[window.currentFrame]` from `onUpdate` is safe, and `tlases[i].address` changes when a TLAS outgrows its storage.
|
||||||
- `PipelineRTVulkan::Init` defaults `maxRecursionDepth` to 1; a closest-hit that traces needs 2, and tracing deeper than the pipeline allows loses the device.
|
- `PipelineRTVulkan::Init` defaults `maxRecursionDepth` to 1; a closest-hit that traces needs 2, and tracing deeper than the pipeline allows loses the device.
|
||||||
- `BuildTlasWriteBarrier`'s source access mask carries no read bit on purpose: synchronization validation 1.4.363 otherwise reports two builds in one command buffer as a write-after-write hazard.
|
- `BuildTlasWriteBarrier`'s source access mask carries no read bit on purpose: synchronization validation 1.4.363 otherwise reports two builds in one command buffer as a write-after-write hazard.
|
||||||
- NVIDIA, as of driver `615.78.08`: reading a TLAS through the heap still faults (worked around); dynamic descriptor indexing in hit shaders works there and faulted on `610.43.02`, so the Sponza pattern (a spec-constant slot, a dynamic index within the resource) stays the portable one. `HeapDynamicIndexHit` skips on the older drivers.
|
- NVIDIA, as of driver `615.78.08`: reading a TLAS through the heap still faults (worked around); dynamic descriptor indexing in hit shaders works there and faulted on `610.43.02`, so the Sponza pattern (a spec-constant slot, a dynamic index within the resource) stays the portable one. `HeapDynamicIndexHit` skips on the older drivers.
|
||||||
|
|
|
||||||
34
README.md
34
README.md
|
|
@ -91,18 +91,36 @@ shaded through an intersection shader with an any-hit cut-out.
|
||||||
compositor has them. Without xdg-decoration (GNOME) the window has no
|
compositor has them. Without xdg-decoration (GNOME) the window has no
|
||||||
title bar, as the library draws no client-side decorations; when either
|
title bar, as the library draws no client-side decorations; when either
|
||||||
of the other two is missing it draws at scale 1, and the compositor
|
of the other two is missing it draws at scale 1, and the compositor
|
||||||
scales it up on a HiDPI output.
|
scales it up on a HiDPI output. Natively the frame loop keeps three
|
||||||
|
frames in flight (`Window::numFrames`) over however many images the
|
||||||
|
swapchain has: as many as its present mode needs. Where the instance has
|
||||||
|
`surface_maintenance1` and the device `swapchain_maintenance1`, that is
|
||||||
|
the mode's own count, which on Mesa's Wayland is below the surface's
|
||||||
|
count for all modes (FIFO three, Mailbox four); without, the stricter
|
||||||
|
of the two. `window.currentFrame` picks what lives per frame in flight
|
||||||
|
(the command buffer, the descriptor-heap copy,
|
||||||
|
`RenderingElement3D::tlases`, the `frameIdx` a pass is recorded with);
|
||||||
|
`window.currentImage` is the swapchain image the frame draws into,
|
||||||
|
`window.images[window.currentImage]`. A shader reads that image from a
|
||||||
|
heap slot listed with `heap.AddSwapchainSlot(slot, window)`, which the
|
||||||
|
window rewrites with the acquired image every frame, so nothing is
|
||||||
|
rewritten after a resize, and two windows can share a heap. A `Resize`
|
||||||
|
or `SetPresentMode` from a listener of the frame being recorded takes
|
||||||
|
effect after that frame's present.
|
||||||
- **Device** *(Vulkan only)* — single-instance bring-up targeting
|
- **Device** *(Vulkan only)* — single-instance bring-up targeting
|
||||||
`VK_EXT_descriptor_heap`; pipelines are created with
|
`VK_EXT_descriptor_heap`; pipelines are created with
|
||||||
`VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT` so there are no
|
`VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT` so there are no
|
||||||
descriptor-set layouts and push constants travel via
|
descriptor-set layouts and push constants travel via
|
||||||
`vkCmdPushDataEXT`. Needs Vulkan 1.3. The heap, ray tracing and
|
`vkCmdPushDataEXT`. Needs Vulkan 1.3. The heap, ray tracing,
|
||||||
device-fault extensions are optional groups, detected at start-up
|
device-fault and swapchain-maintenance extensions are optional groups,
|
||||||
(`Device::descriptorHeapSupported`, `rayTracingSupported`,
|
detected at start-up (`Device::descriptorHeapSupported`,
|
||||||
`deviceFaultSupported`). Device features split by who uses them: the
|
`rayTracingSupported`, `deviceFaultSupported`,
|
||||||
ones the binding model and the library's own shaders need are the
|
`swapchainMaintenance1Supported`), as is the instance's
|
||||||
library's to enable; arithmetic, storage and layout features an
|
`surface_maintenance1` (`surfaceMaintenance1Supported`). Device
|
||||||
application's shaders use (`shaderInt16` for a `uint16_t` spec
|
features split by who uses them: the ones the binding model and the
|
||||||
|
library's own shaders need are the library's to enable; arithmetic,
|
||||||
|
storage and layout features an application's shaders use
|
||||||
|
(`shaderInt16` for a `uint16_t` spec
|
||||||
constant, `scalarBlockLayout`, fp16, 16-bit storage) are the
|
constant, `scalarBlockLayout`, fp16, 16-bit storage) are the
|
||||||
application's to ask for, as core feature structs:
|
application's to ask for, as core feature structs:
|
||||||
`Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}})`. A second
|
`Device::Initialize({.vulkan10 = {.shaderInt16 = VK_TRUE}})`. A second
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ Ray-traced single triangle through `vkCmdTraceRaysKHR`. Shows the full
|
||||||
ray-tracing setup: `DescriptorHeapVulkan` with image and buffer slots,
|
ray-tracing setup: `DescriptorHeapVulkan` with image and buffer slots,
|
||||||
`PipelineRTVulkan` from raygen / miss / closesthit SPIR-V, BLAS via
|
`PipelineRTVulkan` from raygen / miss / closesthit SPIR-V, BLAS via
|
||||||
`Mesh::Build`, TLAS via `RenderingElement3D::BuildTLAS`, direct
|
`Mesh::Build`, TLAS via `RenderingElement3D::BuildTLAS`, direct
|
||||||
`vkWriteResourceDescriptorsEXT` for swapchain views, `RTPass` on
|
`vkWriteResourceDescriptorsEXT` for the per-frame TLASes,
|
||||||
`window.passes`. Smallest test of the bindless ray-tracing path.
|
`AddSwapchainSlot` for the output image, `RTPass` on `window.passes`.
|
||||||
|
Smallest test of the bindless ray-tracing path.
|
||||||
|
|
||||||
### [HelloUI](HelloUI/)
|
### [HelloUI](HelloUI/)
|
||||||
Compute-shader UI demo using all three UI tiers:
|
Compute-shader UI demo using all three UI tiers:
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ int main() {
|
||||||
// descriptor-heap compute pipeline still expects a heap bound for its
|
// descriptor-heap compute pipeline still expects a heap bound for its
|
||||||
// push-data path — the render loop binds it every frame, but the init
|
// push-data path — the render loop binds it every frame, but the init
|
||||||
// command buffer hasn't, so bind it here before dispatching.
|
// command buffer hasn't, so bind it here before dispatching.
|
||||||
descriptorHeap.Bind(cmd, window.currentBuffer);
|
descriptorHeap.Bind(cmd, window.currentFrame);
|
||||||
|
|
||||||
ComputeShader aabbWriter;
|
ComputeShader aabbWriter;
|
||||||
aabbWriter.Load("aabbs.comp.spv", descriptorHeap);
|
aabbWriter.Load("aabbs.comp.spv", descriptorHeap);
|
||||||
|
|
@ -167,23 +167,20 @@ int main() {
|
||||||
|
|
||||||
window.FinishInit();
|
window.FinishInit();
|
||||||
|
|
||||||
// Write descriptors: TLAS at bufSlots[0], output image at imgSlots[0].
|
// Write descriptors: each frame's TLAS at bufSlots[0] of that frame's
|
||||||
// Per-frame replicated — same pattern as the Sponza example.
|
// copy of the heap — same pattern as the Sponza example. The output image
|
||||||
|
// at imgSlots[0] is the swapchain's, which the window writes every frame
|
||||||
|
// with the image that frame draws into.
|
||||||
|
descriptorHeap.AddSwapchainSlot(imgSlots.firstElement, window);
|
||||||
VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
|
VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
|
||||||
VkImageDescriptorInfoEXT outImgInfos[Window::numFrames];
|
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
||||||
outImgInfos[f] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[f],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<VkResourceDescriptorInfoEXT> resources;
|
std::vector<VkResourceDescriptorInfoEXT> resources;
|
||||||
std::vector<VkHostAddressRangeEXT> destinations;
|
std::vector<VkHostAddressRangeEXT> destinations;
|
||||||
resources.reserve(Window::numFrames * 2);
|
resources.reserve(Window::numFrames);
|
||||||
destinations.reserve(Window::numFrames * 2);
|
destinations.reserve(Window::numFrames);
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
resources.push_back({
|
resources.push_back({
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
|
|
@ -195,16 +192,6 @@ int main() {
|
||||||
+ descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
+ descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
||||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
||||||
});
|
});
|
||||||
resources.push_back({
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &outImgInfos[f] },
|
|
||||||
});
|
|
||||||
destinations.push_back({
|
|
||||||
.address = descriptorHeap.resourceHeap[f].value
|
|
||||||
+ descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Device::vkWriteResourceDescriptorsEXT(Device::device,
|
Device::vkWriteResourceDescriptorsEXT(Device::device,
|
||||||
static_cast<std::uint32_t>(resources.size()),
|
static_cast<std::uint32_t>(resources.size()),
|
||||||
|
|
|
||||||
|
|
@ -165,11 +165,13 @@ int main() {
|
||||||
|
|
||||||
window.FinishInit();
|
window.FinishInit();
|
||||||
|
|
||||||
// Write descriptors: TLAS at bufSlots[0], output image at imgSlots[0],
|
// Write descriptors: TLAS at bufSlots[0] and albedo (a sampled image) at
|
||||||
// albedo (a sampled image) at imgSlots[1], and its sampler at
|
// imgSlots[1], into every frame's copy of the heap, each frame with its
|
||||||
// samplerSlots[0] in the sampler heap. Per-frame replicated.
|
// own TLAS, and the albedo's sampler at samplerSlots[0] in the sampler
|
||||||
|
// heap. The output image at imgSlots[0] is the swapchain's, which the
|
||||||
|
// window writes every frame with the image that frame draws into.
|
||||||
|
descriptorHeap.AddSwapchainSlot(imgSlots.firstElement, window);
|
||||||
VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
|
VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
|
||||||
VkImageDescriptorInfoEXT outImgInfos[Window::numFrames];
|
|
||||||
// The heap takes an image view's create-info, not a VkImageView.
|
// The heap takes an image view's create-info, not a VkImageView.
|
||||||
const VkImageViewCreateInfo albedoView {
|
const VkImageViewCreateInfo albedoView {
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||||
|
|
@ -189,17 +191,12 @@ int main() {
|
||||||
};
|
};
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
||||||
outImgInfos[f] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[f],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<VkResourceDescriptorInfoEXT> resources;
|
std::vector<VkResourceDescriptorInfoEXT> resources;
|
||||||
std::vector<VkHostAddressRangeEXT> destinations;
|
std::vector<VkHostAddressRangeEXT> destinations;
|
||||||
resources.reserve(Window::numFrames * 3);
|
resources.reserve(Window::numFrames * 2);
|
||||||
destinations.reserve(Window::numFrames * 3);
|
destinations.reserve(Window::numFrames * 2);
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
resources.push_back({
|
resources.push_back({
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
|
|
@ -211,16 +208,6 @@ int main() {
|
||||||
+ descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
+ descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
||||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
||||||
});
|
});
|
||||||
resources.push_back({
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &outImgInfos[f] },
|
|
||||||
});
|
|
||||||
destinations.push_back({
|
|
||||||
.address = descriptorHeap.resourceHeap[f].value
|
|
||||||
+ descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize,
|
|
||||||
});
|
|
||||||
resources.push_back({
|
resources.push_back({
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
|
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
|
||||||
|
|
|
||||||
|
|
@ -190,29 +190,26 @@ int main() {
|
||||||
albedo.Update(loadedTex, cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
albedo.Update(loadedTex, cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||||
// The albedo is uploaded to keep the texture-decompress + staging
|
// The albedo is uploaded to keep the texture-decompress + staging
|
||||||
// setup path in the measured "setup" window, but is not bound — the
|
// setup path in the measured "setup" window, but is not bound — the
|
||||||
// closest-hit shades from barycentrics, so only the TLAS + per-frame
|
// closest-hit shades from barycentrics, so only the per-frame TLAS and
|
||||||
// output image need descriptors.
|
// the output image need descriptors.
|
||||||
|
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f)
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f)
|
||||||
RenderingElement3D::BuildTLAS(cmd, f);
|
RenderingElement3D::BuildTLAS(cmd, f);
|
||||||
|
|
||||||
window.FinishInit();
|
window.FinishInit();
|
||||||
|
|
||||||
|
// The output image at imgSlots[0] is the swapchain's, which the window
|
||||||
|
// writes every frame with the image that frame draws into.
|
||||||
|
descriptorHeap.AddSwapchainSlot(imgSlots.firstElement, window);
|
||||||
VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
|
VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
|
||||||
VkImageDescriptorInfoEXT outImgInfos[Window::numFrames];
|
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
||||||
outImgInfos[f] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[f],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<VkResourceDescriptorInfoEXT> resources;
|
std::vector<VkResourceDescriptorInfoEXT> resources;
|
||||||
std::vector<VkHostAddressRangeEXT> destinations;
|
std::vector<VkHostAddressRangeEXT> destinations;
|
||||||
resources.reserve(Window::numFrames * 2);
|
resources.reserve(Window::numFrames);
|
||||||
destinations.reserve(Window::numFrames * 2);
|
destinations.reserve(Window::numFrames);
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
resources.push_back({
|
resources.push_back({
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
|
|
@ -224,16 +221,6 @@ int main() {
|
||||||
+ descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
+ descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
||||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
||||||
});
|
});
|
||||||
resources.push_back({
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &outImgInfos[f] },
|
|
||||||
});
|
|
||||||
destinations.push_back({
|
|
||||||
.address = descriptorHeap.resourceHeap[f].value
|
|
||||||
+ descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Device::vkWriteResourceDescriptorsEXT(Device::device,
|
Device::vkWriteResourceDescriptorsEXT(Device::device,
|
||||||
static_cast<std::uint32_t>(resources.size()),
|
static_cast<std::uint32_t>(resources.size()),
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ The minimal ray-traced example. Renders a single static triangle through
|
||||||
- `Mesh::Build` constructing a BLAS and `RenderingElement3D::BuildTLAS`
|
- `Mesh::Build` constructing a BLAS and `RenderingElement3D::BuildTLAS`
|
||||||
the per-frame TLAS.
|
the per-frame TLAS.
|
||||||
- Direct descriptor writes via `vkWriteResourceDescriptorsEXT` for the
|
- Direct descriptor writes via `vkWriteResourceDescriptorsEXT` for the
|
||||||
swapchain views and TLAS device addresses.
|
per-frame TLAS device addresses, and `AddSwapchainSlot` for the output
|
||||||
|
image, which the window writes with the swapchain image each frame
|
||||||
|
draws into.
|
||||||
- `RTPass{&pipeline}` plugged into `window.passes` — the canonical
|
- `RTPass{&pipeline}` plugged into `window.passes` — the canonical
|
||||||
way to add ray tracing to a window in this library.
|
way to add ray tracing to a window in this library.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,12 @@ int main() {
|
||||||
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
|
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
|
||||||
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
||||||
|
|
||||||
|
// The output image is the swapchain image the frame draws into, which
|
||||||
|
// changes from frame to frame: the window writes it into the heap at this
|
||||||
|
// slot every frame.
|
||||||
|
descriptorHeap.AddSwapchainSlot(imgSlots.firstElement, window);
|
||||||
|
|
||||||
|
// Each frame in flight has its own TLAS and its own copy of the heap.
|
||||||
VkDeviceAddressRangeKHR tlasRange0 = {
|
VkDeviceAddressRangeKHR tlasRange0 = {
|
||||||
.address = RenderingElement3D::tlases[0].address,
|
.address = RenderingElement3D::tlases[0].address,
|
||||||
};
|
};
|
||||||
|
|
@ -117,85 +123,40 @@ int main() {
|
||||||
.address = RenderingElement3D::tlases[2].address,
|
.address = RenderingElement3D::tlases[2].address,
|
||||||
};
|
};
|
||||||
|
|
||||||
VkImageDescriptorInfoEXT imageInfo0 = {
|
VkResourceDescriptorInfoEXT resources[3] = {
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[0],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL
|
|
||||||
};
|
|
||||||
|
|
||||||
VkImageDescriptorInfoEXT imageInfo1 = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[1],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL
|
|
||||||
};
|
|
||||||
|
|
||||||
VkImageDescriptorInfoEXT imageInfo2 = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[2],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL
|
|
||||||
};
|
|
||||||
|
|
||||||
VkResourceDescriptorInfoEXT resources[6] = {
|
|
||||||
{
|
{
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
||||||
.data = { .pAddressRange = &tlasRange0}
|
.data = { .pAddressRange = &tlasRange0}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &imageInfo0 }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
||||||
.data = { .pAddressRange = &tlasRange1}
|
.data = { .pAddressRange = &tlasRange1}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &imageInfo1 }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
||||||
.data = { .pAddressRange = &tlasRange2}
|
.data = { .pAddressRange = &tlasRange2}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &imageInfo2 }
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
VkHostAddressRangeEXT destinations[6] = {
|
VkHostAddressRangeEXT destinations[3] = {
|
||||||
{
|
{
|
||||||
.address = descriptorHeap.resourceHeap[0].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
.address = descriptorHeap.resourceHeap[0].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
||||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize
|
.size = Device::descriptorHeapProperties.bufferDescriptorSize
|
||||||
},
|
},
|
||||||
{
|
|
||||||
.address = descriptorHeap.resourceHeap[0].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
.address = descriptorHeap.resourceHeap[1].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
.address = descriptorHeap.resourceHeap[1].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
||||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize
|
.size = Device::descriptorHeapProperties.bufferDescriptorSize
|
||||||
},
|
},
|
||||||
{
|
|
||||||
.address = descriptorHeap.resourceHeap[1].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
.address = descriptorHeap.resourceHeap[2].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
.address = descriptorHeap.resourceHeap[2].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
||||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize
|
.size = Device::descriptorHeapProperties.bufferDescriptorSize
|
||||||
},
|
},
|
||||||
{
|
|
||||||
.address = descriptorHeap.resourceHeap[2].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Device::vkWriteResourceDescriptorsEXT(Device::device, 6, resources, destinations);
|
Device::vkWriteResourceDescriptorsEXT(Device::device, 3, resources, destinations);
|
||||||
|
|
||||||
descriptorHeap.resourceHeap[0].FlushDevice();
|
descriptorHeap.resourceHeap[0].FlushDevice();
|
||||||
descriptorHeap.resourceHeap[1].FlushDevice();
|
descriptorHeap.resourceHeap[1].FlushDevice();
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,12 @@ const char* const instanceExtensionNames[] = {
|
||||||
"VK_KHR_wayland_surface"
|
"VK_KHR_wayland_surface"
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
namespace {
|
||||||
|
// The surface_maintenance1 Initialize enabled on the instance, the KHR
|
||||||
|
// form or the EXT one, or null. A device's swapchain_maintenance1 has to
|
||||||
|
// be the same form: each requires its own.
|
||||||
|
const char* instanceSurfaceMaintenance1 = nullptr;
|
||||||
|
}
|
||||||
// Device extensions are split by what breaks without them. Only the first
|
// Device extensions are split by what breaks without them. Only the first
|
||||||
// group is needed to put UI pixels on screen, and a GPU without it is skipped,
|
// group is needed to put UI pixels on screen, and a GPU without it is skipped,
|
||||||
// as is one below Vulkan 1.4 without VK_KHR_maintenance5 (core from 1.4).
|
// as is one below Vulkan 1.4 without VK_KHR_maintenance5 (core from 1.4).
|
||||||
|
|
@ -778,6 +784,31 @@ void Device::seat_handle_capabilities(void*, wl_seat* seat, uint32_t capabilitie
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
std::vector<std::vector<VkPhysicalDevice>> Device::DistinctGpus(VkInstance instance) {
|
||||||
|
std::uint32_t physDeviceCount = 0;
|
||||||
|
CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, nullptr));
|
||||||
|
std::vector<VkPhysicalDevice> physDevices(physDeviceCount);
|
||||||
|
CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, physDevices.data()));
|
||||||
|
|
||||||
|
std::vector<std::vector<VkPhysicalDevice>> gpus;
|
||||||
|
std::vector<std::array<std::uint8_t, VK_UUID_SIZE>> seen;
|
||||||
|
for (VkPhysicalDevice device : physDevices) {
|
||||||
|
VkPhysicalDeviceIDProperties id { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES };
|
||||||
|
VkPhysicalDeviceProperties2 properties { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &id };
|
||||||
|
vkGetPhysicalDeviceProperties2(device, &properties);
|
||||||
|
std::array<std::uint8_t, VK_UUID_SIZE> uuid;
|
||||||
|
std::ranges::copy(id.deviceUUID, uuid.begin());
|
||||||
|
const auto at = std::ranges::find(seen, uuid);
|
||||||
|
if (at == seen.end()) {
|
||||||
|
seen.push_back(uuid);
|
||||||
|
gpus.push_back({ device });
|
||||||
|
} else {
|
||||||
|
gpus[at - seen.begin()].push_back(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gpus;
|
||||||
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// What Initialize learns about one GPU before choosing it.
|
// What Initialize learns about one GPU before choosing it.
|
||||||
struct Candidate {
|
struct Candidate {
|
||||||
|
|
@ -800,6 +831,10 @@ namespace {
|
||||||
bool faultVendorBinary = false;
|
bool faultVendorBinary = false;
|
||||||
bool decompression = false;
|
bool decompression = false;
|
||||||
bool workaroundAS = false;
|
bool workaroundAS = false;
|
||||||
|
// VK_KHR_swapchain_maintenance1 or VK_EXT_swapchain_maintenance1,
|
||||||
|
// whichever matches the instance's surface_maintenance1, with its
|
||||||
|
// feature bit; null without. Becomes swapchainMaintenance1Supported.
|
||||||
|
const char* swapchainMaintenance1 = nullptr;
|
||||||
// The extensions every device needs, maintenance5 among them below
|
// The extensions every device needs, maintenance5 among them below
|
||||||
// Vulkan 1.4. Initialize adds the groups'.
|
// Vulkan 1.4. Initialize adds the groups'.
|
||||||
std::vector<const char*> requiredExtensions;
|
std::vector<const char*> requiredExtensions;
|
||||||
|
|
@ -880,6 +915,14 @@ namespace {
|
||||||
const bool rayTracingExtensions = hasAll(rayTracingExtensionNames);
|
const bool rayTracingExtensions = hasAll(rayTracingExtensionNames);
|
||||||
const bool faultExtension = availableExtensions.contains(VK_EXT_DEVICE_FAULT_EXTENSION_NAME);
|
const bool faultExtension = availableExtensions.contains(VK_EXT_DEVICE_FAULT_EXTENSION_NAME);
|
||||||
const bool decompressionExtension = availableExtensions.contains(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
|
const bool decompressionExtension = availableExtensions.contains(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
|
||||||
|
const char* swapchainMaintenance1Extension =
|
||||||
|
instanceSurfaceMaintenance1 == nullptr ? nullptr
|
||||||
|
: std::string_view(instanceSurfaceMaintenance1) == VK_KHR_SURFACE_MAINTENANCE_1_EXTENSION_NAME
|
||||||
|
? VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME
|
||||||
|
: VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME;
|
||||||
|
if (swapchainMaintenance1Extension != nullptr && !availableExtensions.contains(swapchainMaintenance1Extension)) {
|
||||||
|
swapchainMaintenance1Extension = nullptr;
|
||||||
|
}
|
||||||
DeviceFeatures& supported = candidate.supported;
|
DeviceFeatures& supported = candidate.supported;
|
||||||
VkPhysicalDeviceMaintenance5Features maintenance5Query {
|
VkPhysicalDeviceMaintenance5Features maintenance5Query {
|
||||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES,
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES,
|
||||||
|
|
@ -905,6 +948,10 @@ namespace {
|
||||||
VkPhysicalDeviceMemoryDecompressionFeaturesEXT decompressionQuery {
|
VkPhysicalDeviceMemoryDecompressionFeaturesEXT decompressionQuery {
|
||||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT,
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT,
|
||||||
};
|
};
|
||||||
|
// The EXT form's struct is the same one under another name.
|
||||||
|
VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR swapchainMaintenance1Query {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR,
|
||||||
|
};
|
||||||
VkPhysicalDeviceFeatures2 featureQuery {
|
VkPhysicalDeviceFeatures2 featureQuery {
|
||||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
|
||||||
};
|
};
|
||||||
|
|
@ -935,6 +982,9 @@ namespace {
|
||||||
if (decompressionExtension) {
|
if (decompressionExtension) {
|
||||||
ask(decompressionQuery);
|
ask(decompressionQuery);
|
||||||
}
|
}
|
||||||
|
if (swapchainMaintenance1Extension != nullptr) {
|
||||||
|
ask(swapchainMaintenance1Query);
|
||||||
|
}
|
||||||
vkGetPhysicalDeviceFeatures2(physical, &featureQuery);
|
vkGetPhysicalDeviceFeatures2(physical, &featureQuery);
|
||||||
supported.vulkan10 = featureQuery.features;
|
supported.vulkan10 = featureQuery.features;
|
||||||
// The chain pointed at this function's locals.
|
// The chain pointed at this function's locals.
|
||||||
|
|
@ -959,6 +1009,7 @@ namespace {
|
||||||
candidate.fault = faultExtension && faultQuery.deviceFault;
|
candidate.fault = faultExtension && faultQuery.deviceFault;
|
||||||
candidate.faultVendorBinary = faultQuery.deviceFaultVendorBinary;
|
candidate.faultVendorBinary = faultQuery.deviceFaultVendorBinary;
|
||||||
candidate.decompression = decompressionExtension && decompressionQuery.memoryDecompression;
|
candidate.decompression = decompressionExtension && decompressionQuery.memoryDecompression;
|
||||||
|
candidate.swapchainMaintenance1 = swapchainMaintenance1Query.swapchainMaintenance1 ? swapchainMaintenance1Extension : nullptr;
|
||||||
// NVIDIA's brand-new VK_EXT_descriptor_heap acceleration-structure read
|
// NVIDIA's brand-new VK_EXT_descriptor_heap acceleration-structure read
|
||||||
// path faults (see #7); enable the SPIR-V rewrite workaround there.
|
// path faults (see #7); enable the SPIR-V rewrite workaround there.
|
||||||
// Other drivers (and any future fixed NVIDIA driver, once this gate is
|
// Other drivers (and any future fixed NVIDIA driver, once this gate is
|
||||||
|
|
@ -1043,33 +1094,7 @@ namespace {
|
||||||
// The GPU Initialize uses, or a throw naming why there is none.
|
// The GPU Initialize uses, or a throw naming why there is none.
|
||||||
Candidate ChooseGpu(VkInstance instance, std::uint32_t instanceVersion,
|
Candidate ChooseGpu(VkInstance instance, std::uint32_t instanceVersion,
|
||||||
const DeviceFeatures& required, const DeviceFeatures& optional) {
|
const DeviceFeatures& required, const DeviceFeatures& optional) {
|
||||||
std::uint32_t physDeviceCount = 0;
|
const std::vector<std::vector<VkPhysicalDevice>> gpus = Device::DistinctGpus(instance);
|
||||||
Device::CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, nullptr));
|
|
||||||
std::vector<VkPhysicalDevice> physDevices(physDeviceCount);
|
|
||||||
Device::CheckVkResult(vkEnumeratePhysicalDevices(instance, &physDeviceCount, physDevices.data()));
|
|
||||||
|
|
||||||
// The handles, grouped by GPU: a GPU two drivers both list (a Flatpak
|
|
||||||
// sees the runtime's and the GL extension's manifests; RADV beside
|
|
||||||
// AMDVLK) has one device UUID and two handles. The numbers below count
|
|
||||||
// GPUs, so one means the same GPU inside a sandbox and out.
|
|
||||||
std::vector<std::vector<VkPhysicalDevice>> gpus;
|
|
||||||
{
|
|
||||||
std::vector<std::array<std::uint8_t, VK_UUID_SIZE>> seen;
|
|
||||||
for (VkPhysicalDevice device : physDevices) {
|
|
||||||
VkPhysicalDeviceIDProperties id { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES };
|
|
||||||
VkPhysicalDeviceProperties2 properties { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &id };
|
|
||||||
vkGetPhysicalDeviceProperties2(device, &properties);
|
|
||||||
std::array<std::uint8_t, VK_UUID_SIZE> uuid;
|
|
||||||
std::ranges::copy(id.deviceUUID, uuid.begin());
|
|
||||||
const auto at = std::ranges::find(seen, uuid);
|
|
||||||
if (at == seen.end()) {
|
|
||||||
seen.push_back(uuid);
|
|
||||||
gpus.push_back({ device });
|
|
||||||
} else {
|
|
||||||
gpus[at - seen.begin()].push_back(device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// A GPU's first handle that can run the application, else its first
|
// A GPU's first handle that can run the application, else its first
|
||||||
// handle, gaps and all.
|
// handle, gaps and all.
|
||||||
const auto evaluate = [&](const std::vector<VkPhysicalDevice>& handles) {
|
const auto evaluate = [&](const std::vector<VkPhysicalDevice>& handles) {
|
||||||
|
|
@ -1153,8 +1178,36 @@ void Device::Initialize(const DeviceFeatures& required, const DeviceFeatures& op
|
||||||
instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
|
instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
|
||||||
instanceCreateInfo.pNext = enableValidation ? (const void*)&validationFeatures : nullptr;
|
instanceCreateInfo.pNext = enableValidation ? (const void*)&validationFeatures : nullptr;
|
||||||
instanceCreateInfo.pApplicationInfo = &app;
|
instanceCreateInfo.pApplicationInfo = &app;
|
||||||
instanceCreateInfo.enabledExtensionCount = sizeof(instanceExtensionNames) / sizeof(const char*);
|
|
||||||
instanceCreateInfo.ppEnabledExtensionNames = instanceExtensionNames;
|
// The extensions above, plus optional ones where the loader has them:
|
||||||
|
// surface_maintenance1 answers a surface's capabilities for one present
|
||||||
|
// mode, through vkGetPhysicalDeviceSurfaceCapabilities2KHR, so it comes
|
||||||
|
// with get_surface_capabilities2 or not at all. The KHR form is the EXT
|
||||||
|
// one promoted; either will do.
|
||||||
|
std::vector<const char*> enabledInstanceExtensions(std::begin(instanceExtensionNames), std::end(instanceExtensionNames));
|
||||||
|
{
|
||||||
|
std::uint32_t count = 0;
|
||||||
|
CheckVkResult(vkEnumerateInstanceExtensionProperties(nullptr, &count, nullptr));
|
||||||
|
std::vector<VkExtensionProperties> available(count);
|
||||||
|
CheckVkResult(vkEnumerateInstanceExtensionProperties(nullptr, &count, available.data()));
|
||||||
|
const auto offered = [&](const char* name) {
|
||||||
|
return std::ranges::any_of(available, [&](const VkExtensionProperties& extension) {
|
||||||
|
return std::strcmp(extension.extensionName, name) == 0;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const char* surfaceMaintenance1 =
|
||||||
|
offered(VK_KHR_SURFACE_MAINTENANCE_1_EXTENSION_NAME) ? VK_KHR_SURFACE_MAINTENANCE_1_EXTENSION_NAME
|
||||||
|
: offered(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME) ? VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME
|
||||||
|
: nullptr;
|
||||||
|
surfaceMaintenance1Supported = surfaceMaintenance1 != nullptr && offered(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME);
|
||||||
|
instanceSurfaceMaintenance1 = surfaceMaintenance1Supported ? surfaceMaintenance1 : nullptr;
|
||||||
|
if (surfaceMaintenance1Supported) {
|
||||||
|
enabledInstanceExtensions.push_back(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME);
|
||||||
|
enabledInstanceExtensions.push_back(surfaceMaintenance1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instanceCreateInfo.enabledExtensionCount = static_cast<std::uint32_t>(enabledInstanceExtensions.size());
|
||||||
|
instanceCreateInfo.ppEnabledExtensionNames = enabledInstanceExtensions.data();
|
||||||
|
|
||||||
size_t foundInstanceLayers = 0;
|
size_t foundInstanceLayers = 0;
|
||||||
|
|
||||||
|
|
@ -1182,6 +1235,14 @@ void Device::Initialize(const DeviceFeatures& required, const DeviceFeatures& op
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckVkResult(vkCreateInstance(&instanceCreateInfo, NULL, &instance));
|
CheckVkResult(vkCreateInstance(&instanceCreateInfo, NULL, &instance));
|
||||||
|
if (surfaceMaintenance1Supported) {
|
||||||
|
vkGetPhysicalDeviceSurfaceCapabilities2KHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceCapabilities2KHR>(
|
||||||
|
vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceSurfaceCapabilities2KHR"));
|
||||||
|
surfaceMaintenance1Supported = vkGetPhysicalDeviceSurfaceCapabilities2KHR != nullptr;
|
||||||
|
if (!surfaceMaintenance1Supported) {
|
||||||
|
instanceSurfaceMaintenance1 = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo = {};
|
VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo = {};
|
||||||
debugUtilsMessengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
|
debugUtilsMessengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
|
||||||
|
|
@ -1239,6 +1300,7 @@ void Device::Initialize(const DeviceFeatures& required, const DeviceFeatures& op
|
||||||
rayTracingSupported = chosen.rayTracing;
|
rayTracingSupported = chosen.rayTracing;
|
||||||
deviceFaultSupported = chosen.fault;
|
deviceFaultSupported = chosen.fault;
|
||||||
memoryDecompressionSupported = chosen.decompression;
|
memoryDecompressionSupported = chosen.decompression;
|
||||||
|
swapchainMaintenance1Supported = chosen.swapchainMaintenance1 != nullptr;
|
||||||
workaroundDescriptorHeapAS = chosen.workaroundAS;
|
workaroundDescriptorHeapAS = chosen.workaroundAS;
|
||||||
shaderInt64ForWorkaroundOnly = chosen.workaroundAS
|
shaderInt64ForWorkaroundOnly = chosen.workaroundAS
|
||||||
&& required.vulkan10.shaderInt64 == VK_FALSE && optional.vulkan10.shaderInt64 == VK_FALSE;
|
&& required.vulkan10.shaderInt64 == VK_FALSE && optional.vulkan10.shaderInt64 == VK_FALSE;
|
||||||
|
|
@ -1344,6 +1406,11 @@ void Device::Initialize(const DeviceFeatures& required, const DeviceFeatures& op
|
||||||
.memoryDecompression = VK_TRUE,
|
.memoryDecompression = VK_TRUE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR swapchainMaintenance1Features {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR,
|
||||||
|
.swapchainMaintenance1 = VK_TRUE,
|
||||||
|
};
|
||||||
|
|
||||||
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
|
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
|
||||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
|
||||||
.descriptorHeap = VK_TRUE,
|
.descriptorHeap = VK_TRUE,
|
||||||
|
|
@ -1379,6 +1446,9 @@ void Device::Initialize(const DeviceFeatures& required, const DeviceFeatures& op
|
||||||
if (memoryDecompressionSupported) {
|
if (memoryDecompressionSupported) {
|
||||||
link(memoryDecompressionFeatures);
|
link(memoryDecompressionFeatures);
|
||||||
}
|
}
|
||||||
|
if (swapchainMaintenance1Supported) {
|
||||||
|
link(swapchainMaintenance1Features);
|
||||||
|
}
|
||||||
if (rayTracingSupported) {
|
if (rayTracingSupported) {
|
||||||
link(physicalDeviceRayQueryFeatures);
|
link(physicalDeviceRayQueryFeatures);
|
||||||
link(physicalDeviceRayTracingPipelineFeatures);
|
link(physicalDeviceRayTracingPipelineFeatures);
|
||||||
|
|
@ -1417,6 +1487,9 @@ void Device::Initialize(const DeviceFeatures& required, const DeviceFeatures& op
|
||||||
if (memoryDecompressionSupported) {
|
if (memoryDecompressionSupported) {
|
||||||
enabledDeviceExtensions.push_back(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
|
enabledDeviceExtensions.push_back(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
|
||||||
}
|
}
|
||||||
|
if (swapchainMaintenance1Supported) {
|
||||||
|
enabledDeviceExtensions.push_back(chosen.swapchainMaintenance1);
|
||||||
|
}
|
||||||
|
|
||||||
VkDeviceCreateInfo deviceCreateInfo = {};
|
VkDeviceCreateInfo deviceCreateInfo = {};
|
||||||
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||||||
|
|
@ -1610,9 +1683,11 @@ namespace {
|
||||||
void Device::ReclaimDeletions() {
|
void Device::ReclaimDeletions() {
|
||||||
std::lock_guard lock(deletionMutex);
|
std::lock_guard lock(deletionMutex);
|
||||||
// Free every entry whose retire frame has been reached. An entry tagged at
|
// Free every entry whose retire frame has been reached. An entry tagged at
|
||||||
// frame F retires at F + framesInFlight: by the time the CPU has begun that
|
// frame F retires at F + framesInFlight: Window::Render calls this after
|
||||||
// many later frames (each gated by a per-image fence wait), single-queue
|
// waiting for its frame slot's fence, and that slot's last submission is
|
||||||
// submission order guarantees all GPU work from frame F is complete.
|
// frame F itself once framesInFlight frames have passed, so all GPU work
|
||||||
|
// from frame F is complete (and, by single-queue submission order,
|
||||||
|
// everything before it).
|
||||||
//
|
//
|
||||||
// frameCounter is monotonic, so retireAfter is non-decreasing down the
|
// frameCounter is monotonic, so retireAfter is non-decreasing down the
|
||||||
// queue and the ready entries are a contiguous prefix: pop them off the
|
// queue and the ready entries are a contiguous prefix: pop them off the
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ UIDispatchHeader UIRenderer::FillHeader(std::uint32_t itemBufferSlot,
|
||||||
h.clipH = clipRectPx[3];
|
h.clipH = clipRectPx[3];
|
||||||
h.itemCount = itemCount;
|
h.itemCount = itemCount;
|
||||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
h.frameIdx = window_->currentBuffer;
|
h.frameIdx = window_->currentFrame;
|
||||||
#else
|
#else
|
||||||
h.frameIdx = 0;
|
h.frameIdx = 0;
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,8 @@ void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, Graphi
|
||||||
fontAtlasSamplerSlot_ = RegisterLinearClampSampler();
|
fontAtlasSamplerSlot_ = RegisterLinearClampSampler();
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeSub_.SetEvent(&window.onResize, [this]() {
|
// Nothing to redo on a resize: storage textures and bind groups are
|
||||||
// Storage textures + bind groups are recreated JS-side at next
|
// recreated JS-side at the next wgpuFrameBegin (the ensureSized path).
|
||||||
// wgpuFrameBegin (via the ensureSized path). Nothing to do here.
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UIRenderer::Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
|
void UIRenderer::Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,13 @@ void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, Graphi
|
||||||
drawText.Load(textSpv, heap);
|
drawText.Load(textSpv, heap);
|
||||||
drawFused.Load(fusedSpv, heap);
|
drawFused.Load(fusedSpv, heap);
|
||||||
|
|
||||||
// Allocate one image slot for the swapchain output. Each per-frame heap
|
// One image slot for the swapchain output. The window writes the image
|
||||||
// copy will hold ITS frame's image at this slot.
|
// each frame draws into there, in that frame's heap copy (or set), so it
|
||||||
|
// follows resizes and present-mode changes by itself; called during a
|
||||||
|
// frame, it is written for that frame too.
|
||||||
auto outRange = heap_->AllocateImageSlots(1);
|
auto outRange = heap_->AllocateImageSlots(1);
|
||||||
outImageSlot_ = ImageSlot{heap_, outRange.firstElement};
|
outImageSlot_ = ImageSlot{heap_, outRange.firstElement};
|
||||||
|
heap_->AddSwapchainSlot(outRange.firstElement, window);
|
||||||
WriteSwapchainDescriptors();
|
|
||||||
|
|
||||||
// Optional font-atlas registration (user must have called atlas->Initialize
|
// Optional font-atlas registration (user must have called atlas->Initialize
|
||||||
// already before reaching here, so atlas->image is live).
|
// already before reaching here, so atlas->image is live).
|
||||||
|
|
@ -52,17 +53,9 @@ void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, Graphi
|
||||||
WriteFontAtlasDescriptor();
|
WriteFontAtlasDescriptor();
|
||||||
}
|
}
|
||||||
|
|
||||||
// No whole-heap flush here: WriteSwapchainDescriptors / WriteFontAtlasDescriptor
|
// No whole-heap flush here: WriteFontAtlasDescriptor / RegisterSampler each
|
||||||
// / RegisterSampler each flush their own written byte range above, so the GPU
|
// flush their own written byte range above, so the GPU already sees
|
||||||
// already sees everything written during setup.
|
// everything written during setup.
|
||||||
|
|
||||||
// Re-bind the swapchain image descriptors after every resize. Window
|
|
||||||
// already drained the queue and rebuilt the imageViews[] before
|
|
||||||
// firing the event, so vkWriteResourceDescriptorsEXT is safe here.
|
|
||||||
// WriteSwapchainDescriptors flushes its own range, so no extra flush needed.
|
|
||||||
resizeSub_.SetEvent(&window.onResize, [this]() {
|
|
||||||
WriteSwapchainDescriptors();
|
|
||||||
});
|
|
||||||
|
|
||||||
(void)initCmd; // reserved for future image-layout tweaks
|
(void)initCmd; // reserved for future image-layout tweaks
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +208,7 @@ void UIRenderer::DispatchFused(GraphicsCommandBuffer cmd,
|
||||||
| clipBit(images, 0x4u) | clipBit(text, 0x8u);
|
| clipBit(images, 0x4u) | clipBit(text, 0x8u);
|
||||||
pc.surfaceWidth = window_->width;
|
pc.surfaceWidth = window_->width;
|
||||||
pc.surfaceHeight = window_->height;
|
pc.surfaceHeight = window_->height;
|
||||||
pc.frameIdx = window_->currentBuffer;
|
pc.frameIdx = window_->currentFrame;
|
||||||
pc._pad = 0;
|
pc._pad = 0;
|
||||||
std::copy(quads.clipRectPx.begin(), quads.clipRectPx.end(), pc.clipQuads);
|
std::copy(quads.clipRectPx.begin(), quads.clipRectPx.end(), pc.clipQuads);
|
||||||
std::copy(circles.clipRectPx.begin(), circles.clipRectPx.end(), pc.clipCircles);
|
std::copy(circles.clipRectPx.begin(), circles.clipRectPx.end(), pc.clipCircles);
|
||||||
|
|
@ -254,7 +247,7 @@ void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const GraphicsComputeShader
|
||||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||||
.image = window_->images[window_->currentBuffer],
|
.image = window_->images[window_->currentImage],
|
||||||
.subresourceRange = {
|
.subresourceRange = {
|
||||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||||
.baseMipLevel = 0,
|
.baseMipLevel = 0,
|
||||||
|
|
@ -274,50 +267,6 @@ void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const GraphicsComputeShader
|
||||||
|
|
||||||
// ─── descriptor writes ─────────────────────────────────────────────────
|
// ─── descriptor writes ─────────────────────────────────────────────────
|
||||||
|
|
||||||
void UIRenderer::WriteSwapchainDescriptors() {
|
|
||||||
if (!Device::descriptorHeapSupported) {
|
|
||||||
// Sets mode: each frame's descriptor set holds ITS swapchain image at
|
|
||||||
// outImageSlot_, through a view the heap object creates and owns.
|
|
||||||
for (std::uint8_t f = 0; f < Window::numFrames; ++f) {
|
|
||||||
heap_->WriteImageSet(f, outImageSlot_, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
window_->imageViews[f], VK_IMAGE_LAYOUT_GENERAL);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Each per-frame heap holds ITS swapchain image at outImageSlot_.
|
|
||||||
std::array<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
|
|
||||||
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
|
|
||||||
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
|
|
||||||
|
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
|
||||||
infos[f] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window_->imageViews[f],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
|
||||||
};
|
|
||||||
resources[f] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &infos[f] },
|
|
||||||
};
|
|
||||||
destinations[f] = {
|
|
||||||
.address = heap_->resourceHeap[f].value
|
|
||||||
+ heap_->ImageByteOffset(outImageSlot_),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Device::vkWriteResourceDescriptorsEXT(
|
|
||||||
Device::device, Window::numFrames, resources.data(), destinations.data()
|
|
||||||
);
|
|
||||||
// Only the one image descriptor at outImageSlot_ changed in each per-frame
|
|
||||||
// heap (same slot offset in every frame), so flush just that byte range
|
|
||||||
// rather than the whole multi-KB heap.
|
|
||||||
const std::uint32_t off = heap_->ImageByteOffset(outImageSlot_);
|
|
||||||
const std::uint32_t sz = Device::descriptorHeapProperties.imageDescriptorSize;
|
|
||||||
for (auto& h : heap_->resourceHeap) h.FlushDevice(off, sz);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UIRenderer::WriteFontAtlasDescriptor() {
|
void UIRenderer::WriteFontAtlasDescriptor() {
|
||||||
atlasViewCreateInfo_ = {
|
atlasViewCreateInfo_ = {
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||||
|
|
|
||||||
|
|
@ -538,20 +538,18 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
||||||
Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &cmdBufAllocateInfo, drawCmdBuffers));
|
Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &cmdBufAllocateInfo, drawCmdBuffers));
|
||||||
|
|
||||||
// Per-frame-in-flight synchronisation objects (issue #40). The fences are
|
// Per-frame-in-flight synchronisation objects (issue #40). The fences are
|
||||||
// created signaled so the first wait on each image's fence — before that
|
// created signaled so the first wait on each frame slot's fence — before
|
||||||
// image has ever been submitted — returns immediately instead of
|
// that slot has ever been submitted — returns immediately instead of
|
||||||
// deadlocking. The submitInfo is rebuilt per frame in Render() now that
|
// deadlocking. The submitInfo is rebuilt per frame in Render() now that
|
||||||
// the wait/signal semaphores vary by frame.
|
// the wait/signal semaphores vary by frame. The per-image render-finished
|
||||||
|
// semaphores come with the images, in CreateSwapchain.
|
||||||
VkSemaphoreCreateInfo semaphoreCreateInfo {};
|
VkSemaphoreCreateInfo semaphoreCreateInfo {};
|
||||||
semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
|
semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
|
||||||
VkFenceCreateInfo fenceCreateInfo {};
|
VkFenceCreateInfo fenceCreateInfo {};
|
||||||
fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||||
fenceCreateInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
|
fenceCreateInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
|
||||||
for (std::uint8_t i = 0; i < numFrames; i++) {
|
for (std::uint8_t i = 0; i < numFrames; i++) {
|
||||||
Device::CheckVkResult(vkCreateSemaphore(Device::device, &semaphoreCreateInfo, nullptr, &renderFinishedSemaphores[i]));
|
|
||||||
Device::CheckVkResult(vkCreateFence(Device::device, &fenceCreateInfo, nullptr, &waitFences[i]));
|
Device::CheckVkResult(vkCreateFence(Device::device, &fenceCreateInfo, nullptr, &waitFences[i]));
|
||||||
}
|
|
||||||
for (std::uint8_t i = 0; i < numAcquireSemaphores; i++) {
|
|
||||||
Device::CheckVkResult(vkCreateSemaphore(Device::device, &semaphoreCreateInfo, nullptr, &imageAcquiredSemaphores[i]));
|
Device::CheckVkResult(vkCreateSemaphore(Device::device, &semaphoreCreateInfo, nullptr, &imageAcquiredSemaphores[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -562,7 +560,7 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
||||||
|
|
||||||
// Per-frame info structs: everything that never varies between frames is
|
// Per-frame info structs: everything that never varies between frames is
|
||||||
// set here once. Render() patches the barriers' image/oldLayout and the
|
// set here once. Render() patches the barriers' image/oldLayout and the
|
||||||
// sync handles that follow currentBuffer/frameCounter (submitInfo's
|
// sync handles that follow currentFrame/currentImage (submitInfo's
|
||||||
// wait/signal semaphores + command buffer, presentInfo's wait semaphore).
|
// wait/signal semaphores + command buffer, presentInfo's wait semaphore).
|
||||||
cmdBufInfo = {};
|
cmdBufInfo = {};
|
||||||
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||||
|
|
@ -617,15 +615,15 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
||||||
};
|
};
|
||||||
|
|
||||||
// pImageIndices/pSwapchains point at members, so they follow value changes
|
// pImageIndices/pSwapchains point at members, so they follow value changes
|
||||||
// (currentBuffer each frame, swapChain across recreation) on their own. The
|
// (currentImage each frame, swapChain across recreation) on their own. The
|
||||||
// wait semaphore is the per-image render-finished semaphore, patched per
|
// wait semaphore is the per-image render-finished semaphore, patched per
|
||||||
// frame in Render() (it follows currentBuffer).
|
// frame in Render() (it follows currentImage).
|
||||||
presentInfo = {};
|
presentInfo = {};
|
||||||
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
|
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
|
||||||
presentInfo.pNext = NULL;
|
presentInfo.pNext = NULL;
|
||||||
presentInfo.swapchainCount = 1;
|
presentInfo.swapchainCount = 1;
|
||||||
presentInfo.pSwapchains = &swapChain;
|
presentInfo.pSwapchains = &swapChain;
|
||||||
presentInfo.pImageIndices = ¤tBuffer;
|
presentInfo.pImageIndices = ¤tImage;
|
||||||
presentInfo.waitSemaphoreCount = 1;
|
presentInfo.waitSemaphoreCount = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -636,12 +634,17 @@ Window::~Window() {
|
||||||
if (Device::device != VK_NULL_HANDLE) {
|
if (Device::device != VK_NULL_HANDLE) {
|
||||||
if (drawCmdBuffers[0] != VK_NULL_HANDLE) vkFreeCommandBuffers(Device::device, Device::commandPool, numFrames, drawCmdBuffers);
|
if (drawCmdBuffers[0] != VK_NULL_HANDLE) vkFreeCommandBuffers(Device::device, Device::commandPool, numFrames, drawCmdBuffers);
|
||||||
for (std::uint8_t i = 0; i < numFrames; i++) {
|
for (std::uint8_t i = 0; i < numFrames; i++) {
|
||||||
if (renderFinishedSemaphores[i]) vkDestroySemaphore(Device::device, renderFinishedSemaphores[i], nullptr);
|
|
||||||
if (waitFences[i]) vkDestroyFence(Device::device, waitFences[i], nullptr);
|
if (waitFences[i]) vkDestroyFence(Device::device, waitFences[i], nullptr);
|
||||||
}
|
|
||||||
for (std::uint8_t i = 0; i < numAcquireSemaphores; i++) {
|
|
||||||
if (imageAcquiredSemaphores[i]) vkDestroySemaphore(Device::device, imageAcquiredSemaphores[i], nullptr);
|
if (imageAcquiredSemaphores[i]) vkDestroySemaphore(Device::device, imageAcquiredSemaphores[i], nullptr);
|
||||||
}
|
}
|
||||||
|
for (VkSemaphore semaphore : renderFinishedSemaphores) {
|
||||||
|
vkDestroySemaphore(Device::device, semaphore, nullptr);
|
||||||
|
}
|
||||||
|
renderFinishedSemaphores.clear();
|
||||||
|
for (VkImageView view : imageViews) {
|
||||||
|
vkDestroyImageView(Device::device, view, nullptr);
|
||||||
|
}
|
||||||
|
imageViews.clear();
|
||||||
if (swapChain != VK_NULL_HANDLE) vkDestroySwapchainKHR(Device::device, swapChain, nullptr);
|
if (swapChain != VK_NULL_HANDLE) vkDestroySwapchainKHR(Device::device, swapChain, nullptr);
|
||||||
swapChain = VK_NULL_HANDLE;
|
swapChain = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
|
|
@ -726,6 +729,15 @@ void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) {
|
||||||
// Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland
|
// Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland
|
||||||
// sometimes echoes the current size in a configure.
|
// sometimes echoes the current size in a configure.
|
||||||
if (newWidth == 0 || newHeight == 0) return;
|
if (newWidth == 0 || newHeight == 0) return;
|
||||||
|
// A listener of the frame being recorded asked: the rest of that frame
|
||||||
|
// still draws into an image of the current swapchain at the current
|
||||||
|
// size, so the size changes once the frame has been presented. The last
|
||||||
|
// size asked for wins, the current one too.
|
||||||
|
if (recording) {
|
||||||
|
deferredWidth = newWidth;
|
||||||
|
deferredHeight = newHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (newWidth == width && newHeight == height) return;
|
if (newWidth == width && newHeight == height) return;
|
||||||
// Win32 fires WM_SIZE synchronously inside CreateWindowEx (before the
|
// Win32 fires WM_SIZE synchronously inside CreateWindowEx (before the
|
||||||
// constructor's CreateSwapchain). Defer the first resize to that
|
// constructor's CreateSwapchain). Defer the first resize to that
|
||||||
|
|
@ -739,9 +751,13 @@ void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) {
|
||||||
|
|
||||||
width = newWidth;
|
width = newWidth;
|
||||||
height = newHeight;
|
height = newHeight;
|
||||||
|
RebuildSwapchain();
|
||||||
|
}
|
||||||
|
|
||||||
// Caller (configure handler / WM_SIZE) runs between frames, but be
|
void Window::RebuildSwapchain() {
|
||||||
// defensive: ensure no in-flight commands reference the old swapchain.
|
// Configure handlers and WM_SIZE run between frames, and a frame that
|
||||||
|
// asked waited for its present, but frames may still be in flight: none
|
||||||
|
// may still use the old swapchain.
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
// The queue is idle, so every deferred resource is now safe to free
|
// The queue is idle, so every deferred resource is now safe to free
|
||||||
// regardless of its retire frame (issue #101).
|
// regardless of its retire frame (issue #101).
|
||||||
|
|
@ -771,15 +787,17 @@ void Window::RecreateSwapchainAndImages() {
|
||||||
|
|
||||||
void Window::SetPresentMode(PresentMode mode) {
|
void Window::SetPresentMode(PresentMode mode) {
|
||||||
presentMode = mode;
|
presentMode = mode;
|
||||||
presentModeUnfit = false;
|
|
||||||
// A window without a swapchain yet picks the mode up when it makes one.
|
// A window without a swapchain yet picks the mode up when it makes one.
|
||||||
if (swapChain == VK_NULL_HANDLE) return;
|
if (swapChain == VK_NULL_HANDLE) return;
|
||||||
// The mode is fixed when a swapchain is made, so make a new one, as
|
// The mode is fixed when a swapchain is made, so make a new one, as
|
||||||
// Resize does: nothing in flight may still use the old images.
|
// Resize does: after the present when a frame is being recorded (a
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
// VSync key ticked from onUpdate), since the rest of that frame draws
|
||||||
Device::DrainDeletions();
|
// into the old swapchain's image.
|
||||||
RecreateSwapchainAndImages();
|
if (recording) {
|
||||||
onResize.Invoke();
|
swapchainRebuildPending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RebuildSwapchain();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Window::SetTitle(const std::string_view title) {
|
void Window::SetTitle(const std::string_view title) {
|
||||||
|
|
@ -1255,37 +1273,61 @@ namespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Window::Render() {
|
void Window::Render() {
|
||||||
|
// This frame's slot among the numFrames in flight. Its command buffer,
|
||||||
|
// descriptor-heap copy and acquire semaphore were last used numFrames
|
||||||
|
// frames ago: wait for that submission's fence before touching any of
|
||||||
|
// them. This — not a full-queue wait-idle — is what bounds
|
||||||
|
// frames-in-flight and gives the CPU/GPU overlap. The wait comes before
|
||||||
|
// the acquire and is keyed by frame, not by the acquired image as when
|
||||||
|
// the two were one index, so the swapchain may have any number of images
|
||||||
|
// (#160) and a frame still waits for exactly the one numFrames before it,
|
||||||
|
// which the deletion queue and the staging rings count on.
|
||||||
|
currentFrame = static_cast<std::uint32_t>(frameCounter % numFrames);
|
||||||
|
Device::CheckVkResult(vkWaitForFences(Device::device, 1, &waitFences[currentFrame], VK_TRUE, UINT64_MAX));
|
||||||
|
|
||||||
// Acquire the next image from the swap chain. If the surface has
|
// Acquire the next image from the swap chain. If the surface has
|
||||||
// changed size out from under us (compositor/Win32 resize delivered
|
// changed size out from under us (compositor/Win32 resize delivered
|
||||||
// between Render calls), recreate and retry once.
|
// between Render calls), recreate and retry once.
|
||||||
// Pick this frame's acquire semaphore from a free-running CPU counter: the
|
VkSemaphore imageAcquired = imageAcquiredSemaphores[currentFrame];
|
||||||
// image index (currentBuffer) isn't known until acquire returns, so the
|
|
||||||
// acquire's signal semaphore can't be keyed by the image.
|
|
||||||
const std::uint32_t acquireSlot = static_cast<std::uint32_t>(frameCounter % numAcquireSemaphores);
|
|
||||||
VkSemaphore imageAcquired = imageAcquiredSemaphores[acquireSlot];
|
|
||||||
{
|
{
|
||||||
VkResult acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
|
VkResult acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
|
||||||
imageAcquired, (VkFence)nullptr, ¤tBuffer);
|
imageAcquired, (VkFence)nullptr, ¤tImage);
|
||||||
if (acquire == VK_ERROR_OUT_OF_DATE_KHR) {
|
if (acquire == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
Device::DrainDeletions();
|
Device::DrainDeletions();
|
||||||
RecreateSwapchainAndImages();
|
RecreateSwapchainAndImages();
|
||||||
onResize.Invoke();
|
onResize.Invoke();
|
||||||
acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
|
acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
|
||||||
imageAcquired, (VkFence)nullptr, ¤tBuffer);
|
imageAcquired, (VkFence)nullptr, ¤tImage);
|
||||||
}
|
}
|
||||||
if (acquire != VK_SUBOPTIMAL_KHR) {
|
if (acquire != VK_SUBOPTIMAL_KHR) {
|
||||||
Device::CheckVkResult(acquire);
|
Device::CheckVkResult(acquire);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawCmdBuffers[currentBuffer] and this image's descriptor-heap slot are
|
// From here to the present the frame owns images[currentImage] and its
|
||||||
// about to be re-recorded. Wait on the fence guarding this image's
|
// heap copy: a Resize or SetPresentMode from a listener waits for the
|
||||||
// previous submission (keyed by image index, since acquire returns an
|
// present (see the end of this function), and a swapchain slot listed
|
||||||
// arbitrary index), then reset it for this submit. This — not a full-queue
|
// now is written at once. The guard clears it on the way out of a throw
|
||||||
// wait-idle — is what bounds frames-in-flight and gives the CPU/GPU overlap.
|
// too, so a failed frame doesn't hold every later rebuild back.
|
||||||
Device::CheckVkResult(vkWaitForFences(Device::device, 1, &waitFences[currentBuffer], VK_TRUE, UINT64_MAX));
|
recording = true;
|
||||||
Device::CheckVkResult(vkResetFences(Device::device, 1, &waitFences[currentBuffer]));
|
struct RecordingGuard {
|
||||||
|
bool& recording;
|
||||||
|
~RecordingGuard() { recording = false; }
|
||||||
|
} recordingGuard { recording };
|
||||||
|
|
||||||
|
// The image this frame draws into, at this window's swapchain slots, in
|
||||||
|
// this frame's copy of the heap (or its descriptor set), which the fence
|
||||||
|
// wait above freed. Before onUpdate and the passes, which record against
|
||||||
|
// it.
|
||||||
|
if (descriptorHeap) {
|
||||||
|
descriptorHeap->WriteSwapchainImage(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset for this frame's submit after the acquire, which can fail (and
|
||||||
|
// recreate the swapchain on the way), and the slot writes: a fence reset
|
||||||
|
// and then never submitted would hang the next wait on this slot.
|
||||||
|
Device::CheckVkResult(vkResetFences(Device::device, 1, &waitFences[currentFrame]));
|
||||||
|
|
||||||
// Now that a frame's fence has been waited, free any deferred resources
|
// Now that a frame's fence has been waited, free any deferred resources
|
||||||
// whose retire frame has been reached (issue #101). frameCounter is bumped
|
// whose retire frame has been reached (issue #101). frameCounter is bumped
|
||||||
|
|
@ -1293,19 +1335,19 @@ void Window::Render() {
|
||||||
// value entries enqueued this frame are tagged with.
|
// value entries enqueued this frame are tagged with.
|
||||||
Device::ReclaimDeletions();
|
Device::ReclaimDeletions();
|
||||||
|
|
||||||
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));
|
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentFrame], &cmdBufInfo));
|
||||||
|
|
||||||
// On an image's first use after (re)creating the swapchain it is still in
|
// On an image's first use after (re)creating the swapchain it is still in
|
||||||
// VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in
|
// VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in
|
||||||
// PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation
|
// PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation
|
||||||
// error, so pick based on whether this image has been rendered before.
|
// error, so pick based on whether this image has been rendered before.
|
||||||
// The image was just acquired above, so touching it here is legal.
|
// The image was just acquired above, so touching it here is legal.
|
||||||
const bool firstUse = !imageInitialised[currentBuffer];
|
const bool firstUse = !imageInitialised[currentImage];
|
||||||
imageInitialised[currentBuffer] = true;
|
imageInitialised[currentImage] = true;
|
||||||
|
|
||||||
acquireBarrier.oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED
|
acquireBarrier.oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED
|
||||||
: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||||
acquireBarrier.image = images[currentBuffer];
|
acquireBarrier.image = images[currentImage];
|
||||||
|
|
||||||
// Real per-pass stage union for this frame's passes — the swapchain image
|
// Real per-pass stage union for this frame's passes — the swapchain image
|
||||||
// is only ever written by compute/RT (and possibly transfer), never by the
|
// is only ever written by compute/RT (and possibly transfer), never by the
|
||||||
|
|
@ -1327,7 +1369,7 @@ void Window::Render() {
|
||||||
|
|
||||||
// The acquire->GENERAL transition only needs to be visible before the first
|
// The acquire->GENERAL transition only needs to be visible before the first
|
||||||
// pass writes the image, i.e. at the writer stages — not ALL_COMMANDS.
|
// pass writes the image, i.e. at the writer stages — not ALL_COMMANDS.
|
||||||
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, swapWriterStages, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier);
|
vkCmdPipelineBarrier(drawCmdBuffers[currentFrame], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, swapWriterStages, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier);
|
||||||
|
|
||||||
// Synthesise key-repeat events before listeners run, so the focused
|
// Synthesise key-repeat events before listeners run, so the focused
|
||||||
// widget's OnTextInput / OnKeyDown sees them in the same frame.
|
// widget's OnTextInput / OnKeyDown sees them in the same frame.
|
||||||
|
|
@ -1340,7 +1382,7 @@ void Window::Render() {
|
||||||
// Pass-side dispatches still run with the same heaps bound — moving
|
// Pass-side dispatches still run with the same heaps bound — moving
|
||||||
// the bind earlier doesn't change anything for them.
|
// the bind earlier doesn't change anything for them.
|
||||||
if (descriptorHeap) {
|
if (descriptorHeap) {
|
||||||
descriptorHeap->Bind(drawCmdBuffers[currentBuffer], currentBuffer);
|
descriptorHeap->Bind(drawCmdBuffers[currentFrame], currentFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate.Invoke({startTime, startTime-lastFrameBegin});
|
onUpdate.Invoke({startTime, startTime-lastFrameBegin});
|
||||||
|
|
@ -1360,7 +1402,7 @@ void Window::Render() {
|
||||||
(void)clearColor;
|
(void)clearColor;
|
||||||
|
|
||||||
for (std::size_t i = 0; i < passes.size(); ++i) {
|
for (std::size_t i = 0; i < passes.size(); ++i) {
|
||||||
passes[i]->Record(drawCmdBuffers[currentBuffer], currentBuffer, *this);
|
passes[i]->Record(drawCmdBuffers[currentFrame], currentFrame, *this);
|
||||||
|
|
||||||
if (i + 1 < passes.size()) {
|
if (i + 1 < passes.size()) {
|
||||||
// Make pass i's writes to the swapchain image visible to pass i+1.
|
// Make pass i's writes to the swapchain image visible to pass i+1.
|
||||||
|
|
@ -1376,13 +1418,13 @@ void Window::Render() {
|
||||||
// default stage reports a union that names RAY_TRACING_SHADER.
|
// default stage reports a union that names RAY_TRACING_SHADER.
|
||||||
const VkPipelineStageFlags srcStages = passes[i]->SwapchainStage() & Device::stageMask;
|
const VkPipelineStageFlags srcStages = passes[i]->SwapchainStage() & Device::stageMask;
|
||||||
const VkPipelineStageFlags dstStages = passes[i + 1]->SwapchainStage() & Device::stageMask;
|
const VkPipelineStageFlags dstStages = passes[i + 1]->SwapchainStage() & Device::stageMask;
|
||||||
VkImageMemoryBarrier ib = BuildSwapchainInterPassBarrier(images[currentBuffer], srcStages, dstStages);
|
VkImageMemoryBarrier ib = BuildSwapchainInterPassBarrier(images[currentImage], srcStages, dstStages);
|
||||||
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], srcStages, dstStages,
|
vkCmdPipelineBarrier(drawCmdBuffers[currentFrame], srcStages, dstStages,
|
||||||
0, 0, nullptr, 0, nullptr, 1, &ib);
|
0, 0, nullptr, 0, nullptr, 1, &ib);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
presentBarrier.image = images[currentBuffer];
|
presentBarrier.image = images[currentImage];
|
||||||
|
|
||||||
// Mirror of the acquire barrier: srcAccessMask must be supported by the src
|
// Mirror of the acquire barrier: srcAccessMask must be supported by the src
|
||||||
// stage mask (swapWriterStages). SHADER_WRITE alone is valid for COMPUTE/RT
|
// stage mask (swapWriterStages). SHADER_WRITE alone is valid for COMPUTE/RT
|
||||||
|
|
@ -1395,7 +1437,7 @@ void Window::Render() {
|
||||||
std::optional<FrameCapture> capture;
|
std::optional<FrameCapture> capture;
|
||||||
VkPipelineStageFlags presentWaitStages = swapWriterStages;
|
VkPipelineStageFlags presentWaitStages = swapWriterStages;
|
||||||
if (pendingCapture) {
|
if (pendingCapture) {
|
||||||
capture.emplace(RecordFrameCapture(drawCmdBuffers[currentBuffer], images[currentBuffer], width, height, swapWriterStages, presentBarrier.srcAccessMask));
|
capture.emplace(RecordFrameCapture(drawCmdBuffers[currentFrame], images[currentImage], width, height, swapWriterStages, presentBarrier.srcAccessMask));
|
||||||
presentWaitStages |= VK_PIPELINE_STAGE_TRANSFER_BIT;
|
presentWaitStages |= VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
capture->path = std::move(*pendingCapture);
|
capture->path = std::move(*pendingCapture);
|
||||||
pendingCapture.reset();
|
pendingCapture.reset();
|
||||||
|
|
@ -1404,36 +1446,31 @@ void Window::Render() {
|
||||||
// The ->PRESENT_SRC transition only needs to wait on the stages that
|
// The ->PRESENT_SRC transition only needs to wait on the stages that
|
||||||
// actually wrote the image (the per-pass union), not ALL_COMMANDS; present
|
// actually wrote the image (the per-pass union), not ALL_COMMANDS; present
|
||||||
// itself is gated by the render-finished semaphore, so dst stays BOTTOM.
|
// itself is gated by the render-finished semaphore, so dst stays BOTTOM.
|
||||||
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], presentWaitStages, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier);
|
vkCmdPipelineBarrier(drawCmdBuffers[currentFrame], presentWaitStages, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier);
|
||||||
|
|
||||||
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
|
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentFrame]));
|
||||||
|
|
||||||
// Patch the per-frame fields of the cached submitInfo/presentInfo (the
|
// Patch the per-frame fields of the cached submitInfo/presentInfo (the
|
||||||
// invariant fields were set once in the constructor): submit waits on this
|
// invariant fields were set once in the constructor): submit waits on this
|
||||||
// frame's acquire semaphore and signals this image's render-finished
|
// frame's acquire semaphore and signals this image's render-finished
|
||||||
// semaphore, present waits on that same render-finished semaphore. The
|
// semaphore, present waits on that same render-finished semaphore. The
|
||||||
// fence (keyed by image index) lets a future Render() reuse this image's
|
// fence (keyed by frame slot) lets the Render() numFrames later reuse this
|
||||||
// command buffer + heap slot safely. The semaphore handles live in member
|
// slot's command buffer, heap copy and acquire semaphore safely. The
|
||||||
// arrays, so taking their addresses here is stable for the call.
|
// semaphore handles live in member arrays, so taking their addresses here
|
||||||
submitInfo.pWaitSemaphores = &imageAcquiredSemaphores[acquireSlot];
|
// is stable for the call.
|
||||||
submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentBuffer];
|
submitInfo.pWaitSemaphores = &imageAcquiredSemaphores[currentFrame];
|
||||||
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
|
submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentImage];
|
||||||
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, waitFences[currentBuffer]));
|
submitInfo.pCommandBuffers = &drawCmdBuffers[currentFrame];
|
||||||
|
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, waitFences[currentFrame]));
|
||||||
|
|
||||||
presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentBuffer];
|
presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentImage];
|
||||||
|
|
||||||
VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo);
|
VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo);
|
||||||
|
// The frame is handed over: a swapchain rebuild may go ahead now.
|
||||||
|
recording = false;
|
||||||
if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) {
|
if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||||
// Surface size changed mid-present. Drain the queue (the only place we
|
// Surface size changed mid-present: rebuild below.
|
||||||
// still wait-idle, alongside Resize() and the acquire OUT_OF_DATE
|
swapchainRebuildPending = true;
|
||||||
// path), rebuild the swapchain, and let dependents (descriptors
|
|
||||||
// holding old image handles) re-bind via onResize before the next
|
|
||||||
// frame. The wait-idle here also re-settles every per-frame fence to a
|
|
||||||
// signaled state, so the next Render()'s fence wait passes through.
|
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
|
||||||
Device::DrainDeletions();
|
|
||||||
RecreateSwapchainAndImages();
|
|
||||||
onResize.Invoke();
|
|
||||||
} else {
|
} else {
|
||||||
Device::CheckVkResult(result);
|
Device::CheckVkResult(result);
|
||||||
}
|
}
|
||||||
|
|
@ -1442,8 +1479,8 @@ void Window::Render() {
|
||||||
// while the GPU is still executing frame N.
|
// while the GPU is still executing frame N.
|
||||||
frameCounter++;
|
frameCounter++;
|
||||||
// Advance the global deferred-deletion clock too (issue #101). Kept
|
// Advance the global deferred-deletion clock too (issue #101). Kept
|
||||||
// separate from the per-window frameCounter above (which also keys the
|
// separate from the per-window frameCounter above (which also picks the
|
||||||
// acquire-semaphore slot) so non-window code can enqueue against it.
|
// frame slot) so non-window code can enqueue against it.
|
||||||
Device::frameCounter++;
|
Device::frameCounter++;
|
||||||
|
|
||||||
if (capture) {
|
if (capture) {
|
||||||
|
|
@ -1452,6 +1489,25 @@ void Window::Render() {
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
WriteFrameCapture(*capture, colorFormat);
|
WriteFrameCapture(*capture, colorFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The rebuild the frame held back: the present's, or a Resize or
|
||||||
|
// SetPresentMode from one of its listeners. RebuildSwapchain drains the
|
||||||
|
// queue (the only other wait-idles are Resize's and SetPresentMode's own
|
||||||
|
// and the acquire's OUT_OF_DATE path) and fires onResize before the next
|
||||||
|
// frame; the wait also leaves every per-frame fence signaled, so the next
|
||||||
|
// Render()'s fence wait passes through.
|
||||||
|
bool rebuild = swapchainRebuildPending;
|
||||||
|
if (deferredWidth != 0) {
|
||||||
|
rebuild = rebuild || deferredWidth != width || deferredHeight != height;
|
||||||
|
width = deferredWidth;
|
||||||
|
height = deferredHeight;
|
||||||
|
deferredWidth = 0;
|
||||||
|
deferredHeight = 0;
|
||||||
|
}
|
||||||
|
if (rebuild) {
|
||||||
|
swapchainRebuildPending = false;
|
||||||
|
RebuildSwapchain();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CRAFTER_TIMING
|
#ifdef CRAFTER_TIMING
|
||||||
|
|
@ -1493,14 +1549,86 @@ void Window::LogTiming() {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
VkSurfaceCapabilitiesKHR Window::SwapchainCapabilities(VkPhysicalDevice gpu, VkSurfaceKHR surface,
|
||||||
|
VkPresentModeKHR mode, bool modePassedAlong) {
|
||||||
|
VkSurfaceCapabilitiesKHR allModes {};
|
||||||
|
if (!Device::surfaceMaintenance1Supported || !modePassedAlong) {
|
||||||
|
Device::CheckVkResult(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(gpu, surface, &allModes));
|
||||||
|
if (!Device::surfaceMaintenance1Supported) {
|
||||||
|
return allModes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VkSurfacePresentModeKHR modeInfo {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_SURFACE_PRESENT_MODE_KHR,
|
||||||
|
.presentMode = mode,
|
||||||
|
};
|
||||||
|
const VkPhysicalDeviceSurfaceInfo2KHR surfaceInfo {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SURFACE_INFO_2_KHR,
|
||||||
|
.pNext = &modeInfo,
|
||||||
|
.surface = surface,
|
||||||
|
};
|
||||||
|
VkSurfaceCapabilities2KHR forMode { .sType = VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_2_KHR };
|
||||||
|
Device::CheckVkResult(Device::vkGetPhysicalDeviceSurfaceCapabilities2KHR(gpu, &surfaceInfo, &forMode));
|
||||||
|
VkSurfaceCapabilitiesKHR capabilities = forMode.surfaceCapabilities;
|
||||||
|
if (!modePassedAlong) {
|
||||||
|
// VUID-VkSwapchainCreateInfoKHR-presentMode-02839 and the
|
||||||
|
// forward-progress rule of a UINT64_MAX acquire count against the
|
||||||
|
// surface's minimum for all modes unless the swapchain names its
|
||||||
|
// mode, so that minimum binds too.
|
||||||
|
capabilities.minImageCount = std::max(capabilities.minImageCount, allModes.minImageCount);
|
||||||
|
if (allModes.maxImageCount != 0) {
|
||||||
|
capabilities.maxImageCount = capabilities.maxImageCount == 0
|
||||||
|
? allModes.maxImageCount
|
||||||
|
: std::min(capabilities.maxImageCount, allModes.maxImageCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t Window::SwapchainMinImageCount(const VkSurfaceCapabilitiesKHR& capabilities) {
|
||||||
|
std::uint32_t count = std::max<std::uint32_t>(numFrames, capabilities.minImageCount);
|
||||||
|
if (capabilities.maxImageCount != 0) {
|
||||||
|
count = std::min(count, capabilities.maxImageCount);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
void Window::CreateSwapchain()
|
void Window::CreateSwapchain()
|
||||||
{
|
{
|
||||||
// Store the current swap chain handle so we can use it later on to ease up recreation
|
// Store the current swap chain handle so we can use it later on to ease up recreation
|
||||||
VkSwapchainKHR oldSwapchain = swapChain;
|
VkSwapchainKHR oldSwapchain = swapChain;
|
||||||
|
|
||||||
// Get physical device surface properties and formats
|
// VK_PRESENT_MODE_FIFO_KHR ("v-sync") is guaranteed available per spec, so
|
||||||
VkSurfaceCapabilitiesKHR surfCaps;
|
// it is the fallback for a mode the surface lacks. For benchmarking we
|
||||||
Device::CheckVkResult(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device::physDevice, vulkanSurface, &surfCaps));
|
// want frame pacing uncapped from the display refresh, so when
|
||||||
|
// CRAFTER_PRESENT_IMMEDIATE is set in the environment we prefer IMMEDIATE
|
||||||
|
// (then MAILBOX) whatever presentMode asks for. Chosen first, because how
|
||||||
|
// many images the swapchain needs depends on it.
|
||||||
|
{
|
||||||
|
std::uint32_t presentModeCount = 0;
|
||||||
|
vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, nullptr);
|
||||||
|
std::vector<VkPresentModeKHR> presentModes(presentModeCount);
|
||||||
|
vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, presentModes.data());
|
||||||
|
auto offered = [&](VkPresentModeKHR mode) { return std::ranges::find(presentModes, mode) != presentModes.end(); };
|
||||||
|
swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR;
|
||||||
|
if (std::getenv("CRAFTER_PRESENT_IMMEDIATE")) {
|
||||||
|
if (offered(VK_PRESENT_MODE_IMMEDIATE_KHR)) swapchainPresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR;
|
||||||
|
else if (offered(VK_PRESENT_MODE_MAILBOX_KHR)) swapchainPresentMode = VK_PRESENT_MODE_MAILBOX_KHR;
|
||||||
|
} else if (presentMode == PresentMode::Mailbox && offered(VK_PRESENT_MODE_MAILBOX_KHR)) {
|
||||||
|
swapchainPresentMode = VK_PRESENT_MODE_MAILBOX_KHR;
|
||||||
|
} else if (presentMode == PresentMode::Immediate && offered(VK_PRESENT_MODE_IMMEDIATE_KHR)) {
|
||||||
|
swapchainPresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get physical device surface properties, for that present mode. With
|
||||||
|
// swapchain_maintenance1 the swapchain names its mode, so that mode's
|
||||||
|
// image counts are the ones that apply, which on Mesa's Wayland are below
|
||||||
|
// the surface's for all modes (FIFO three where MAILBOX needs four, #160).
|
||||||
|
// This is the last capabilities query before vkCreateSwapchainKHR, as the
|
||||||
|
// validation layer checks minImageCount against the surface's last answer.
|
||||||
|
const VkSurfaceCapabilitiesKHR surfCaps = SwapchainCapabilities(Device::physDevice, vulkanSurface,
|
||||||
|
swapchainPresentMode, Device::swapchainMaintenance1Supported);
|
||||||
|
|
||||||
VkExtent2D swapchainExtent = {};
|
VkExtent2D swapchainExtent = {};
|
||||||
// If width (and height) equals the special value 0xFFFFFFFF, the size of the surface will be set by the swapchain
|
// If width (and height) equals the special value 0xFFFFFFFF, the size of the surface will be set by the swapchain
|
||||||
|
|
@ -1534,7 +1662,7 @@ void Window::CreateSwapchain()
|
||||||
VkSwapchainCreateInfoKHR swapchainCI = {};
|
VkSwapchainCreateInfoKHR swapchainCI = {};
|
||||||
swapchainCI.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
|
swapchainCI.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
|
||||||
swapchainCI.surface = vulkanSurface;
|
swapchainCI.surface = vulkanSurface;
|
||||||
swapchainCI.minImageCount = numFrames;
|
swapchainCI.minImageCount = SwapchainMinImageCount(surfCaps);
|
||||||
swapchainCI.imageFormat = colorFormat;
|
swapchainCI.imageFormat = colorFormat;
|
||||||
swapchainCI.imageColorSpace = colorSpace;
|
swapchainCI.imageColorSpace = colorSpace;
|
||||||
swapchainCI.imageExtent = { swapchainExtent.width, swapchainExtent.height };
|
swapchainCI.imageExtent = { swapchainExtent.width, swapchainExtent.height };
|
||||||
|
|
@ -1545,75 +1673,66 @@ void Window::CreateSwapchain()
|
||||||
swapchainCI.imageArrayLayers = 1;
|
swapchainCI.imageArrayLayers = 1;
|
||||||
swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
swapchainCI.queueFamilyIndexCount = 0;
|
swapchainCI.queueFamilyIndexCount = 0;
|
||||||
// VK_PRESENT_MODE_FIFO_KHR ("v-sync") is guaranteed available per spec, so
|
|
||||||
// it is the fallback for a mode the surface lacks. For benchmarking we
|
|
||||||
// want frame pacing uncapped from the display refresh, so when
|
|
||||||
// CRAFTER_PRESENT_IMMEDIATE is set in the environment we prefer IMMEDIATE
|
|
||||||
// (then MAILBOX) whatever presentMode asks for.
|
|
||||||
{
|
|
||||||
std::uint32_t presentModeCount = 0;
|
|
||||||
vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, nullptr);
|
|
||||||
std::vector<VkPresentModeKHR> presentModes(presentModeCount);
|
|
||||||
vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, vulkanSurface, &presentModeCount, presentModes.data());
|
|
||||||
auto offered = [&](VkPresentModeKHR mode) { return std::ranges::find(presentModes, mode) != presentModes.end(); };
|
|
||||||
swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR;
|
|
||||||
if (presentModeUnfit) {
|
|
||||||
// Stays FIFO; see below.
|
|
||||||
} else if (std::getenv("CRAFTER_PRESENT_IMMEDIATE")) {
|
|
||||||
if (offered(VK_PRESENT_MODE_IMMEDIATE_KHR)) swapchainPresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR;
|
|
||||||
else if (offered(VK_PRESENT_MODE_MAILBOX_KHR)) swapchainPresentMode = VK_PRESENT_MODE_MAILBOX_KHR;
|
|
||||||
} else if (presentMode == PresentMode::Mailbox && offered(VK_PRESENT_MODE_MAILBOX_KHR)) {
|
|
||||||
swapchainPresentMode = VK_PRESENT_MODE_MAILBOX_KHR;
|
|
||||||
} else if (presentMode == PresentMode::Immediate && offered(VK_PRESENT_MODE_IMMEDIATE_KHR)) {
|
|
||||||
swapchainPresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
swapchainCI.presentMode = swapchainPresentMode;
|
swapchainCI.presentMode = swapchainPresentMode;
|
||||||
|
// The mode, named again for swapchain_maintenance1: what makes the mode's
|
||||||
|
// own image counts the ones that apply, at creation and to the acquires.
|
||||||
|
const VkSwapchainPresentModesCreateInfoKHR presentModes {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODES_CREATE_INFO_KHR,
|
||||||
|
.presentModeCount = 1,
|
||||||
|
.pPresentModes = &swapchainPresentMode,
|
||||||
|
};
|
||||||
|
if (Device::swapchainMaintenance1Supported) {
|
||||||
|
swapchainCI.pNext = &presentModes;
|
||||||
|
}
|
||||||
// Setting oldSwapChain to the saved handle of the previous swapchain aids in resource reuse and makes sure that we can still present already acquired images
|
// Setting oldSwapChain to the saved handle of the previous swapchain aids in resource reuse and makes sure that we can still present already acquired images
|
||||||
swapchainCI.oldSwapchain = oldSwapchain;
|
swapchainCI.oldSwapchain = oldSwapchain;
|
||||||
// Setting clipped to VK_TRUE allows the implementation to discard rendering outside of the surface area
|
// Setting clipped to VK_TRUE allows the implementation to discard rendering outside of the surface area
|
||||||
swapchainCI.clipped = VK_TRUE;
|
swapchainCI.clipped = VK_TRUE;
|
||||||
swapchainCI.compositeAlpha = compositeAlpha;
|
swapchainCI.compositeAlpha = compositeAlpha;
|
||||||
|
|
||||||
|
// The old images' views go before the images do. Nothing uses them: the
|
||||||
|
// caller waited for the queue, and a heap copy or descriptor set that
|
||||||
|
// still names one is written again before its frame binds it.
|
||||||
|
for (VkImageView view : imageViews) {
|
||||||
|
vkDestroyImageView(Device::device, view, nullptr);
|
||||||
|
}
|
||||||
|
imageViews.clear();
|
||||||
|
|
||||||
Device::CheckVkResult(vkCreateSwapchainKHR(Device::device, &swapchainCI, nullptr, &swapChain));
|
Device::CheckVkResult(vkCreateSwapchainKHR(Device::device, &swapchainCI, nullptr, &swapChain));
|
||||||
|
|
||||||
// If an existing swap chain is re-created, destroy the old swap chain and the ressources owned by the application (image views, images are owned by the swap chain)
|
// If an existing swap chain is re-created, destroy the old swap chain and the ressources owned by the application (image views, images are owned by the swap chain)
|
||||||
if (oldSwapchain != VK_NULL_HANDLE) {
|
if (oldSwapchain != VK_NULL_HANDLE) {
|
||||||
vkDestroySwapchainKHR(Device::device, oldSwapchain, nullptr);
|
vkDestroySwapchainKHR(Device::device, oldSwapchain, nullptr);
|
||||||
}
|
}
|
||||||
|
// The swap chain images: as many as the driver made, which may be more
|
||||||
|
// than minImageCount asked for. Everything kept per image is sized from
|
||||||
|
// this count; what is kept per frame in flight stays numFrames, so any
|
||||||
|
// count works (#160).
|
||||||
uint32_t imageCount{ 0 };
|
uint32_t imageCount{ 0 };
|
||||||
Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, nullptr));
|
Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, nullptr));
|
||||||
// images[] and every other per-frame array hold exactly numFrames
|
images.resize(imageCount);
|
||||||
// entries. A driver may create more images than minImageCount asked for
|
Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, images.data()));
|
||||||
// (a surface whose minimum is above numFrames must, and Mesa gives
|
|
||||||
// MAILBOX at least four). MAILBOX and IMMEDIATE are wishes: FIFO, with
|
|
||||||
// numFrames, is the frame loop's own. If that has other than numFrames
|
|
||||||
// too, writing them into images[] would run off its end, so refuse.
|
|
||||||
if (imageCount != numFrames && swapchainPresentMode != VK_PRESENT_MODE_FIFO_KHR) {
|
|
||||||
std::println(std::cerr, "Window: the {} swapchain has {} images, not the frame loop's {}; using FIFO",
|
|
||||||
swapchainPresentMode == VK_PRESENT_MODE_MAILBOX_KHR ? "MAILBOX" : "IMMEDIATE", imageCount, numFrames);
|
|
||||||
vkDestroySwapchainKHR(Device::device, swapChain, nullptr);
|
|
||||||
presentModeUnfit = true;
|
|
||||||
swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR;
|
|
||||||
swapchainCI.presentMode = swapchainPresentMode;
|
|
||||||
swapchainCI.oldSwapchain = VK_NULL_HANDLE;
|
|
||||||
Device::CheckVkResult(vkCreateSwapchainKHR(Device::device, &swapchainCI, nullptr, &swapChain));
|
|
||||||
Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, nullptr));
|
|
||||||
}
|
|
||||||
if (imageCount != numFrames) {
|
|
||||||
throw std::runtime_error(std::format("Window: the swapchain has {} images, but the frame loop is built for exactly {}", imageCount, numFrames));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the swap chain images
|
|
||||||
Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, images));
|
|
||||||
|
|
||||||
// Brand-new swapchain images are in VK_IMAGE_LAYOUT_UNDEFINED; none have
|
// Brand-new swapchain images are in VK_IMAGE_LAYOUT_UNDEFINED; none have
|
||||||
// been rendered/presented yet. Render() consults this to pick the correct
|
// been rendered/presented yet. Render() consults this to pick the correct
|
||||||
// oldLayout for each image's first post-acquire transition.
|
// oldLayout for each image's first post-acquire transition.
|
||||||
imageInitialised.fill(false);
|
imageInitialised.assign(imageCount, false);
|
||||||
|
|
||||||
for (std::uint8_t i = 0; i < numFrames; i++) {
|
// A render-finished semaphore per image. The ones the old swapchain's
|
||||||
imageViews[i] = {
|
// images used are kept: its last presents may still be waiting on them.
|
||||||
|
const VkSemaphoreCreateInfo semaphoreCreateInfo { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
|
||||||
|
while (renderFinishedSemaphores.size() < imageCount) {
|
||||||
|
VkSemaphore semaphore = VK_NULL_HANDLE;
|
||||||
|
Device::CheckVkResult(vkCreateSemaphore(Device::device, &semaphoreCreateInfo, nullptr, &semaphore));
|
||||||
|
renderFinishedSemaphores.push_back(semaphore);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::atomic<std::uint64_t> swapchainsMade = 0;
|
||||||
|
swapchainGeneration = ++swapchainsMade;
|
||||||
|
|
||||||
|
imageViewInfos.resize(imageCount);
|
||||||
|
for (std::uint32_t i = 0; i < imageCount; i++) {
|
||||||
|
imageViewInfos[i] = {
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||||
.flags = 0,
|
.flags = 0,
|
||||||
.image = images[i],
|
.image = images[i],
|
||||||
|
|
@ -1634,13 +1753,19 @@ void Window::CreateSwapchain()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// The views made, for descriptor sets, once per swapchain rather than
|
||||||
|
// once per frame whose image changed.
|
||||||
|
imageViews.resize(imageCount);
|
||||||
|
for (std::uint32_t i = 0; i < imageCount; i++) {
|
||||||
|
Device::CheckVkResult(vkCreateImageView(Device::device, &imageViewInfos[i], nullptr, &imageViews[i]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VkCommandBuffer Window::StartInit() {
|
VkCommandBuffer Window::StartInit() {
|
||||||
VkCommandBufferBeginInfo cmdBufInfo {};
|
VkCommandBufferBeginInfo cmdBufInfo {};
|
||||||
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||||
// StartInit reuses drawCmdBuffers[currentBuffer] — the same buffer the
|
// StartInit reuses drawCmdBuffers[currentFrame] — the same buffer the
|
||||||
// steady-state Render() loop submits, guarded by waitFences[currentBuffer].
|
// steady-state Render() loop submits, guarded by waitFences[currentFrame].
|
||||||
// When setup runs mid-session (e.g. building map meshes / acceleration
|
// When setup runs mid-session (e.g. building map meshes / acceleration
|
||||||
// structures on a scene transition) the loop's last submission of this
|
// structures on a scene transition) the loop's last submission of this
|
||||||
// buffer may still be in flight: re-beginning it would trip
|
// buffer may still be in flight: re-beginning it would trip
|
||||||
|
|
@ -1653,7 +1778,7 @@ VkCommandBuffer Window::StartInit() {
|
||||||
// (not the per-frame path), so a full wait-idle here is fine — FinishInit
|
// (not the per-frame path), so a full wait-idle here is fine — FinishInit
|
||||||
// already wait-idles at the end.
|
// already wait-idles at the end.
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));
|
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentFrame], &cmdBufInfo));
|
||||||
|
|
||||||
// The swapchain images are deliberately NOT transitioned here. They are
|
// The swapchain images are deliberately NOT transitioned here. They are
|
||||||
// presentable images and may only be touched after vkAcquireNextImageKHR
|
// presentable images and may only be touched after vkAcquireNextImageKHR
|
||||||
|
|
@ -1662,15 +1787,15 @@ VkCommandBuffer Window::StartInit() {
|
||||||
// VK_IMAGE_LAYOUT_UNDEFINED on its first acquired use (see
|
// VK_IMAGE_LAYOUT_UNDEFINED on its first acquired use (see
|
||||||
// imageInitialised). This command buffer is for the caller's one-time
|
// imageInitialised). This command buffer is for the caller's one-time
|
||||||
// setup work (uploads, acceleration-structure builds, etc.).
|
// setup work (uploads, acceleration-structure builds, etc.).
|
||||||
return drawCmdBuffers[currentBuffer];
|
return drawCmdBuffers[currentFrame];
|
||||||
}
|
}
|
||||||
|
|
||||||
void Window::FinishInit() {
|
void Window::FinishInit() {
|
||||||
VkSubmitInfo submitInfo{};
|
VkSubmitInfo submitInfo{};
|
||||||
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||||
submitInfo.commandBufferCount = 1;
|
submitInfo.commandBufferCount = 1;
|
||||||
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
|
submitInfo.pCommandBuffers = &drawCmdBuffers[currentFrame];
|
||||||
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
|
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentFrame]));
|
||||||
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE));
|
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE));
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
}
|
}
|
||||||
|
|
@ -1679,11 +1804,11 @@ VkCommandBuffer Window::GetCmd() {
|
||||||
VkCommandBufferBeginInfo cmdBufInfo {};
|
VkCommandBufferBeginInfo cmdBufInfo {};
|
||||||
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||||
// Same lifecycle hazard as StartInit: GetCmd re-begins and EndCmd
|
// Same lifecycle hazard as StartInit: GetCmd re-begins and EndCmd
|
||||||
// re-submits the shared drawCmdBuffers[currentBuffer], so drain any
|
// re-submits the shared drawCmdBuffers[currentFrame], so drain any
|
||||||
// in-flight submission of it (and free the resources it references) before
|
// in-flight submission of it (and free the resources it references) before
|
||||||
// re-recording (issue #153).
|
// re-recording (issue #153).
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));
|
Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentFrame], &cmdBufInfo));
|
||||||
|
|
||||||
VkImageSubresourceRange range{};
|
VkImageSubresourceRange range{};
|
||||||
range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
|
@ -1691,16 +1816,16 @@ VkCommandBuffer Window::GetCmd() {
|
||||||
range.levelCount = VK_REMAINING_MIP_LEVELS;
|
range.levelCount = VK_REMAINING_MIP_LEVELS;
|
||||||
range.baseArrayLayer = 0;
|
range.baseArrayLayer = 0;
|
||||||
range.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
range.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
||||||
|
|
||||||
return drawCmdBuffers[currentBuffer];
|
return drawCmdBuffers[currentFrame];
|
||||||
}
|
}
|
||||||
|
|
||||||
void Window::EndCmd(VkCommandBuffer cmd) {
|
void Window::EndCmd(VkCommandBuffer cmd) {
|
||||||
VkSubmitInfo submitInfo{};
|
VkSubmitInfo submitInfo{};
|
||||||
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||||
submitInfo.commandBufferCount = 1;
|
submitInfo.commandBufferCount = 1;
|
||||||
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
|
submitInfo.pCommandBuffers = &drawCmdBuffers[currentFrame];
|
||||||
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
|
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentFrame]));
|
||||||
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE));
|
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE));
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
}
|
}
|
||||||
|
|
@ -1974,7 +2099,7 @@ void Window::Render() {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
Crafter::WebGPU::wgpuFrameBegin();
|
Crafter::WebGPU::wgpuFrameBegin();
|
||||||
for (RenderPass* p : passes) {
|
for (RenderPass* p : passes) {
|
||||||
if (p) p->Record(/*cmd*/ 0u, currentBuffer, *this);
|
if (p) p->Record(/*cmd*/ 0u, currentFrame, *this);
|
||||||
}
|
}
|
||||||
Crafter::WebGPU::wgpuFrameEnd();
|
Crafter::WebGPU::wgpuFrameEnd();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,10 @@ export namespace Crafter {
|
||||||
// All bindings are PARTIALLY_BOUND | UPDATE_AFTER_BIND |
|
// All bindings are PARTIALLY_BOUND | UPDATE_AFTER_BIND |
|
||||||
// UPDATE_UNUSED_WHILE_PENDING: most slots are never written, and a
|
// UPDATE_UNUSED_WHILE_PENDING: most slots are never written, and a
|
||||||
// slot is written into every frame's set at once while earlier frames
|
// slot is written into every frame's set at once while earlier frames
|
||||||
// may still be executing with their set bound.
|
// may still be executing with their set bound. A swapchain slot too
|
||||||
|
// is written into a set another window's frame in flight is bound
|
||||||
|
// to, and when listed during a frame, into the set that frame has
|
||||||
|
// already bound.
|
||||||
static constexpr std::uint32_t storageImageBinding = 0;
|
static constexpr std::uint32_t storageImageBinding = 0;
|
||||||
static constexpr std::uint32_t sampledImageBinding = 1;
|
static constexpr std::uint32_t sampledImageBinding = 1;
|
||||||
static constexpr std::uint32_t samplerBinding = 2;
|
static constexpr std::uint32_t samplerBinding = 2;
|
||||||
|
|
@ -77,12 +80,116 @@ export namespace Crafter {
|
||||||
VkDescriptorSet sets[Window::numFrames] {};
|
VkDescriptorSet sets[Window::numFrames] {};
|
||||||
// Descriptor sets reference VkImageView / VkSampler objects, where the
|
// Descriptor sets reference VkImageView / VkSampler objects, where the
|
||||||
// heap takes create-infos, so in sets mode this struct creates and
|
// heap takes create-infos, so in sets mode this struct creates and
|
||||||
// owns them. setImageViews[frame][slot] is the view frame's set uses
|
// owns them: setImageViews[slot] is the view every frame's set uses
|
||||||
// at that image slot; a view shared by all frames is stored under
|
// at that image slot, setSamplers[slot] likewise. The swapchain's
|
||||||
// frame 0 only. setSamplers[slot] likewise.
|
// views are its window's (Window::imageViews).
|
||||||
std::vector<VkImageView> setImageViews[Window::numFrames];
|
std::vector<VkImageView> setImageViews;
|
||||||
std::vector<VkSampler> setSamplers;
|
std::vector<VkSampler> setSamplers;
|
||||||
|
|
||||||
|
// ─── Swapchain slots ────────────────────────────────────────────
|
||||||
|
// Image slots that name a window's swapchain image. A window's
|
||||||
|
// swapchain images are not its frames: it keeps Window::numFrames
|
||||||
|
// frames in flight over however many images the driver made, and
|
||||||
|
// which one a frame draws into is known only once it has been
|
||||||
|
// acquired (#160). So each frame, after the acquire and before
|
||||||
|
// onUpdate, Window::Render writes the image it acquired into the slots
|
||||||
|
// listed here for it, as a storage image in GENERAL, in that frame's
|
||||||
|
// heap copy (or descriptor set), through WriteSwapchainImage. A shader
|
||||||
|
// reads the image it draws into from one fixed slot, and nothing has
|
||||||
|
// to be written again after a resize. Each slot is one window's, so
|
||||||
|
// two windows can share a heap: neither writes the other's slots,
|
||||||
|
// which the other's frames in flight may be reading. UIRenderer lists
|
||||||
|
// its output slot; a pass of your own lists its own with
|
||||||
|
// AddSwapchainSlot. Freeing a slot takes it off the list.
|
||||||
|
struct SwapchainSlot {
|
||||||
|
std::uint16_t slot = 0;
|
||||||
|
// Compared, never dereferenced: a window gone takes nothing down.
|
||||||
|
const Window* window = nullptr;
|
||||||
|
// What each frame's copy holds at the slot, so a frame whose image
|
||||||
|
// hasn't changed writes nothing: the image's index in the
|
||||||
|
// swapchain of that Window::swapchainGeneration, which starts at
|
||||||
|
// 1, so generation 0 means nothing yet.
|
||||||
|
struct Written {
|
||||||
|
std::uint64_t generation = 0;
|
||||||
|
std::uint32_t image = 0;
|
||||||
|
};
|
||||||
|
std::array<Written, Window::numFrames> written {};
|
||||||
|
};
|
||||||
|
std::vector<SwapchainSlot> swapchainSlots;
|
||||||
|
|
||||||
|
// Lists an image slot, raw as AllocateImageSlots returned it, as one
|
||||||
|
// that names `window`'s swapchain image. Every frame's copy gets it
|
||||||
|
// from that frame on; a slot listed during a frame `window` records
|
||||||
|
// with this heap gets that frame's image at once, so a pass set up
|
||||||
|
// mid-frame draws into it the same frame. Listing a slot again moves
|
||||||
|
// it to `window`. Throws std::invalid_argument for a slot past the
|
||||||
|
// heap's image slots, which an empty ImageSlot (0xFFFF) is.
|
||||||
|
void AddSwapchainSlot(std::uint16_t slot, const Window& window) {
|
||||||
|
if (slot >= imageCapacity) {
|
||||||
|
throw std::invalid_argument(std::format(
|
||||||
|
"DescriptorHeapVulkan::AddSwapchainSlot: image slot {} is not one of the heap's {} image slots", slot, imageCapacity));
|
||||||
|
}
|
||||||
|
std::erase_if(swapchainSlots, [slot](const SwapchainSlot& listed) { return listed.slot == slot; });
|
||||||
|
swapchainSlots.push_back({ .slot = slot, .window = &window });
|
||||||
|
if (window.recording && window.descriptorHeap == this) {
|
||||||
|
WriteSwapchainImage(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes window.imageViewInfos[window.currentImage] (in sets mode,
|
||||||
|
// window.imageViews[window.currentImage]) into window's swapchain
|
||||||
|
// slots in the copy of window.currentFrame. Window::Render calls it
|
||||||
|
// once the frame's fence has been waited, so none of window's frames
|
||||||
|
// still reads that copy, and other windows' frames don't read these
|
||||||
|
// slots.
|
||||||
|
void WriteSwapchainImage(const Window& window) {
|
||||||
|
for (SwapchainSlot& listed : swapchainSlots) {
|
||||||
|
if (listed.window != &window) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SwapchainSlot::Written& written = listed.written[window.currentFrame];
|
||||||
|
if (written.generation == window.swapchainGeneration && written.image == window.currentImage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Device::descriptorHeapSupported) {
|
||||||
|
const VkImageDescriptorInfoEXT imageInfo {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
||||||
|
.pView = &window.imageViewInfos[window.currentImage],
|
||||||
|
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
||||||
|
};
|
||||||
|
const VkResourceDescriptorInfoEXT resource {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
|
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
||||||
|
.data = { .pImage = &imageInfo },
|
||||||
|
};
|
||||||
|
VulkanBuffer<std::uint8_t, true>& heap = resourceHeap[window.currentFrame];
|
||||||
|
const std::uint32_t size = Device::descriptorHeapProperties.imageDescriptorSize;
|
||||||
|
const VkHostAddressRangeEXT destination {
|
||||||
|
.address = heap.value + ImageByteOffset(listed.slot),
|
||||||
|
.size = size,
|
||||||
|
};
|
||||||
|
Device::vkWriteResourceDescriptorsEXT(Device::device, 1, &resource, &destination);
|
||||||
|
heap.FlushDevice(ImageByteOffset(listed.slot), size);
|
||||||
|
} else {
|
||||||
|
const VkDescriptorImageInfo imageInfo {
|
||||||
|
.imageView = window.imageViews[window.currentImage],
|
||||||
|
.imageLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||||
|
};
|
||||||
|
const VkWriteDescriptorSet write {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
|
||||||
|
.dstSet = sets[window.currentFrame],
|
||||||
|
.dstBinding = storageImageBinding,
|
||||||
|
.dstArrayElement = listed.slot,
|
||||||
|
.descriptorCount = 1,
|
||||||
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
||||||
|
.pImageInfo = &imageInfo,
|
||||||
|
};
|
||||||
|
vkUpdateDescriptorSets(Device::device, 1, &write, 0, nullptr);
|
||||||
|
}
|
||||||
|
written = { .generation = window.swapchainGeneration, .image = window.currentImage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DescriptorHeapVulkan() = default;
|
DescriptorHeapVulkan() = default;
|
||||||
DescriptorHeapVulkan(const DescriptorHeapVulkan&) = delete;
|
DescriptorHeapVulkan(const DescriptorHeapVulkan&) = delete;
|
||||||
DescriptorHeapVulkan& operator=(const DescriptorHeapVulkan&) = delete;
|
DescriptorHeapVulkan& operator=(const DescriptorHeapVulkan&) = delete;
|
||||||
|
|
@ -105,12 +212,10 @@ export namespace Crafter {
|
||||||
// Safe on a partly built heap: destroying VK_NULL_HANDLE is a no-op.
|
// Safe on a partly built heap: destroying VK_NULL_HANDLE is a no-op.
|
||||||
void DestroySets() noexcept {
|
void DestroySets() noexcept {
|
||||||
vkDestroyDescriptorPool(Device::device, setPool, nullptr);
|
vkDestroyDescriptorPool(Device::device, setPool, nullptr);
|
||||||
for (std::vector<VkImageView>& views : setImageViews) {
|
for (VkImageView view : setImageViews) {
|
||||||
for (VkImageView view : views) {
|
vkDestroyImageView(Device::device, view, nullptr);
|
||||||
vkDestroyImageView(Device::device, view, nullptr);
|
|
||||||
}
|
|
||||||
views.clear();
|
|
||||||
}
|
}
|
||||||
|
setImageViews.clear();
|
||||||
for (VkSampler sampler : setSamplers) {
|
for (VkSampler sampler : setSamplers) {
|
||||||
vkDestroySampler(Device::device, sampler, nullptr);
|
vkDestroySampler(Device::device, sampler, nullptr);
|
||||||
}
|
}
|
||||||
|
|
@ -224,6 +329,8 @@ export namespace Crafter {
|
||||||
bufferFreelist.reserve(buffers);
|
bufferFreelist.reserve(buffers);
|
||||||
samplerFreelist.clear();
|
samplerFreelist.clear();
|
||||||
samplerFreelist.reserve(samplers);
|
samplerFreelist.reserve(samplers);
|
||||||
|
// Every slot is unallocated again, and the new heaps hold nothing.
|
||||||
|
swapchainSlots.clear();
|
||||||
|
|
||||||
// The heaps are written by the CPU every frame (HOST_VISIBLE is
|
// The heaps are written by the CPU every frame (HOST_VISIBLE is
|
||||||
// mandatory) and read by the GPU, so DEVICE_LOCAL is a perf
|
// mandatory) and read by the GPU, so DEVICE_LOCAL is a perf
|
||||||
|
|
@ -272,6 +379,8 @@ export namespace Crafter {
|
||||||
bufferFreelist.reserve(buffers);
|
bufferFreelist.reserve(buffers);
|
||||||
samplerFreelist.clear();
|
samplerFreelist.clear();
|
||||||
samplerFreelist.reserve(samplers);
|
samplerFreelist.reserve(samplers);
|
||||||
|
// Every slot is unallocated again, as in heap mode.
|
||||||
|
swapchainSlots.clear();
|
||||||
|
|
||||||
// At least one descriptor per binding even when a pool was sized
|
// At least one descriptor per binding even when a pool was sized
|
||||||
// zero (Initialize(1, 1, 0) is common): a zero-count pool size is
|
// zero (Initialize(1, 1, 0) is common): a zero-count pool size is
|
||||||
|
|
@ -347,9 +456,7 @@ export namespace Crafter {
|
||||||
};
|
};
|
||||||
Device::CheckVkResult(vkAllocateDescriptorSets(Device::device, &allocInfo, sets));
|
Device::CheckVkResult(vkAllocateDescriptorSets(Device::device, &allocInfo, sets));
|
||||||
|
|
||||||
for (std::vector<VkImageView>& views : setImageViews) {
|
setImageViews.assign(imageCount, VK_NULL_HANDLE);
|
||||||
views.assign(imageCount, VK_NULL_HANDLE);
|
|
||||||
}
|
|
||||||
setSamplers.assign(samplerCount, VK_NULL_HANDLE);
|
setSamplers.assign(samplerCount, VK_NULL_HANDLE);
|
||||||
rollback.heap = nullptr;
|
rollback.heap = nullptr;
|
||||||
}
|
}
|
||||||
|
|
@ -364,30 +471,8 @@ export namespace Crafter {
|
||||||
// destroys it immediately. That relies on the same contract as slot
|
// destroys it immediately. That relies on the same contract as slot
|
||||||
// reuse in heap mode: no frame in flight may still reference the slot
|
// reuse in heap mode: no frame in flight may still reference the slot
|
||||||
// (true after the queue drain that precedes Window::onResize, and for
|
// (true after the queue drain that precedes Window::onResize, and for
|
||||||
// a slot that was freed at a scene-switch barrier).
|
// a slot that was freed at a scene-switch barrier). The swapchain
|
||||||
|
// image is written per frame by WriteSwapchainImage instead.
|
||||||
// Writes `type` (STORAGE_IMAGE or SAMPLED_IMAGE) at an image slot of
|
|
||||||
// one frame's set, for images that differ per frame: the swapchain.
|
|
||||||
// A slot written this way must be written for every frame.
|
|
||||||
void WriteImageSet(std::uint8_t frame, std::uint16_t slot, VkDescriptorType type,
|
|
||||||
const VkImageViewCreateInfo& viewInfo, VkImageLayout layout) {
|
|
||||||
VkImageView view = VK_NULL_HANDLE;
|
|
||||||
Device::CheckVkResult(vkCreateImageView(Device::device, &viewInfo, nullptr, &view));
|
|
||||||
vkDestroyImageView(Device::device, setImageViews[frame][slot], nullptr);
|
|
||||||
setImageViews[frame][slot] = view;
|
|
||||||
|
|
||||||
const VkDescriptorImageInfo imageInfo { .imageView = view, .imageLayout = layout };
|
|
||||||
const VkWriteDescriptorSet write {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
|
|
||||||
.dstSet = sets[frame],
|
|
||||||
.dstBinding = type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? storageImageBinding : sampledImageBinding,
|
|
||||||
.dstArrayElement = slot,
|
|
||||||
.descriptorCount = 1,
|
|
||||||
.descriptorType = type,
|
|
||||||
.pImageInfo = &imageInfo,
|
|
||||||
};
|
|
||||||
vkUpdateDescriptorSets(Device::device, 1, &write, 0, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writes the same image at an image slot of every frame's set, through
|
// Writes the same image at an image slot of every frame's set, through
|
||||||
// one shared view.
|
// one shared view.
|
||||||
|
|
@ -395,11 +480,8 @@ export namespace Crafter {
|
||||||
const VkImageViewCreateInfo& viewInfo, VkImageLayout layout) {
|
const VkImageViewCreateInfo& viewInfo, VkImageLayout layout) {
|
||||||
VkImageView view = VK_NULL_HANDLE;
|
VkImageView view = VK_NULL_HANDLE;
|
||||||
Device::CheckVkResult(vkCreateImageView(Device::device, &viewInfo, nullptr, &view));
|
Device::CheckVkResult(vkCreateImageView(Device::device, &viewInfo, nullptr, &view));
|
||||||
for (std::vector<VkImageView>& views : setImageViews) {
|
vkDestroyImageView(Device::device, setImageViews[slot], nullptr);
|
||||||
vkDestroyImageView(Device::device, views[slot], nullptr);
|
setImageViews[slot] = view;
|
||||||
views[slot] = VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
setImageViews[0][slot] = view;
|
|
||||||
|
|
||||||
const VkDescriptorImageInfo imageInfo { .imageView = view, .imageLayout = layout };
|
const VkDescriptorImageInfo imageInfo { .imageView = view, .imageLayout = layout };
|
||||||
VkWriteDescriptorSet writes[Window::numFrames];
|
VkWriteDescriptorSet writes[Window::numFrames];
|
||||||
|
|
@ -543,7 +625,12 @@ export namespace Crafter {
|
||||||
// freeing. Multi-slot ranges are decomposed into their individual slots,
|
// freeing. Multi-slot ranges are decomposed into their individual slots,
|
||||||
// each put in its sorted place (TakeRun looks for runs).
|
// each put in its sorted place (TakeRun looks for runs).
|
||||||
// noexcept: capacity is reserved at Initialize so inserting never reallocates.
|
// noexcept: capacity is reserved at Initialize so inserting never reallocates.
|
||||||
|
// A freed swapchain slot stops being one, so the window doesn't write
|
||||||
|
// the swapchain image over the slot's next owner.
|
||||||
void FreeImageSlots(ImageSlotRange r) noexcept {
|
void FreeImageSlots(ImageSlotRange r) noexcept {
|
||||||
|
std::erase_if(swapchainSlots, [r](const SwapchainSlot& listed) {
|
||||||
|
return listed.slot >= r.firstElement && listed.slot - r.firstElement < r.count;
|
||||||
|
});
|
||||||
Release(imageFreelist, r.firstElement, r.count);
|
Release(imageFreelist, r.firstElement, r.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,20 @@ export namespace Crafter {
|
||||||
inline static bool rayTracingSupported = false;
|
inline static bool rayTracingSupported = false;
|
||||||
// VK_EXT_device_fault: the VK_ERROR_DEVICE_LOST report in CheckVkResult.
|
// VK_EXT_device_fault: the VK_ERROR_DEVICE_LOST report in CheckVkResult.
|
||||||
inline static bool deviceFaultSupported = false;
|
inline static bool deviceFaultSupported = false;
|
||||||
|
// VK_KHR_surface_maintenance1 (or its EXT form) with
|
||||||
|
// VK_KHR_get_surface_capabilities2, instance extensions enabled where
|
||||||
|
// the loader has them: a surface's capabilities for one present mode.
|
||||||
|
// Its image counts can differ from the surface's answer for all modes,
|
||||||
|
// and Window::CreateSwapchain asks for the mode it uses (#160).
|
||||||
|
inline static bool surfaceMaintenance1Supported = false;
|
||||||
|
inline static PFN_vkGetPhysicalDeviceSurfaceCapabilities2KHR vkGetPhysicalDeviceSurfaceCapabilities2KHR = nullptr;
|
||||||
|
// VK_KHR_swapchain_maintenance1 (or its EXT form) with its
|
||||||
|
// swapchainMaintenance1 feature, a device group enabled where the
|
||||||
|
// driver has it. With it a swapchain is made naming its present mode
|
||||||
|
// (VkSwapchainPresentModesCreateInfoKHR), which is what lets it have
|
||||||
|
// that mode's image count where the surface's count for all modes is
|
||||||
|
// higher (Turnip's FIFO: 3 where MAILBOX needs 4).
|
||||||
|
inline static bool swapchainMaintenance1Supported = false;
|
||||||
// Pipeline stages that are valid on this device: everything, minus
|
// Pipeline stages that are valid on this device: everything, minus
|
||||||
// RAY_TRACING_SHADER when ray tracing is unsupported. AND it into any
|
// RAY_TRACING_SHADER when ray tracing is unsupported. AND it into any
|
||||||
// stage mask built from a constant that names the RT stage.
|
// stage mask built from a constant that names the RT stage.
|
||||||
|
|
@ -340,6 +354,12 @@ export namespace Crafter {
|
||||||
// over, and throws listing every GPU's gaps when none qualifies. Pure
|
// over, and throws listing every GPU's gaps when none qualifies. Pure
|
||||||
// logic apart from that print, driven by tests/DeviceFeatures.
|
// logic apart from that print, driven by tests/DeviceFeatures.
|
||||||
static std::size_t ChooseDevice(std::span<const DeviceChoice> choices);
|
static std::size_t ChooseDevice(std::span<const DeviceChoice> choices);
|
||||||
|
// `instance`'s physical devices grouped by GPU, in the loader's order:
|
||||||
|
// a GPU two drivers both list (a Flatpak sees the runtime's and the GL
|
||||||
|
// extension's manifests; RADV beside AMDVLK) has one device UUID and
|
||||||
|
// two handles. CRAFTER_GRAPHICS_DEVICE numbers these groups, so one
|
||||||
|
// number means the same GPU inside a sandbox and out.
|
||||||
|
static std::vector<std::vector<VkPhysicalDevice>> DistinctGpus(VkInstance instance);
|
||||||
// Throws when `spirv` declares, directly or implicitly (Int64Atomics
|
// Throws when `spirv` declares, directly or implicitly (Int64Atomics
|
||||||
// declares Int64), a SPIR-V capability that nothing enabled: a core
|
// declares Int64), a SPIR-V capability that nothing enabled: a core
|
||||||
// feature that is off, a subgroup operation the GPU lacks, or one of
|
// feature that is off, a subgroup operation the GPU lacks, or one of
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export namespace Crafter {
|
||||||
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
||||||
struct RenderPass {
|
struct RenderPass {
|
||||||
|
// `frameIdx` is the frame's slot among the frames in flight
|
||||||
|
// (Window::currentFrame), for what lives per frame: the heap copy
|
||||||
|
// bound, RenderingElement3D::tlases. It is not the swapchain image,
|
||||||
|
// which is window.images[window.currentImage] natively: the two stopped
|
||||||
|
// being one index once the swapchain could have more images than
|
||||||
|
// frames in flight (#160).
|
||||||
virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
|
virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
|
||||||
virtual ~RenderPass() = default;
|
virtual ~RenderPass() = default;
|
||||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
|
||||||
|
|
@ -151,11 +151,11 @@ export namespace Crafter {
|
||||||
// The build writes tlases[index] in place: its instance and metadata
|
// The build writes tlases[index] in place: its instance and metadata
|
||||||
// buffers on the host as it records, its acceleration structure and
|
// buffers on the host as it records, its acceleration structure and
|
||||||
// scratch on the GPU. So call it only once every earlier submission
|
// scratch on the GPU. So call it only once every earlier submission
|
||||||
// that used tlases[index] has completed. Window::Render runs onUpdate
|
// that used tlases[index] has completed. The TLASes are per frame in
|
||||||
// after waiting for the fence of the image it renders, so building
|
// flight, and Window::Render runs onUpdate after waiting for the fence
|
||||||
// tlases[currentBuffer] there is safe. Earlier builds, traces and
|
// of its frame slot, so building tlases[currentFrame] there is safe.
|
||||||
// compute ray queries in the same command buffer are ordered before
|
// Earlier builds, traces and compute ray queries in the same command
|
||||||
// the build by the build itself.
|
// buffer are ordered before the build by the build itself.
|
||||||
//
|
//
|
||||||
// A rebuild for a new instance count or preference builds into the
|
// A rebuild for a new instance count or preference builds into the
|
||||||
// same acceleration structure while it fits its storage, so
|
// same acceleration structure while it fits its storage, so
|
||||||
|
|
|
||||||
|
|
@ -429,10 +429,7 @@ export namespace Crafter {
|
||||||
bool firstDispatchThisFrame_ = true;
|
bool firstDispatchThisFrame_ = true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Crafter::EventListener<void> resizeSub_;
|
|
||||||
|
|
||||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
void WriteSwapchainDescriptors();
|
|
||||||
void WriteFontAtlasDescriptor();
|
void WriteFontAtlasDescriptor();
|
||||||
void WriteBufferDescriptor(std::uint16_t slot, VkBuffer buffer, VkDeviceAddress address, std::uint32_t size);
|
void WriteBufferDescriptor(std::uint16_t slot, VkBuffer buffer, VkDeviceAddress address, std::uint32_t size);
|
||||||
void WriteSampledImageDescriptor(std::uint16_t slot,
|
void WriteSampledImageDescriptor(std::uint16_t slot,
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,10 @@ export namespace Crafter {
|
||||||
// has. Mailbox replaces a frame still waiting to be shown, so presenting
|
// has. Mailbox replaces a frame still waiting to be shown, so presenting
|
||||||
// never waits, which suits a window that updates on its own clock
|
// never waits, which suits a window that updates on its own clock
|
||||||
// (Window::updateInterval). Immediate shows each frame at once and may
|
// (Window::updateInterval). Immediate shows each frame at once and may
|
||||||
// tear. A mode the surface lacks falls back to Fifo, as does one whose
|
// tear. A mode the surface lacks falls back to Fifo. The mode decides how
|
||||||
// swapchain the driver makes with more images than the frame loop's
|
// many images the swapchain has (Mesa's Mailbox needs at least four),
|
||||||
// Window::numFrames (Mesa's Mailbox has at least four). In the browser
|
// which the frame loop takes as they come. In the browser the page
|
||||||
// the page presents the canvas and the mode has no effect.
|
// presents the canvas and the mode has no effect.
|
||||||
enum class PresentMode {
|
enum class PresentMode {
|
||||||
Fifo,
|
Fifo,
|
||||||
Mailbox,
|
Mailbox,
|
||||||
|
|
@ -79,10 +79,12 @@ export namespace Crafter {
|
||||||
Event<void> onClose;
|
Event<void> onClose;
|
||||||
Event<void> onBeforeUpdate;
|
Event<void> onBeforeUpdate;
|
||||||
Event<FrameTime> onUpdate;
|
Event<FrameTime> onUpdate;
|
||||||
// Fires when the swapchain has been recreated for a new size.
|
// Fires when the swapchain has been recreated, for a new size or a
|
||||||
// width/height already reflect the new size. Passes that hold
|
// new present mode. width/height already reflect the new size, and
|
||||||
// descriptors referring to window.imageViews[] must re-write them
|
// the old images have been destroyed: anything that holds one of
|
||||||
// here (the old VkImage handles have been destroyed).
|
// them, or is sized after the window, is made again here. The heap
|
||||||
|
// slots that name the swapchain image follow by themselves (see
|
||||||
|
// DescriptorHeapVulkan::AddSwapchainSlot).
|
||||||
Event<void> onResize;
|
Event<void> onResize;
|
||||||
bool open = true;
|
bool open = true;
|
||||||
bool updating = false;
|
bool updating = false;
|
||||||
|
|
@ -208,7 +210,10 @@ export namespace Crafter {
|
||||||
void StartUpdate();
|
void StartUpdate();
|
||||||
void StopUpdate();
|
void StopUpdate();
|
||||||
// Makes the swapchain again with the new mode and fires onResize, as
|
// Makes the swapchain again with the new mode and fires onResize, as
|
||||||
// a resize does, so call it outside StartInit/FinishInit. The
|
// a resize does, so call it outside StartInit/FinishInit. Called while
|
||||||
|
// a frame is being recorded (from onUpdate, a pass, an input action
|
||||||
|
// ticked there), the new swapchain is made once that frame has been
|
||||||
|
// presented: the frame still draws into an image of the old one. The
|
||||||
// environment variable CRAFTER_PRESENT_IMMEDIATE overrides the mode,
|
// environment variable CRAFTER_PRESENT_IMMEDIATE overrides the mode,
|
||||||
// for benchmarks.
|
// for benchmarks.
|
||||||
void SetPresentMode(PresentMode mode);
|
void SetPresentMode(PresentMode mode);
|
||||||
|
|
@ -221,6 +226,9 @@ export namespace Crafter {
|
||||||
// pixels, or that none has. Remembered: it is applied again whenever
|
// pixels, or that none has. Remembered: it is applied again whenever
|
||||||
// the keyboard focus comes back to this window.
|
// the keyboard focus comes back to this window.
|
||||||
void SetTextInput(bool active, std::int32_t x = 0, std::int32_t y = 0, std::int32_t width = 0, std::int32_t height = 0, TextInputPurpose purpose = TextInputPurpose::Normal);
|
void SetTextInput(bool active, std::int32_t x = 0, std::int32_t y = 0, std::int32_t width = 0, std::int32_t height = 0, TextInputPurpose purpose = TextInputPurpose::Normal);
|
||||||
|
// Like SetPresentMode, a resize asked for while a frame is being
|
||||||
|
// recorded takes effect once that frame has been presented, width and
|
||||||
|
// height included, which the rest of the frame still draws at.
|
||||||
void Resize(std::uint32_t width, std::uint32_t height);
|
void Resize(std::uint32_t width, std::uint32_t height);
|
||||||
void Render();
|
void Render();
|
||||||
void Update();
|
void Update();
|
||||||
|
|
@ -337,13 +345,35 @@ export namespace Crafter {
|
||||||
void FinishInit();
|
void FinishInit();
|
||||||
VkCommandBuffer GetCmd();
|
VkCommandBuffer GetCmd();
|
||||||
void EndCmd(VkCommandBuffer cmd);
|
void EndCmd(VkCommandBuffer cmd);
|
||||||
|
// Makes the swapchain, after the old one's images have stopped being
|
||||||
|
// used: every caller has waited for the queue (or has no frames yet).
|
||||||
void CreateSwapchain();
|
void CreateSwapchain();
|
||||||
// Tear-and-rebuild helper used by Resize() and the OUT_OF_DATE
|
// The capabilities a swapchain of `mode` on `surface` is sized by. With
|
||||||
// recovery in Render(). Calls CreateSwapchain() and re-issues the
|
// surface_maintenance1 they are the surface's for that mode, which may
|
||||||
// initial PRESENT_SRC_KHR layout transition for the new images so
|
// differ from its answer for all modes (Mesa on Wayland: FIFO 3, all
|
||||||
// Render()'s barriers (which assume oldLayout = PRESENT_SRC_KHR)
|
// modes 4, because MAILBOX needs 4). Those are the ones that apply when
|
||||||
// stay valid. Does NOT fire onResize — callers do that.
|
// the swapchain is made naming its mode, which `modePassedAlong` says
|
||||||
|
// (swapchain_maintenance1's VkSwapchainPresentModesCreateInfoKHR).
|
||||||
|
// Without that the counts for all modes bind as well, so the minimum
|
||||||
|
// and maximum are the stricter of the two. Asks for the mode last, as
|
||||||
|
// the validation layer checks minImageCount against the surface's
|
||||||
|
// last answer. Static so a test sizes swapchains the way the window
|
||||||
|
// does.
|
||||||
|
static VkSurfaceCapabilitiesKHR SwapchainCapabilities(VkPhysicalDevice gpu, VkSurfaceKHR surface,
|
||||||
|
VkPresentModeKHR mode, bool modePassedAlong);
|
||||||
|
// The minImageCount CreateSwapchain asks for, from those capabilities:
|
||||||
|
// numFrames, or the surface's minimum where that is more, and never
|
||||||
|
// more than its maximum (0 means no maximum). The driver may still
|
||||||
|
// make more. Static so a test can feed it the numbers drivers report.
|
||||||
|
static std::uint32_t SwapchainMinImageCount(const VkSurfaceCapabilitiesKHR& capabilities);
|
||||||
|
// Calls CreateSwapchain(). The new images start in UNDEFINED, and
|
||||||
|
// Render() transitions each on its first acquire (imageInitialised).
|
||||||
|
// Does NOT fire onResize — callers do that.
|
||||||
void RecreateSwapchainAndImages();
|
void RecreateSwapchainAndImages();
|
||||||
|
// What Resize and SetPresentMode do once no frame is being recorded:
|
||||||
|
// waits for the queue, frees the deletion queue, makes the swapchain
|
||||||
|
// again at width x height and fires onResize.
|
||||||
|
void RebuildSwapchain();
|
||||||
|
|
||||||
// Saves the frame the next Render() draws to a PNG at `path`, for
|
// Saves the frame the next Render() draws to a PNG at `path`, for
|
||||||
// visual regression tests and screenshots: that Render copies the
|
// visual regression tests and screenshots: that Render copies the
|
||||||
|
|
@ -353,9 +383,46 @@ export namespace Crafter {
|
||||||
// acquired again.
|
// acquired again.
|
||||||
void CaptureNextFrame(std::filesystem::path path);
|
void CaptureNextFrame(std::filesystem::path path);
|
||||||
std::optional<std::filesystem::path> pendingCapture;
|
std::optional<std::filesystem::path> pendingCapture;
|
||||||
|
|
||||||
|
// ── Frames and images ───────────────────────────────────────────
|
||||||
|
// The frame loop keeps numFrames frames in flight, and that number is
|
||||||
|
// its own: the swapchain has however many images the driver made for
|
||||||
|
// the present mode, which may be fewer or more (Mesa's Mailbox has at
|
||||||
|
// least four; issue #160). Two indices follow from that, set by
|
||||||
|
// Render() before onUpdate:
|
||||||
|
//
|
||||||
|
// currentFrame this frame's slot, frameCounter % numFrames. It
|
||||||
|
// picks everything that lives per frame in flight:
|
||||||
|
// the command buffer, the fence and acquire
|
||||||
|
// semaphore, the descriptor heap copy (or set) the
|
||||||
|
// frame binds, RenderingElement3D::tlases, the
|
||||||
|
// frameIdx a RenderPass is recorded with.
|
||||||
|
// currentImage the swapchain image the frame acquired and draws
|
||||||
|
// into: images[currentImage].
|
||||||
|
//
|
||||||
|
// The two go together differently from frame to frame, so a heap copy
|
||||||
|
// can't hold "its" swapchain image: Render() writes the acquired one
|
||||||
|
// into the frame's copy, at the slots DescriptorHeapVulkan::
|
||||||
|
// AddSwapchainSlot lists for this window, before onUpdate.
|
||||||
static constexpr std::uint8_t numFrames = 3;
|
static constexpr std::uint8_t numFrames = 3;
|
||||||
|
std::uint32_t currentFrame = 0;
|
||||||
|
std::uint32_t currentImage = 0;
|
||||||
|
// True from a frame's acquire to its present: images[currentImage]
|
||||||
|
// and the frame's heap copy are this frame's, and a swapchain rebuild
|
||||||
|
// (Resize, SetPresentMode) waits for the present. What is waiting:
|
||||||
|
// a new present mode, or a present that found the swapchain out of
|
||||||
|
// date; and the last size a resize asked for, 0 when none did.
|
||||||
|
bool recording = false;
|
||||||
|
bool swapchainRebuildPending = false;
|
||||||
|
std::uint32_t deferredWidth = 0;
|
||||||
|
std::uint32_t deferredHeight = 0;
|
||||||
VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE;
|
VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE;
|
||||||
VkSwapchainKHR swapChain = VK_NULL_HANDLE;
|
VkSwapchainKHR swapChain = VK_NULL_HANDLE;
|
||||||
|
// Bumped from a process-wide count whenever CreateSwapchain makes a
|
||||||
|
// swapchain, so it names one swapchain for good: a new swapchain's
|
||||||
|
// handle and images may reuse the old one's handle values. The heap
|
||||||
|
// keys what it wrote into each copy on it.
|
||||||
|
std::uint64_t swapchainGeneration = 0;
|
||||||
// What the swapchain images were created for: STORAGE | TRANSFER_SRC,
|
// What the swapchain images were created for: STORAGE | TRANSFER_SRC,
|
||||||
// plus TRANSFER_DST where the surface allows it, so a pass can blit or
|
// plus TRANSFER_DST where the surface allows it, so a pass can blit or
|
||||||
// clear into the image instead of only writing it from a shader.
|
// clear into the image instead of only writing it from a shader.
|
||||||
|
|
@ -363,10 +430,6 @@ export namespace Crafter {
|
||||||
// The mode the swapchain was made with: presentMode, unless the
|
// The mode the swapchain was made with: presentMode, unless the
|
||||||
// surface lacks it or CRAFTER_PRESENT_IMMEDIATE is set.
|
// surface lacks it or CRAFTER_PRESENT_IMMEDIATE is set.
|
||||||
VkPresentModeKHR swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR;
|
VkPresentModeKHR swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR;
|
||||||
// The mode presentMode asks for made a swapchain with more images than
|
|
||||||
// numFrames once, so swapchains are FIFO until SetPresentMode is
|
|
||||||
// called again, rather than being made twice at every resize.
|
|
||||||
bool presentModeUnfit = false;
|
|
||||||
VkFormat colorFormat;
|
VkFormat colorFormat;
|
||||||
VkColorSpaceKHR colorSpace;
|
VkColorSpaceKHR colorSpace;
|
||||||
// Supported composite-alpha mode for vulkanSurface. A surface property
|
// Supported composite-alpha mode for vulkanSurface. A surface property
|
||||||
|
|
@ -374,8 +437,14 @@ export namespace Crafter {
|
||||||
// once at construction rather than re-derived on every CreateSwapchain
|
// once at construction rather than re-derived on every CreateSwapchain
|
||||||
// (i.e. on every resize configure).
|
// (i.e. on every resize configure).
|
||||||
VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
||||||
VkImage images[numFrames];
|
// One entry per swapchain image, as many as vkGetSwapchainImagesKHR
|
||||||
VkImageViewCreateInfo imageViews[numFrames];
|
// returned; remade with the swapchain. imageViewInfos are the views a
|
||||||
|
// heap descriptor takes (VkImageDescriptorInfoEXT::pView); imageViews
|
||||||
|
// the same views made, which a descriptor set takes, owned by the
|
||||||
|
// window and destroyed with its swapchain.
|
||||||
|
std::vector<VkImage> images;
|
||||||
|
std::vector<VkImageViewCreateInfo> imageViewInfos;
|
||||||
|
std::vector<VkImageView> imageViews;
|
||||||
// Tracks whether each swapchain image has been rendered (and thus
|
// Tracks whether each swapchain image has been rendered (and thus
|
||||||
// left in PRESENT_SRC_KHR) at least once. Freshly created swapchain
|
// left in PRESENT_SRC_KHR) at least once. Freshly created swapchain
|
||||||
// images start in VK_IMAGE_LAYOUT_UNDEFINED, so the first per-frame
|
// images start in VK_IMAGE_LAYOUT_UNDEFINED, so the first per-frame
|
||||||
|
|
@ -383,17 +452,18 @@ export namespace Crafter {
|
||||||
// Reset in CreateSwapchain(). A presentable image may only be touched
|
// Reset in CreateSwapchain(). A presentable image may only be touched
|
||||||
// after it has been acquired, so this initial transition happens
|
// after it has been acquired, so this initial transition happens
|
||||||
// lazily in Render() (post-acquire) rather than up front.
|
// lazily in Render() (post-acquire) rather than up front.
|
||||||
std::array<bool, numFrames> imageInitialised{};
|
std::vector<bool> imageInitialised;
|
||||||
std::thread thread;
|
std::thread thread;
|
||||||
|
// Per frame in flight.
|
||||||
VkCommandBuffer drawCmdBuffers[numFrames] = {};
|
VkCommandBuffer drawCmdBuffers[numFrames] = {};
|
||||||
// Per-frame Vulkan info structs whose contents are almost entirely
|
// Per-frame Vulkan info structs whose contents are almost entirely
|
||||||
// invariant. Initialised once in the constructor; Render() patches only
|
// invariant. Initialised once in the constructor; Render() patches only
|
||||||
// the few fields that change each frame (the two barriers' image and
|
// the few fields that change each frame (the two barriers' image and
|
||||||
// the acquire barrier's oldLayout, and — for the multi-frame-in-flight
|
// the acquire barrier's oldLayout, and — for the multi-frame-in-flight
|
||||||
// sync below — submitInfo/presentInfo's wait/signal semaphores plus
|
// sync below — submitInfo/presentInfo's wait/signal semaphores plus
|
||||||
// submitInfo's command buffer, which follow currentBuffer/frameCounter).
|
// submitInfo's command buffer, which follow currentFrame/currentImage).
|
||||||
// presentInfo.pImageIndices and pSwapchains point at the
|
// presentInfo.pImageIndices and pSwapchains point at the
|
||||||
// currentBuffer/swapChain members, so they track value changes
|
// currentImage/swapChain members, so they track value changes
|
||||||
// (including swapchain recreation) automatically.
|
// (including swapchain recreation) automatically.
|
||||||
VkSubmitInfo submitInfo;
|
VkSubmitInfo submitInfo;
|
||||||
VkCommandBufferBeginInfo cmdBufInfo;
|
VkCommandBufferBeginInfo cmdBufInfo;
|
||||||
|
|
@ -402,14 +472,9 @@ export namespace Crafter {
|
||||||
VkPresentInfoKHR presentInfo;
|
VkPresentInfoKHR presentInfo;
|
||||||
|
|
||||||
// ── Multi-frame-in-flight synchronisation (issue #40) ──────────────
|
// ── Multi-frame-in-flight synchronisation (issue #40) ──────────────
|
||||||
// The renderer keeps up to numFrames frames in flight, so a single
|
// Render() waits for its frame slot's fence before it acquires, so
|
||||||
// semaphore pair + a per-frame wait-idle is no longer enough. Note
|
// the command buffer, the heap copy and the acquire semaphore of that
|
||||||
// that drawCmdBuffers, the per-frame descriptor heap slots, and the
|
// slot are free again, whichever image the acquire then returns.
|
||||||
// swapchain images are ALL keyed by the acquired image index
|
|
||||||
// (currentBuffer) — WriteSwapchainDescriptors bakes heap slot i to
|
|
||||||
// write imageViews[i], so the index that selects a command buffer /
|
|
||||||
// heap slot must equal the acquired image index. Everything below
|
|
||||||
// follows from that.
|
|
||||||
|
|
||||||
// Signaled by the queue submit, waited by vkQueuePresentKHR. Keyed by
|
// Signaled by the queue submit, waited by vkQueuePresentKHR. Keyed by
|
||||||
// the acquired IMAGE index: the presentation engine keeps this
|
// the acquired IMAGE index: the presentation engine keeps this
|
||||||
|
|
@ -417,29 +482,27 @@ export namespace Crafter {
|
||||||
// semaphore is the only safe key. Keying it per-CPU-frame trips
|
// semaphore is the only safe key. Keying it per-CPU-frame trips
|
||||||
// VUID-vkQueueSubmit-pSignalSemaphores-00067.
|
// VUID-vkQueueSubmit-pSignalSemaphores-00067.
|
||||||
// https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html
|
// https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html
|
||||||
std::array<VkSemaphore, numFrames> renderFinishedSemaphores{};
|
// Grown by CreateSwapchain to the image count and never shrunk: a
|
||||||
|
// present of the old swapchain may still wait on one, and nothing
|
||||||
|
// reports when that wait is done, so they live as long as the window.
|
||||||
|
std::vector<VkSemaphore> renderFinishedSemaphores;
|
||||||
|
|
||||||
// Signaled by the queue submit, waited + reset before
|
// Signaled by the queue submit, waited + reset before
|
||||||
// drawCmdBuffers[image] and that image's descriptor-heap slot are
|
// drawCmdBuffers[frame] and that frame's descriptor-heap copy are
|
||||||
// re-recorded. Keyed by image index. Created signaled so the first
|
// re-recorded. Keyed by frame slot. Created signaled so the first
|
||||||
// wait on each image passes through instead of deadlocking.
|
// wait on each slot passes through instead of deadlocking.
|
||||||
std::array<VkFence, numFrames> waitFences{};
|
std::array<VkFence, numFrames> waitFences{};
|
||||||
|
|
||||||
// Signaled by vkAcquireNextImageKHR, waited by the queue submit. Keyed
|
// Signaled by vkAcquireNextImageKHR, waited by the queue submit. Keyed
|
||||||
// by a free-running CPU frame counter, because the image index isn't
|
// by frame slot: the slot's fence is waited before the acquire, and
|
||||||
// known until acquire returns. Sized numFrames+1: at most numFrames
|
// that fence follows the submit that waited on the semaphore, so the
|
||||||
// frames are ever in flight (each image's command buffer is gated by
|
// semaphore has no pending operation when it is reused
|
||||||
// its own fence, and there are numFrames images), so numFrames+1
|
// (VUID-vkAcquireNextImageKHR-semaphore-01779).
|
||||||
// distinct acquire semaphores guarantees the one being reused has no
|
std::array<VkSemaphore, numFrames> imageAcquiredSemaphores{};
|
||||||
// pending operation — required by
|
|
||||||
// VUID-vkAcquireNextImageKHR-semaphore-01779.
|
|
||||||
static constexpr std::uint8_t numAcquireSemaphores = numFrames + 1;
|
|
||||||
std::array<VkSemaphore, numAcquireSemaphores> imageAcquiredSemaphores{};
|
|
||||||
|
|
||||||
// Free-running count of Render() calls; drives the acquire-semaphore
|
// Free-running count of Render() calls; currentFrame is this modulo
|
||||||
// slot (frameCounter % numAcquireSemaphores).
|
// numFrames.
|
||||||
std::uint64_t frameCounter = 0;
|
std::uint64_t frameCounter = 0;
|
||||||
std::uint32_t currentBuffer = 0;
|
|
||||||
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||||
std::vector<RenderPass*> passes;
|
std::vector<RenderPass*> passes;
|
||||||
DescriptorHeapVulkan* descriptorHeap = nullptr;
|
DescriptorHeapVulkan* descriptorHeap = nullptr;
|
||||||
|
|
@ -450,7 +513,7 @@ export namespace Crafter {
|
||||||
// holds the per-Window state Crafter::Window users expect:
|
// holds the per-Window state Crafter::Window users expect:
|
||||||
// a list of render passes and a pointer to the descriptor heap.
|
// a list of render passes and a pointer to the descriptor heap.
|
||||||
static constexpr std::uint8_t numFrames = 1;
|
static constexpr std::uint8_t numFrames = 1;
|
||||||
std::uint32_t currentBuffer = 0;
|
std::uint32_t currentFrame = 0;
|
||||||
std::vector<RenderPass*> passes;
|
std::vector<RenderPass*> passes;
|
||||||
DescriptorHeapWebGPU* descriptorHeap = nullptr;
|
DescriptorHeapWebGPU* descriptorHeap = nullptr;
|
||||||
std::optional<std::array<float, 4>> clearColor;
|
std::optional<std::array<float, 4>> clearColor;
|
||||||
|
|
|
||||||
18
project.cpp
18
project.cpp
|
|
@ -351,8 +351,9 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
// Drives the wl_pointer callbacks directly, as MouseScroll does.
|
// Drives the wl_pointer callbacks directly, as MouseScroll does.
|
||||||
cfg.tests.push_back(LibraryTest("PointerEnterLeave"));
|
cfg.tests.push_back(LibraryTest("PointerEnterLeave"));
|
||||||
|
|
||||||
// Issue #40: multi-frame-in-flight frame pacing (per-image fences
|
// Issue #40: multi-frame-in-flight frame pacing (per-frame fences
|
||||||
// + per-frame semaphores, no steady-state wait-idle). Drives the
|
// and acquire semaphores, per-image render-finished semaphores, no
|
||||||
|
// steady-state wait-idle). Drives the
|
||||||
// real frame loop against a live Wayland compositor for many more
|
// real frame loop against a live Wayland compositor for many more
|
||||||
// frames than there are in-flight slots and asserts the validation
|
// frames than there are in-flight slots and asserts the validation
|
||||||
// layer stays silent — the old singleton-semaphore design only
|
// layer stays silent — the old singleton-semaphore design only
|
||||||
|
|
@ -392,6 +393,19 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
parityTest.config.files.emplace_back(fs::path("examples/HelloUI/font.ttf"));
|
parityTest.config.files.emplace_back(fs::path("examples/HelloUI/font.ttf"));
|
||||||
cfg.tests.push_back(std::move(parityTest));
|
cfg.tests.push_back(std::move(parityTest));
|
||||||
|
|
||||||
|
// Issue #160: a swapchain has as many images as its present mode
|
||||||
|
// needs, and the frame loop takes any count. Every offered mode
|
||||||
|
// is kept (no fallback to FIFO), and every frame presents what it
|
||||||
|
// drew, read back a texel per frame. Where this process's GPU
|
||||||
|
// never makes more than numFrames images, the frame checks run
|
||||||
|
// again in child processes on a GPU that does (llvmpipe), on the
|
||||||
|
// heap and on descriptor sets. Loads the UI kernels like
|
||||||
|
// UIFallbackParity. Needs the Wayland backend + a real compositor.
|
||||||
|
Test imageCountTest = LibraryTest("SwapchainImageCount");
|
||||||
|
imageCountTest.config.shaders = cfg.shaders;
|
||||||
|
imageCountTest.config.buildFiles = cfg.buildFiles;
|
||||||
|
cfg.tests.push_back(std::move(imageCountTest));
|
||||||
|
|
||||||
// Reading the TLAS from the descriptor heap (#7/#15): renders a
|
// Reading the TLAS from the descriptor heap (#7/#15): renders a
|
||||||
// triangle through traceRayEXT and an inline ray query and checks
|
// triangle through traceRayEXT and an inline ray query and checks
|
||||||
// the read-back pixels, through the engine's normal path (so on
|
// the read-back pixels, through the engine's normal path (so on
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,16 @@
|
||||||
// semaphore pair with zero fences for the Window's whole lifetime. This
|
// semaphore pair with zero fences for the Window's whole lifetime. This
|
||||||
// reworks the pacing model:
|
// reworks the pacing model:
|
||||||
//
|
//
|
||||||
// - one VkFence per swapchain slot, keyed by the ACQUIRED IMAGE INDEX,
|
// - one VkFence per in-flight frame slot (a free-running CPU frame counter
|
||||||
// passed to vkQueueSubmit and waited on (then reset) before that image's
|
// % numFrames), passed to vkQueueSubmit and waited on before the slot's
|
||||||
// command buffer is re-recorded;
|
// command buffer is re-recorded — before the acquire, so the slot's
|
||||||
// - one semaphore pair per in-flight frame, keyed by a free-running CPU
|
// acquire semaphore is free again too. Until #160 the fences were keyed
|
||||||
// frame counter % numFrames (the image index isn't known until acquire
|
// by the acquired image index, which tied the frame loop to a swapchain
|
||||||
// returns), so frame N's render-complete and frame N+1's acquire can be
|
// of exactly numFrames images;
|
||||||
// pending at once;
|
// - one acquire semaphore per frame slot, and one render-complete
|
||||||
|
// semaphore per swapchain image (the present holds it until the image
|
||||||
|
// comes back), so frame N's render-complete and frame N+1's acquire can
|
||||||
|
// be pending at once;
|
||||||
// - the steady-state vkQueueWaitIdle is gone (kept only on resize /
|
// - the steady-state vkQueueWaitIdle is gone (kept only on resize /
|
||||||
// OUT_OF_DATE / teardown).
|
// OUT_OF_DATE / teardown).
|
||||||
//
|
//
|
||||||
|
|
@ -25,9 +28,10 @@
|
||||||
// many more frames than there are in-flight slots, then asserts:
|
// many more frames than there are in-flight slots, then asserts:
|
||||||
//
|
//
|
||||||
// - every Render() completed and the CPU frame counter advanced by the
|
// - every Render() completed and the CPU frame counter advanced by the
|
||||||
// expected amount (no deadlock on the per-image fence wait);
|
// expected amount (no deadlock on the per-frame fence wait);
|
||||||
// - the acquired image index rotated across more than one slot (proof the
|
// - the frame slot followed the frame counter, and the acquired image
|
||||||
// swapchain is genuinely multi-buffered, not pinned to image 0);
|
// index rotated across more than one image (proof the swapchain is
|
||||||
|
// genuinely multi-buffered, not pinned to image 0);
|
||||||
// - the Vulkan validation layer reported ZERO errors over the whole run.
|
// - the Vulkan validation layer reported ZERO errors over the whole run.
|
||||||
// This is the load-bearing check: the old singleton-pair design only
|
// This is the load-bearing check: the old singleton-pair design only
|
||||||
// avoided being an active race because the wait-idle masked it. Reusing a
|
// avoided being an active race because the wait-idle masked it. Reusing a
|
||||||
|
|
@ -64,22 +68,24 @@ int main() {
|
||||||
// per-frame fences/semaphores under test.
|
// per-frame fences/semaphores under test.
|
||||||
Window window(640, 480, "FrameLoopSync test");
|
Window window(640, 480, "FrameLoopSync test");
|
||||||
|
|
||||||
// Render() with no passes still exercises the full pacing path: acquire
|
// Render() with no passes still exercises the full pacing path: per-frame
|
||||||
// (signals frameSemaphores[frame].presentComplete) → per-image fence
|
// fence wait → acquire (signals the slot's acquire semaphore) → fence
|
||||||
// wait+reset → barriers → submit (signals renderComplete, fences the
|
// reset → barriers → submit (signals the image's render-complete
|
||||||
// command buffer) → present. Run well past numFrames so every semaphore
|
// semaphore, fences the command buffer) → present. Run well past
|
||||||
// slot and every per-image fence is reused several times — exactly the
|
// numFrames so every semaphore and every per-frame fence is reused
|
||||||
// condition the old single-pair / wait-idle design could not survive
|
// several times — exactly the condition the old single-pair / wait-idle
|
||||||
// without draining the queue every frame.
|
// design could not survive without draining the queue every frame.
|
||||||
constexpr int kFrames = 60;
|
constexpr int kFrames = 60;
|
||||||
static_assert(kFrames > Window::numFrames * 3,
|
static_assert(kFrames > Window::numFrames * 3,
|
||||||
"must loop enough to reuse each in-flight slot multiple times");
|
"must loop enough to reuse each in-flight slot multiple times");
|
||||||
|
|
||||||
std::array<bool, Window::numFrames> imageSeen{};
|
std::vector<bool> imageSeen(window.images.size());
|
||||||
|
bool framesFollowCounter = true;
|
||||||
for (int i = 0; i < kFrames; ++i) {
|
for (int i = 0; i < kFrames; ++i) {
|
||||||
window.Render();
|
window.Render();
|
||||||
if (window.currentBuffer < Window::numFrames) {
|
framesFollowCounter = framesFollowCounter && window.currentFrame == static_cast<std::uint32_t>(i % Window::numFrames);
|
||||||
imageSeen[window.currentBuffer] = true;
|
if (window.currentImage < imageSeen.size()) {
|
||||||
|
imageSeen[window.currentImage] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,11 +97,13 @@ int main() {
|
||||||
std::format("CPU frame counter advanced once per Render() ({} of {})",
|
std::format("CPU frame counter advanced once per Render() ({} of {})",
|
||||||
window.frameCounter, kFrames));
|
window.frameCounter, kFrames));
|
||||||
|
|
||||||
|
Check(framesFollowCounter, "each Render() used frame slot frameCounter % numFrames");
|
||||||
|
|
||||||
int distinctImages = 0;
|
int distinctImages = 0;
|
||||||
for (bool seen : imageSeen) if (seen) ++distinctImages;
|
for (bool seen : imageSeen) if (seen) ++distinctImages;
|
||||||
Check(distinctImages > 1,
|
Check(distinctImages > 1,
|
||||||
std::format("swapchain rotated across multiple images ({} of {} slots used)",
|
std::format("swapchain rotated across multiple images ({} of {} images used)",
|
||||||
distinctImages, Window::numFrames));
|
distinctImages, window.images.size()));
|
||||||
|
|
||||||
Check(Device::validationErrorCount == 0,
|
Check(Device::validationErrorCount == 0,
|
||||||
std::format("no Vulkan validation errors across {} frames ({} seen)",
|
std::format("no Vulkan validation errors across {} frames ({} seen)",
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@
|
||||||
#include "../../lib/stb_image.h"
|
#include "../../lib/stb_image.h"
|
||||||
|
|
||||||
import Crafter.Graphics;
|
import Crafter.Graphics;
|
||||||
import Crafter.Event;
|
|
||||||
import Crafter.Math;
|
import Crafter.Math;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
|
|
@ -203,24 +202,18 @@ int Render(Path path, const fs::path& out) {
|
||||||
|
|
||||||
window.FinishInit();
|
window.FinishInit();
|
||||||
|
|
||||||
// Per frame: the TLAS at buffer slot 0 and that frame's swapchain image at
|
// Per frame: the TLAS at buffer slot 0, in each frame's own copy of the
|
||||||
// image slot 0, each in its own copy of the heap. Written again whenever
|
// heap. The swapchain image at image slot 0, which the window writes
|
||||||
// the swapchain is recreated, since the image descriptors point at its
|
// every frame with the image that frame draws into.
|
||||||
// images.
|
|
||||||
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
|
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
|
||||||
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
||||||
auto writeDescriptors = [&] {
|
descriptorHeap.AddSwapchainSlot(imgSlots.firstElement, window);
|
||||||
|
{
|
||||||
std::array<VkDeviceAddressRangeKHR, Window::numFrames> tlasRanges{};
|
std::array<VkDeviceAddressRangeKHR, Window::numFrames> tlasRanges{};
|
||||||
std::array<VkImageDescriptorInfoEXT, Window::numFrames> imageInfos{};
|
|
||||||
std::vector<VkResourceDescriptorInfoEXT> resources;
|
std::vector<VkResourceDescriptorInfoEXT> resources;
|
||||||
std::vector<VkHostAddressRangeEXT> destinations;
|
std::vector<VkHostAddressRangeEXT> destinations;
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
||||||
imageInfos[f] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[f],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
|
||||||
};
|
|
||||||
resources.push_back({
|
resources.push_back({
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
||||||
|
|
@ -230,22 +223,11 @@ int Render(Path path, const fs::path& out) {
|
||||||
.address = descriptorHeap.resourceHeap[f].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
.address = descriptorHeap.resourceHeap[f].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
||||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
||||||
});
|
});
|
||||||
resources.push_back({
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &imageInfos[f] },
|
|
||||||
});
|
|
||||||
destinations.push_back({
|
|
||||||
.address = descriptorHeap.resourceHeap[f].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Device::vkWriteResourceDescriptorsEXT(Device::device, static_cast<std::uint32_t>(resources.size()),
|
Device::vkWriteResourceDescriptorsEXT(Device::device, static_cast<std::uint32_t>(resources.size()),
|
||||||
resources.data(), destinations.data());
|
resources.data(), destinations.data());
|
||||||
for (auto& heap : descriptorHeap.resourceHeap) heap.FlushDevice();
|
for (auto& heap : descriptorHeap.resourceHeap) heap.FlushDevice();
|
||||||
};
|
}
|
||||||
writeDescriptors();
|
|
||||||
EventListener<void> resizeListener(&window.onResize, writeDescriptors);
|
|
||||||
|
|
||||||
window.descriptorHeap = &descriptorHeap;
|
window.descriptorHeap = &descriptorHeap;
|
||||||
RTPass rtPass(&pipeline);
|
RTPass rtPass(&pipeline);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@
|
||||||
#include "../../lib/stb_image.h"
|
#include "../../lib/stb_image.h"
|
||||||
|
|
||||||
import Crafter.Graphics;
|
import Crafter.Graphics;
|
||||||
import Crafter.Event;
|
|
||||||
import Crafter.Math;
|
import Crafter.Math;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
|
|
@ -264,11 +263,11 @@ int RenderScene(const fs::path& out) {
|
||||||
colourRanges[i] = { .address = colourBuffers[i].address, .size = 4 * sizeof(float) };
|
colourRanges[i] = { .address = colourBuffers[i].address, .size = 4 * sizeof(float) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Written again whenever the swapchain is recreated, since the output
|
// The output image at image slot 0 is the swapchain's, which the window
|
||||||
// image descriptors point at its images.
|
// writes every frame with the image that frame draws into.
|
||||||
auto writeDescriptors = [&] {
|
descriptorHeap.AddSwapchainSlot(imgSlots.firstElement, window);
|
||||||
|
{
|
||||||
std::array<VkDeviceAddressRangeKHR, Window::numFrames> tlasRanges{};
|
std::array<VkDeviceAddressRangeKHR, Window::numFrames> tlasRanges{};
|
||||||
std::array<VkImageDescriptorInfoEXT, Window::numFrames> outInfos{};
|
|
||||||
std::vector<VkResourceDescriptorInfoEXT> resources;
|
std::vector<VkResourceDescriptorInfoEXT> resources;
|
||||||
std::vector<VkHostAddressRangeEXT> destinations;
|
std::vector<VkHostAddressRangeEXT> destinations;
|
||||||
auto add = [&](std::uint32_t f, VkResourceDescriptorInfoEXT resource, std::uint32_t offset, std::uint32_t size) {
|
auto add = [&](std::uint32_t f, VkResourceDescriptorInfoEXT resource, std::uint32_t offset, std::uint32_t size) {
|
||||||
|
|
@ -279,19 +278,10 @@ int RenderScene(const fs::path& out) {
|
||||||
const std::uint32_t bufferSize = Device::descriptorHeapProperties.bufferDescriptorSize;
|
const std::uint32_t bufferSize = Device::descriptorHeapProperties.bufferDescriptorSize;
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
||||||
outInfos[f] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[f],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
|
||||||
};
|
|
||||||
add(f, { .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
add(f, { .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
||||||
.data = { .pAddressRange = &tlasRanges[f] } },
|
.data = { .pAddressRange = &tlasRanges[f] } },
|
||||||
descriptorHeap.BufferByteOffset(bufSlots.firstElement), bufferSize);
|
descriptorHeap.BufferByteOffset(bufSlots.firstElement), bufferSize);
|
||||||
add(f, { .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &outInfos[f] } },
|
|
||||||
descriptorHeap.ImageByteOffset(imgSlots.firstElement), imageSize);
|
|
||||||
for (std::uint16_t i = 0; i < 2; ++i) {
|
for (std::uint16_t i = 0; i < 2; ++i) {
|
||||||
add(f, { .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
add(f, { .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
|
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
|
||||||
|
|
@ -306,9 +296,7 @@ int RenderScene(const fs::path& out) {
|
||||||
Device::vkWriteResourceDescriptorsEXT(Device::device, static_cast<std::uint32_t>(resources.size()),
|
Device::vkWriteResourceDescriptorsEXT(Device::device, static_cast<std::uint32_t>(resources.size()),
|
||||||
resources.data(), destinations.data());
|
resources.data(), destinations.data());
|
||||||
for (auto& heap : descriptorHeap.resourceHeap) heap.FlushDevice();
|
for (auto& heap : descriptorHeap.resourceHeap) heap.FlushDevice();
|
||||||
};
|
}
|
||||||
writeDescriptors();
|
|
||||||
EventListener<void> resizeListener(&window.onResize, writeDescriptors);
|
|
||||||
|
|
||||||
VkSamplerCreateInfo samplerInfo = {
|
VkSamplerCreateInfo samplerInfo = {
|
||||||
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
#include "../../lib/stb_image.h"
|
#include "../../lib/stb_image.h"
|
||||||
|
|
||||||
import Crafter.Graphics;
|
import Crafter.Graphics;
|
||||||
import Crafter.Event;
|
|
||||||
import Crafter.Math;
|
import Crafter.Math;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
|
|
@ -51,7 +50,7 @@ struct ClearPass : RenderPass {
|
||||||
void Record(VkCommandBuffer cmd, std::uint32_t, Window& window) override {
|
void Record(VkCommandBuffer cmd, std::uint32_t, Window& window) override {
|
||||||
const VkClearColorValue magenta = {{1.0f, 0.0f, 1.0f, 1.0f}};
|
const VkClearColorValue magenta = {{1.0f, 0.0f, 1.0f, 1.0f}};
|
||||||
const VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
const VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||||
vkCmdClearColorImage(cmd, window.images[window.currentBuffer], VK_IMAGE_LAYOUT_GENERAL, &magenta, 1, &range);
|
vkCmdClearColorImage(cmd, window.images[window.currentImage], VK_IMAGE_LAYOUT_GENERAL, &magenta, 1, &range);
|
||||||
}
|
}
|
||||||
VkPipelineStageFlags SwapchainStage() const override { return VK_PIPELINE_STAGE_TRANSFER_BIT; }
|
VkPipelineStageFlags SwapchainStage() const override { return VK_PIPELINE_STAGE_TRANSFER_BIT; }
|
||||||
};
|
};
|
||||||
|
|
@ -121,30 +120,25 @@ int main() {
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) RenderingElement3D::BuildTLAS(cmd, f);
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) RenderingElement3D::BuildTLAS(cmd, f);
|
||||||
window.FinishInit();
|
window.FinishInit();
|
||||||
|
|
||||||
|
// Each frame's TLAS at buffer slot 0 of that frame's heap copy; the
|
||||||
|
// swapchain image at image slot 0, which the window writes every frame.
|
||||||
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
|
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
|
||||||
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
||||||
auto writeDescriptors = [&] {
|
descriptorHeap.AddSwapchainSlot(imgSlots.firstElement, window);
|
||||||
|
{
|
||||||
std::array<VkDeviceAddressRangeKHR, Window::numFrames> tlasRanges{};
|
std::array<VkDeviceAddressRangeKHR, Window::numFrames> tlasRanges{};
|
||||||
std::array<VkImageDescriptorInfoEXT, Window::numFrames> imageInfos{};
|
|
||||||
std::vector<VkResourceDescriptorInfoEXT> resources;
|
std::vector<VkResourceDescriptorInfoEXT> resources;
|
||||||
std::vector<VkHostAddressRangeEXT> destinations;
|
std::vector<VkHostAddressRangeEXT> destinations;
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
||||||
imageInfos[f] = { .sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT, .pView = &window.imageViews[f], .layout = VK_IMAGE_LAYOUT_GENERAL };
|
|
||||||
resources.push_back({ .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, .type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
resources.push_back({ .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, .type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
||||||
.data = { .pAddressRange = &tlasRanges[f] } });
|
.data = { .pAddressRange = &tlasRanges[f] } });
|
||||||
destinations.push_back({ .address = descriptorHeap.resourceHeap[f].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
destinations.push_back({ .address = descriptorHeap.resourceHeap[f].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
||||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize });
|
.size = Device::descriptorHeapProperties.bufferDescriptorSize });
|
||||||
resources.push_back({ .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &imageInfos[f] } });
|
|
||||||
destinations.push_back({ .address = descriptorHeap.resourceHeap[f].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize });
|
|
||||||
}
|
}
|
||||||
Device::vkWriteResourceDescriptorsEXT(Device::device, static_cast<std::uint32_t>(resources.size()), resources.data(), destinations.data());
|
Device::vkWriteResourceDescriptorsEXT(Device::device, static_cast<std::uint32_t>(resources.size()), resources.data(), destinations.data());
|
||||||
for (auto& heap : descriptorHeap.resourceHeap) heap.FlushDevice();
|
for (auto& heap : descriptorHeap.resourceHeap) heap.FlushDevice();
|
||||||
};
|
}
|
||||||
writeDescriptors();
|
|
||||||
EventListener<void> resizeListener(&window.onResize, writeDescriptors);
|
|
||||||
window.descriptorHeap = &descriptorHeap;
|
window.descriptorHeap = &descriptorHeap;
|
||||||
|
|
||||||
ClearPass clear;
|
ClearPass clear;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
// narrows SwapchainStage() to COMPUTE_SHADER, exactly like UIRenderer.
|
// narrows SwapchainStage() to COMPUTE_SHADER, exactly like UIRenderer.
|
||||||
//
|
//
|
||||||
// Problem 2 — mid-session StartInit/FinishInit reuse the shared
|
// Problem 2 — mid-session StartInit/FinishInit reuse the shared
|
||||||
// drawCmdBuffers[currentBuffer]. With no steady-state wait-idle the loop's
|
// drawCmdBuffers[currentFrame]. With no steady-state wait-idle the loop's
|
||||||
// last submission of that buffer is still in flight when setup runs, so the
|
// last submission of that buffer is still in flight when setup runs, so the
|
||||||
// old code re-began it (VUID-vkBeginCommandBuffer-commandBuffer-00049) and
|
// old code re-began it (VUID-vkBeginCommandBuffer-commandBuffer-00049) and
|
||||||
// re-submitted it (VUID-vkQueueSubmit-pCommandBuffers-00071) while pending,
|
// re-submitted it (VUID-vkQueueSubmit-pCommandBuffers-00071) while pending,
|
||||||
|
|
@ -82,7 +82,7 @@ int main() {
|
||||||
|
|
||||||
// ── Problem 2: mid-session setup while a frame is in flight ──────────────
|
// ── Problem 2: mid-session setup while a frame is in flight ──────────────
|
||||||
// After Render() returns there is no steady-state wait-idle, so the last
|
// After Render() returns there is no steady-state wait-idle, so the last
|
||||||
// submission of drawCmdBuffers[currentBuffer] is still pending. Calling
|
// submission of drawCmdBuffers[currentFrame] is still pending. Calling
|
||||||
// StartInit()/FinishInit() now (as a scene transition would, to build map
|
// StartInit()/FinishInit() now (as a scene transition would, to build map
|
||||||
// meshes / acceleration structures) reuses that exact command buffer. The
|
// meshes / acceleration structures) reuses that exact command buffer. The
|
||||||
// setup buffer is left empty on purpose: it's the begin/submit lifecycle
|
// setup buffer is left empty on purpose: it's the begin/submit lifecycle
|
||||||
|
|
|
||||||
577
tests/SwapchainImageCount/main.cpp
Normal file
577
tests/SwapchainImageCount/main.cpp
Normal file
|
|
@ -0,0 +1,577 @@
|
||||||
|
//SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
// Issue #160: the swapchain gets as many images as its present mode needs,
|
||||||
|
// and the frame loop takes however many that is. CreateSwapchain used to ask
|
||||||
|
// for exactly Window::numFrames (3) images whatever the surface said, read
|
||||||
|
// them into a three-element array, and key everything a frame in flight
|
||||||
|
// owns, its command buffer, its fence and the descriptor-heap copy holding
|
||||||
|
// "its" swapchain image, by the acquired image index. A swapchain of any
|
||||||
|
// other size fell back to FIFO (asked for MAILBOX or IMMEDIATE) or threw,
|
||||||
|
// and a surface whose minimum is above three got a create info below it.
|
||||||
|
// Mesa's MAILBOX needs at least four images: Turnip on phoc, and llvmpipe on
|
||||||
|
// any Wayland compositor.
|
||||||
|
//
|
||||||
|
// Checked here:
|
||||||
|
//
|
||||||
|
// - Window::SwapchainMinImageCount on the numbers drivers report.
|
||||||
|
// - For every present mode the surface offers, SetPresentMode makes a
|
||||||
|
// swapchain of that mode, with at least the images the window asks for
|
||||||
|
// (Window::SwapchainCapabilities, the mode's own count where the device
|
||||||
|
// has swapchain_maintenance1).
|
||||||
|
// - Every frame draws into the image it presents. A UI pass fills the
|
||||||
|
// window with a colour that differs from frame to frame, and a transfer
|
||||||
|
// pass after it copies the middle texel of the frame's swapchain image
|
||||||
|
// into the frame slot's buffer, read back when the slot comes round
|
||||||
|
// again. With an image count other than numFrames, frames and images
|
||||||
|
// pair up differently from frame to frame: a heap copy that kept one
|
||||||
|
// image would have the UI draw into another frame's image, and the texel
|
||||||
|
// would hold an older colour.
|
||||||
|
// - A SetPresentMode or Resize from a listener of the frame being recorded
|
||||||
|
// takes effect after that frame's present: the rest of the frame indexes
|
||||||
|
// the swapchain it acquired from.
|
||||||
|
// - A swapchain slot listed during a frame is written for that frame (heap
|
||||||
|
// mode, where the descriptor can be read back and compared with the UI's).
|
||||||
|
// - Two windows sharing one heap each draw their own colours: a window
|
||||||
|
// writes only its own swapchain slots.
|
||||||
|
// - No Vulkan validation errors, which only means something with
|
||||||
|
// CRAFTER_GRAPHICS_VALIDATION set and the layer loaded.
|
||||||
|
//
|
||||||
|
// The GPU this process picks may give every mode numFrames images (NVIDIA
|
||||||
|
// does), which leaves the case that matters untried, and a process runs one
|
||||||
|
// binding model. So the frame checks also run in child processes, picked
|
||||||
|
// with CRAFTER_GRAPHICS_DEVICE, on a GPU whose surface needs more images for
|
||||||
|
// some mode (this process's own, else llvmpipe, which most Linux machines
|
||||||
|
// have), in each binding model the parent didn't run there: on the
|
||||||
|
// descriptor heap, and with CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP on descriptor
|
||||||
|
// sets, the path Turnip takes. Without such a GPU that part is a note, not a
|
||||||
|
// failure. `--frames heap|sets` runs the frame checks alone on whichever GPU
|
||||||
|
// the environment picks.
|
||||||
|
//
|
||||||
|
// Needs a live Wayland compositor and a Vulkan device, like FrameLoopSync.
|
||||||
|
|
||||||
|
#include "vulkan/vulkan.h"
|
||||||
|
#include "vulkan/vk_enum_string_helper.h"
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
import Crafter.Graphics;
|
||||||
|
import Crafter.Event;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using namespace Crafter;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
void Check(bool ok, std::string_view what) {
|
||||||
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||||
|
if (!ok) ++failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The test binary's own directory: the build places the UI kernels (both
|
||||||
|
// variants) next to it, and the test runner's working directory is not that
|
||||||
|
// directory.
|
||||||
|
fs::path ExeDir() {
|
||||||
|
return fs::read_symlink("/proc/self/exe").parent_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
int RunCommand(const std::string& cmd) {
|
||||||
|
int status = std::system(cmd.c_str());
|
||||||
|
if (status == -1) return -1;
|
||||||
|
return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModeCase {
|
||||||
|
PresentMode mode;
|
||||||
|
VkPresentModeKHR vk;
|
||||||
|
std::string_view name;
|
||||||
|
};
|
||||||
|
constexpr ModeCase kModes[] = {
|
||||||
|
{ PresentMode::Fifo, VK_PRESENT_MODE_FIFO_KHR, "FIFO" },
|
||||||
|
{ PresentMode::Mailbox, VK_PRESENT_MODE_MAILBOX_KHR, "MAILBOX" },
|
||||||
|
{ PresentMode::Immediate, VK_PRESENT_MODE_IMMEDIATE_KHR, "IMMEDIATE" },
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<VkPresentModeKHR> OfferedModes(VkPhysicalDevice gpu, VkSurfaceKHR surface) {
|
||||||
|
std::uint32_t count = 0;
|
||||||
|
Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &count, nullptr));
|
||||||
|
std::vector<VkPresentModeKHR> modes(count);
|
||||||
|
Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &count, modes.data()));
|
||||||
|
return modes;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Offers(const std::vector<VkPresentModeKHR>& offered, VkPresentModeKHR mode) {
|
||||||
|
return std::ranges::find(offered, mode) != offered.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The most images any present mode under test asks for, for a swapchain of
|
||||||
|
// `surface` on `gpu`, sized as a window on it would size it.
|
||||||
|
std::uint32_t MostImagesAsked(VkPhysicalDevice gpu, VkSurfaceKHR surface, bool modePassedAlong) {
|
||||||
|
const std::vector<VkPresentModeKHR> offered = OfferedModes(gpu, surface);
|
||||||
|
std::uint32_t most = 0;
|
||||||
|
for (const ModeCase& m : kModes) {
|
||||||
|
if (Offers(offered, m.vk)) {
|
||||||
|
most = std::max(most, Window::SwapchainMinImageCount(
|
||||||
|
Window::SwapchainCapabilities(gpu, surface, m.vk, modePassedAlong)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return most;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckMinImageCount() {
|
||||||
|
const auto asked = [](std::uint32_t minImageCount, std::uint32_t maxImageCount) {
|
||||||
|
VkSurfaceCapabilitiesKHR capabilities{};
|
||||||
|
capabilities.minImageCount = minImageCount;
|
||||||
|
capabilities.maxImageCount = maxImageCount;
|
||||||
|
return Window::SwapchainMinImageCount(capabilities);
|
||||||
|
};
|
||||||
|
Check(asked(2, 8) == Window::numFrames, "a minimum below numFrames asks for numFrames (NVIDIA: 2, at most 8)");
|
||||||
|
Check(asked(3, 0) == 3, "a minimum of numFrames asks for numFrames (Mesa's FIFO: 3, no maximum)");
|
||||||
|
Check(asked(4, 0) == 4, "a minimum above numFrames asks for the minimum (Mesa's MAILBOX: 4, no maximum)");
|
||||||
|
Check(asked(5, 6) == 5, "a minimum above numFrames within the maximum asks for the minimum");
|
||||||
|
Check(asked(1, 2) == 2, "a maximum below numFrames caps the request");
|
||||||
|
Check(asked(2, 0) == Window::numFrames, "no maximum leaves numFrames as it is");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A colour for frame `n` that differs from every frame's within 200 of it,
|
||||||
|
// exact in eight bits.
|
||||||
|
std::array<std::uint8_t, 3> Colour(std::uint64_t n) {
|
||||||
|
return {
|
||||||
|
static_cast<std::uint8_t>(40 + (n * 7) % 200),
|
||||||
|
static_cast<std::uint8_t>(250 - (n * 13) % 200),
|
||||||
|
static_cast<std::uint8_t>((n * 29) % 256),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copies the middle texel of the frame's swapchain image into the frame
|
||||||
|
// slot's buffer, after the UI pass drew it: what the frame presents. Added
|
||||||
|
// only for an 8-bit RGBA or BGRA swapchain, whose texel fits the buffer.
|
||||||
|
struct ReadbackPass : RenderPass {
|
||||||
|
std::array<VulkanBuffer<std::uint32_t, true>, Window::numFrames> texels;
|
||||||
|
|
||||||
|
void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override {
|
||||||
|
const VkBufferImageCopy region {
|
||||||
|
.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
|
||||||
|
.imageOffset = { static_cast<std::int32_t>(window.width / 2), static_cast<std::int32_t>(window.height / 2), 0 },
|
||||||
|
.imageExtent = { 1, 1, 1 },
|
||||||
|
};
|
||||||
|
vkCmdCopyImageToBuffer(cmd, window.images[window.currentImage], VK_IMAGE_LAYOUT_GENERAL,
|
||||||
|
texels[frameIdx].buffer, 1, ®ion);
|
||||||
|
const VkBufferMemoryBarrier copied {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
|
||||||
|
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
|
.dstAccessMask = VK_ACCESS_HOST_READ_BIT,
|
||||||
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||||
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||||
|
.buffer = texels[frameIdx].buffer,
|
||||||
|
.offset = 0,
|
||||||
|
.size = VK_WHOLE_SIZE,
|
||||||
|
};
|
||||||
|
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
|
||||||
|
0, 0, nullptr, 1, &copied, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
VkPipelineStageFlags SwapchainStage() const override {
|
||||||
|
return VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// One window filled with a colour per frame through a UIRenderer on `heap`,
|
||||||
|
// and what each frame presented read back. Holds listeners that capture it:
|
||||||
|
// build it in place and don't move it.
|
||||||
|
struct ColourScene {
|
||||||
|
Window& window;
|
||||||
|
GraphicsDescriptorHeap& heap;
|
||||||
|
std::string_view name;
|
||||||
|
// Added to the frame number: two scenes sharing a heap draw different
|
||||||
|
// colours, so one that drew into the other's image is seen.
|
||||||
|
std::uint64_t colourOffset;
|
||||||
|
|
||||||
|
UIRenderer ui;
|
||||||
|
// One quad buffer per frame slot: the colour changes every frame, and a
|
||||||
|
// buffer the frames shared would be overwritten under the frames still
|
||||||
|
// in flight.
|
||||||
|
std::array<GraphicsBuffer<QuadItem, true>, Window::numFrames> quads;
|
||||||
|
std::array<BufferSlot, Window::numFrames> quadSlots;
|
||||||
|
ReadbackPass readback;
|
||||||
|
bool bgra = false;
|
||||||
|
bool checkTexels = false;
|
||||||
|
|
||||||
|
// The frame each slot's texel buffer holds, -1 for none, and what the
|
||||||
|
// texel checks found since Reset.
|
||||||
|
std::array<std::int64_t, Window::numFrames> frameOfSlot;
|
||||||
|
std::uint64_t frame = 0;
|
||||||
|
std::uint64_t framesChecked = 0;
|
||||||
|
std::uint32_t wrong = 0;
|
||||||
|
std::string firstWrong;
|
||||||
|
std::set<std::uint32_t> imagesUsed;
|
||||||
|
std::set<std::pair<std::uint32_t, std::uint32_t>> pairsUsed;
|
||||||
|
// Run once at the start of the next frame's build, then dropped.
|
||||||
|
std::function<void()> duringNextFrame;
|
||||||
|
EventListener<UIBuildArgs> buildSub;
|
||||||
|
|
||||||
|
ColourScene(Window& window, GraphicsDescriptorHeap& heap, std::string_view name, std::uint64_t colourOffset)
|
||||||
|
: window(window), heap(heap), name(name), colourOffset(colourOffset) {
|
||||||
|
const fs::path dir = ExeDir();
|
||||||
|
VkCommandBuffer init = window.StartInit();
|
||||||
|
ui.Initialize(window, heap, init,
|
||||||
|
dir / "ui-quads.comp.spv", dir / "ui-circles.comp.spv",
|
||||||
|
dir / "ui-images.comp.spv", dir / "ui-text.comp.spv",
|
||||||
|
dir / "ui-fused.comp.spv");
|
||||||
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
|
quads[f].Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
||||||
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 1, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||||
|
quadSlots[f] = ui.RegisterBuffer(quads[f]);
|
||||||
|
}
|
||||||
|
bgra = window.colorFormat == VK_FORMAT_B8G8R8A8_UNORM;
|
||||||
|
checkTexels = bgra || window.colorFormat == VK_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
window.passes.push_back(&ui);
|
||||||
|
if (checkTexels) {
|
||||||
|
for (VulkanBuffer<std::uint32_t, true>& texel : readback.texels) {
|
||||||
|
texel.Create(VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||||
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 1);
|
||||||
|
}
|
||||||
|
window.passes.push_back(&readback);
|
||||||
|
} else {
|
||||||
|
std::println("NOTE {}: the swapchain format {} isn't 8-bit RGBA or BGRA: its colours aren't checked",
|
||||||
|
name, string_VkFormat(window.colorFormat));
|
||||||
|
}
|
||||||
|
window.FinishInit();
|
||||||
|
Reset();
|
||||||
|
|
||||||
|
buildSub.SetEvent(&ui.onBuild, [this](UIBuildArgs a) {
|
||||||
|
if (duringNextFrame) {
|
||||||
|
std::function<void()> run = std::move(duringNextFrame);
|
||||||
|
duringNextFrame = nullptr;
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
// The slot's fence has been waited: its buffer holds what the
|
||||||
|
// frame numFrames ago presented. This frame's copy comes after.
|
||||||
|
CheckSlot(a.frameIdx);
|
||||||
|
frameOfSlot[a.frameIdx] = static_cast<std::int64_t>(frame);
|
||||||
|
imagesUsed.insert(this->window.currentImage);
|
||||||
|
pairsUsed.insert({ a.frameIdx, this->window.currentImage });
|
||||||
|
|
||||||
|
const std::array<std::uint8_t, 3> colour = Colour(frame + this->colourOffset);
|
||||||
|
quads[a.frameIdx].value[0] = QuadItem{ 0, 0, 4096, 4096,
|
||||||
|
colour[0] / 255.0f, colour[1] / 255.0f, colour[2] / 255.0f, 1.0f,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0 };
|
||||||
|
quads[a.frameIdx].FlushDevice(a.cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||||
|
ui.DispatchQuads(a.cmd, quadSlots[a.frameIdx], 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
~ColourScene() {
|
||||||
|
std::erase(window.passes, &ui);
|
||||||
|
std::erase(window.passes, &readback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reset() {
|
||||||
|
frameOfSlot.fill(-1);
|
||||||
|
framesChecked = 0;
|
||||||
|
wrong = 0;
|
||||||
|
firstWrong.clear();
|
||||||
|
imagesUsed.clear();
|
||||||
|
pairsUsed.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckSlot(std::uint32_t slot) {
|
||||||
|
if (!checkTexels || frameOfSlot[slot] < 0) return;
|
||||||
|
const std::uint32_t texel = readback.texels[slot].value[0];
|
||||||
|
const std::array<std::uint8_t, 3> rgb = bgra
|
||||||
|
? std::array<std::uint8_t, 3>{ static_cast<std::uint8_t>(texel >> 16), static_cast<std::uint8_t>(texel >> 8), static_cast<std::uint8_t>(texel) }
|
||||||
|
: std::array<std::uint8_t, 3>{ static_cast<std::uint8_t>(texel), static_cast<std::uint8_t>(texel >> 8), static_cast<std::uint8_t>(texel >> 16) };
|
||||||
|
const std::array<std::uint8_t, 3> expected = Colour(static_cast<std::uint64_t>(frameOfSlot[slot]) + colourOffset);
|
||||||
|
bool same = true;
|
||||||
|
for (std::size_t c = 0; c < 3; ++c) {
|
||||||
|
same = same && std::abs(static_cast<int>(rgb[c]) - static_cast<int>(expected[c])) <= 1;
|
||||||
|
}
|
||||||
|
++framesChecked;
|
||||||
|
if (!same && wrong++ == 0) {
|
||||||
|
firstWrong = std::format("frame {} presented ({}, {}, {}), drew ({}, {}, {})", frameOfSlot[slot],
|
||||||
|
rgb[0], rgb[1], rgb[2], expected[0], expected[1], expected[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One frame of this scene's window.
|
||||||
|
void Render() {
|
||||||
|
window.Render();
|
||||||
|
++frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the frames since Reset: settles the queue, reads the slots not
|
||||||
|
// read yet, and reports.
|
||||||
|
void Report(std::string_view what) {
|
||||||
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
|
for (std::uint32_t slot = 0; slot < Window::numFrames; ++slot) {
|
||||||
|
CheckSlot(slot);
|
||||||
|
frameOfSlot[slot] = -1;
|
||||||
|
}
|
||||||
|
if (checkTexels) {
|
||||||
|
Check(wrong == 0, std::format("{}: each of {} frames presented the colour it drew{}", what, framesChecked,
|
||||||
|
wrong == 0 ? std::string() : std::format(" ({} didn't; {})", wrong, firstWrong)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The present modes on the scene's window, then a rebuild asked for during a
|
||||||
|
// frame, then a slot listed during one. Sets `moreImagesThanFrames` when some
|
||||||
|
// mode's swapchain had more images than numFrames.
|
||||||
|
void CheckModes(ColourScene& scene, bool& moreImagesThanFrames) {
|
||||||
|
Window& window = scene.window;
|
||||||
|
const std::vector<VkPresentModeKHR> offered = OfferedModes(Device::physDevice, window.vulkanSurface);
|
||||||
|
for (const ModeCase& m : kModes) {
|
||||||
|
if (!Offers(offered, m.vk)) {
|
||||||
|
std::println("NOTE the surface doesn't offer {}", m.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Asked before the swapchain is made, which asks again: the
|
||||||
|
// validation layer checks the image count against the last answer.
|
||||||
|
const VkSurfaceCapabilitiesKHR capabilities = Window::SwapchainCapabilities(
|
||||||
|
Device::physDevice, window.vulkanSurface, m.vk, Device::swapchainMaintenance1Supported);
|
||||||
|
const std::uint32_t asked = Window::SwapchainMinImageCount(capabilities);
|
||||||
|
window.SetPresentMode(m.mode);
|
||||||
|
Check(window.swapchainPresentMode == m.vk,
|
||||||
|
std::format("{}: the swapchain was made with it, not a fallback ({})", m.name, string_VkPresentModeKHR(window.swapchainPresentMode)));
|
||||||
|
const std::size_t images = window.images.size();
|
||||||
|
Check(images >= asked && images >= capabilities.minImageCount,
|
||||||
|
std::format("{}: {} images, at least the {} asked for and the mode's minimum of {}", m.name, images, asked, capabilities.minImageCount));
|
||||||
|
Check(window.imageViewInfos.size() == images && window.imageViews.size() == images
|
||||||
|
&& window.imageInitialised.size() == images && window.renderFinishedSemaphores.size() >= images,
|
||||||
|
std::format("{}: the per-image state covers all {} images", m.name, images));
|
||||||
|
moreImagesThanFrames = moreImagesThanFrames || images > Window::numFrames;
|
||||||
|
|
||||||
|
scene.Reset();
|
||||||
|
// Enough frames for every frame slot to meet several images.
|
||||||
|
const std::uint64_t frames = 4 * std::max<std::uint64_t>(images, Window::numFrames);
|
||||||
|
for (std::uint64_t i = 0; i < frames; ++i) {
|
||||||
|
scene.Render();
|
||||||
|
}
|
||||||
|
scene.Report(m.name);
|
||||||
|
Check(scene.imagesUsed.size() > 1, std::format("{}: the frames went round {} of the {} images", m.name, scene.imagesUsed.size(), images));
|
||||||
|
std::println(" {}: {} images, {} pairings of frame slot and image", m.name, images, scene.pairsUsed.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A present mode set, and a resize asked for, from a listener of the
|
||||||
|
// frame being recorded: the frame goes on with the swapchain it
|
||||||
|
// acquired from, and the new one is made after its present.
|
||||||
|
const ModeCase* first = nullptr;
|
||||||
|
const ModeCase* second = nullptr;
|
||||||
|
for (const ModeCase& m : kModes) {
|
||||||
|
if (!Offers(offered, m.vk)) continue;
|
||||||
|
(first == nullptr ? first : second) = &m;
|
||||||
|
if (second != nullptr) break;
|
||||||
|
}
|
||||||
|
if (second != nullptr) {
|
||||||
|
window.SetPresentMode(first->mode);
|
||||||
|
const VkSwapchainKHR before = window.swapChain;
|
||||||
|
const std::uint32_t widthBefore = window.width;
|
||||||
|
const std::uint32_t heightBefore = window.height;
|
||||||
|
bool unchangedInFrame = false;
|
||||||
|
scene.duringNextFrame = [&] {
|
||||||
|
window.SetPresentMode(second->mode);
|
||||||
|
window.Resize(widthBefore + 16, heightBefore + 8);
|
||||||
|
unchangedInFrame = window.swapChain == before && window.swapchainPresentMode == first->vk
|
||||||
|
&& window.width == widthBefore && window.height == heightBefore && window.currentImage < window.images.size();
|
||||||
|
};
|
||||||
|
scene.Reset();
|
||||||
|
scene.Render();
|
||||||
|
Check(unchangedInFrame, std::format("a {} -> {} switch and a resize asked for during a frame leave that frame's swapchain and size alone",
|
||||||
|
first->name, second->name));
|
||||||
|
Check(window.swapChain != before && window.swapchainPresentMode == second->vk,
|
||||||
|
std::format("after the frame the swapchain is made again, with {} ({})", second->name, string_VkPresentModeKHR(window.swapchainPresentMode)));
|
||||||
|
Check(window.width == widthBefore + 16 && window.height == heightBefore + 8,
|
||||||
|
std::format("after the frame the window has the size asked for ({}x{})", window.width, window.height));
|
||||||
|
for (int i = 0; i < 2 * Window::numFrames; ++i) {
|
||||||
|
scene.Render();
|
||||||
|
}
|
||||||
|
scene.Report("after the rebuild a frame asked for");
|
||||||
|
} else {
|
||||||
|
std::println("NOTE the surface offers one present mode: no mid-frame switch to check");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A swapchain slot listed during a frame holds that frame's image at
|
||||||
|
// once: in heap mode the descriptor bytes match the UI output slot's,
|
||||||
|
// which the frame wrote before onUpdate.
|
||||||
|
if (Device::descriptorHeapSupported) {
|
||||||
|
const ImageSlotRange spare = scene.heap.AllocateImageSlots(1);
|
||||||
|
bool same = false;
|
||||||
|
scene.duringNextFrame = [&] {
|
||||||
|
scene.heap.AddSwapchainSlot(spare.firstElement, window);
|
||||||
|
const std::uint8_t* copy = scene.heap.resourceHeap[window.currentFrame].value;
|
||||||
|
same = std::memcmp(copy + scene.heap.ImageByteOffset(spare.firstElement),
|
||||||
|
copy + scene.heap.ImageByteOffset(scene.ui.OutImageSlot()),
|
||||||
|
Device::descriptorHeapProperties.imageDescriptorSize) == 0;
|
||||||
|
};
|
||||||
|
scene.Render();
|
||||||
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
|
scene.heap.FreeImageSlots(spare);
|
||||||
|
Check(same, "a swapchain slot listed during a frame holds that frame's image at once");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders on this process's GPU: every offered mode on one window, then two
|
||||||
|
// windows sharing the heap. Sets `moreImagesThanFrames` when some mode's
|
||||||
|
// swapchain had more images than numFrames.
|
||||||
|
int RenderFrames(bool& moreImagesThanFrames) {
|
||||||
|
// Two scenes' output slots, one spare for the mid-frame slot, and each
|
||||||
|
// scene's quad buffers.
|
||||||
|
GraphicsDescriptorHeap heap;
|
||||||
|
heap.Initialize(/*images*/ 3, /*buffers*/ 2 * Window::numFrames, /*samplers*/ 1);
|
||||||
|
{
|
||||||
|
Window window(160, 120, "SwapchainImageCount test");
|
||||||
|
window.descriptorHeap = &heap;
|
||||||
|
{
|
||||||
|
ColourScene scene(window, heap, "first window", 0);
|
||||||
|
CheckModes(scene, moreImagesThanFrames);
|
||||||
|
|
||||||
|
// A second window on the same heap. Interleaved frames: one
|
||||||
|
// window's slot writes land in copies the other's frames in
|
||||||
|
// flight are bound to, and must not touch its slots.
|
||||||
|
Window second(160, 120, "SwapchainImageCount second window");
|
||||||
|
second.descriptorHeap = &heap;
|
||||||
|
{
|
||||||
|
ColourScene other(second, heap, "second window", 100);
|
||||||
|
scene.Reset();
|
||||||
|
for (int i = 0; i < 4 * Window::numFrames; ++i) {
|
||||||
|
scene.Render();
|
||||||
|
other.Render();
|
||||||
|
}
|
||||||
|
scene.Report("two windows on one heap, the first");
|
||||||
|
other.Report("two windows on one heap, the second");
|
||||||
|
}
|
||||||
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
|
}
|
||||||
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) {
|
||||||
|
Check(Device::validationErrorCount == 0,
|
||||||
|
std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount));
|
||||||
|
}
|
||||||
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A child's run of the frame checks in binding model `model` ("heap" or
|
||||||
|
// "sets") on whatever GPU its environment picks.
|
||||||
|
int FramesOnly(std::string_view model) {
|
||||||
|
try {
|
||||||
|
Device::Initialize();
|
||||||
|
} catch (const std::runtime_error& error) {
|
||||||
|
// A missing capability is a skip; anything else that failed is a
|
||||||
|
// failure of the run that was asked for.
|
||||||
|
if (std::string_view(error.what()).find("can't run this application") != std::string_view::npos) {
|
||||||
|
std::println("SKIP this GPU can't run the library: {}", error.what());
|
||||||
|
return 77;
|
||||||
|
}
|
||||||
|
std::println("FAIL Device::Initialize: {}", error.what());
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
std::println(" on {}, {}", std::string_view(Device::deviceProperties.deviceName),
|
||||||
|
Device::descriptorHeapSupported ? "descriptor heap" : "descriptor sets");
|
||||||
|
if (model == "heap" && !Device::descriptorHeapSupported) {
|
||||||
|
std::println("SKIP this GPU has no descriptor heap");
|
||||||
|
return 77;
|
||||||
|
}
|
||||||
|
Check(Device::descriptorHeapSupported == (model == "heap"),
|
||||||
|
std::format("the run is on {}, as asked", model == "heap" ? "the descriptor heap" : "descriptor sets"));
|
||||||
|
bool moreImagesThanFrames = false;
|
||||||
|
return RenderFrames(moreImagesThanFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The number CRAFTER_GRAPHICS_DEVICE gives the GPU `gpu` is one of.
|
||||||
|
std::optional<std::size_t> GpuNumber(const std::vector<std::vector<VkPhysicalDevice>>& gpus, VkPhysicalDevice gpu) {
|
||||||
|
for (std::size_t i = 0; i < gpus.size(); ++i) {
|
||||||
|
if (std::ranges::find(gpus[i], gpu) != gpus[i].end()) return i;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PresentsTo(VkPhysicalDevice gpu, VkSurfaceKHR surface) {
|
||||||
|
std::uint32_t families = 0;
|
||||||
|
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &families, nullptr);
|
||||||
|
for (std::uint32_t family = 0; family < families; ++family) {
|
||||||
|
VkBool32 supported = VK_FALSE;
|
||||||
|
if (vkGetPhysicalDeviceSurfaceSupportKHR(gpu, family, surface, &supported) == VK_SUCCESS && supported) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The number of a GPU, other than `own`, whose swapchain for `surface` would
|
||||||
|
// have more images than numFrames in some mode. Whether it has
|
||||||
|
// swapchain_maintenance1 isn't known without a device, so its counts are
|
||||||
|
// taken as without: at most an overestimate, and that GPU's MAILBOX needs the
|
||||||
|
// most either way.
|
||||||
|
std::optional<std::size_t> GpuWithMoreImages(const std::vector<std::vector<VkPhysicalDevice>>& gpus,
|
||||||
|
std::size_t own, VkSurfaceKHR surface) {
|
||||||
|
for (std::size_t i = 0; i < gpus.size(); ++i) {
|
||||||
|
if (i == own || !PresentsTo(gpus[i].front(), surface)) continue;
|
||||||
|
if (MostImagesAsked(gpus[i].front(), surface, /*modePassedAlong*/ false) > Window::numFrames) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Drive() {
|
||||||
|
CheckMinImageCount();
|
||||||
|
|
||||||
|
Device::Initialize();
|
||||||
|
const bool ownHeap = Device::descriptorHeapSupported;
|
||||||
|
std::println(" on {}, {}, swapchain_maintenance1 {}", std::string_view(Device::deviceProperties.deviceName),
|
||||||
|
ownHeap ? "descriptor heap" : "descriptor sets", Device::swapchainMaintenance1Supported ? "on" : "off");
|
||||||
|
bool moreImagesThanFrames = false;
|
||||||
|
RenderFrames(moreImagesThanFrames);
|
||||||
|
|
||||||
|
// Where frames and images pair up differently, on each binding model:
|
||||||
|
// this process covered its own GPU's model if that GPU makes more images
|
||||||
|
// than frames; the children cover the rest.
|
||||||
|
const std::vector<std::vector<VkPhysicalDevice>> gpus = Device::DistinctGpus(Device::instance);
|
||||||
|
const std::optional<std::size_t> own = GpuNumber(gpus, Device::physDevice);
|
||||||
|
std::optional<std::size_t> target = moreImagesThanFrames ? own : std::nullopt;
|
||||||
|
if (!target && own) {
|
||||||
|
Window probe(160, 120, "SwapchainImageCount probe");
|
||||||
|
target = GpuWithMoreImages(gpus, *own, probe.vulkanSurface);
|
||||||
|
}
|
||||||
|
if (!target) {
|
||||||
|
std::println("NOTE no GPU here makes a swapchain with more than {} images, "
|
||||||
|
"so frames and images were never paired differently", Window::numFrames);
|
||||||
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs::path exe = fs::read_symlink("/proc/self/exe");
|
||||||
|
const auto child = [&](std::string_view model) {
|
||||||
|
// `env -u` so a CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP inherited from
|
||||||
|
// the caller's environment can't turn the heap run into a sets run.
|
||||||
|
// LP_NUM_THREADS keeps llvmpipe, the usual target, off most of the
|
||||||
|
// machine's cores while the other tests run, timing ones among them.
|
||||||
|
// env reads its options before the first assignment.
|
||||||
|
const int exit = RunCommand(std::format("env {} LP_NUM_THREADS=4 CRAFTER_GRAPHICS_DEVICE={} '{}' --frames {}",
|
||||||
|
model == "heap" ? "-u CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP" : "CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP=1",
|
||||||
|
*target, exe.string(), model));
|
||||||
|
if (exit == 77) {
|
||||||
|
std::println("NOTE GPU {} skipped the run on {} (above)", *target, model);
|
||||||
|
} else {
|
||||||
|
Check(exit == 0, std::format("GPU {} draws the frames right on {} (exit {})", *target, model, exit));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const bool coveredHere = moreImagesThanFrames;
|
||||||
|
if (!(coveredHere && ownHeap)) child("heap");
|
||||||
|
if (!(coveredHere && !ownHeap)) child("sets");
|
||||||
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
// It would override every present mode under test.
|
||||||
|
unsetenv("CRAFTER_PRESENT_IMMEDIATE");
|
||||||
|
const int result = argc == 3 && std::string_view(argv[1]) == "--frames" ? FramesOnly(argv[2]) : Drive();
|
||||||
|
std::println("{}", result == EXIT_SUCCESS ? "all checks passed" : result == 77 ? "skipped" : "check(s) failed");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ struct ClearPass : RenderPass {
|
||||||
|
|
||||||
void Record(VkCommandBuffer cmd, std::uint32_t, Window& window) override {
|
void Record(VkCommandBuffer cmd, std::uint32_t, Window& window) override {
|
||||||
const VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
const VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||||
vkCmdClearColorImage(cmd, window.images[window.currentBuffer], VK_IMAGE_LAYOUT_GENERAL, &colour, 1, &range);
|
vkCmdClearColorImage(cmd, window.images[window.currentImage], VK_IMAGE_LAYOUT_GENERAL, &colour, 1, &range);
|
||||||
}
|
}
|
||||||
|
|
||||||
VkPipelineStageFlags SwapchainStage() const override {
|
VkPipelineStageFlags SwapchainStage() const override {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@
|
||||||
// registered again before the first frame, so the second write has to
|
// registered again before the first frame, so the second write has to
|
||||||
// replace the first one's view / sampler. A stale descriptor shows the
|
// replace the first one's view / sampler. A stale descriptor shows the
|
||||||
// wrong texture and fails the comparison;
|
// wrong texture and fails the comparison;
|
||||||
// - a resize, which rewrites the per-frame swapchain descriptors;
|
// - a resize, after which every frame's copy (heap or set) has to be
|
||||||
|
// given the new swapchain's images: in sets mode through new views;
|
||||||
// - an image created with ImageVulkan::Create's default consumer stage,
|
// - an image created with ImageVulkan::Create's default consumer stage,
|
||||||
// which has to be resolved to a stage the device actually has;
|
// which has to be resolved to a stage the device actually has;
|
||||||
// - a user-style ui.Dispatch whose push payload is not a whole number of
|
// - a user-style ui.Dispatch whose push payload is not a whole number of
|
||||||
|
|
@ -293,9 +294,10 @@ int RenderScene(const fs::path& out) {
|
||||||
window.FinishInit();
|
window.FinishInit();
|
||||||
|
|
||||||
// More frames than there are in flight, so every per-frame heap copy /
|
// More frames than there are in flight, so every per-frame heap copy /
|
||||||
// descriptor set is used; then a resize, which recreates the swapchain
|
// descriptor set is used; then a resize, which recreates the swapchain,
|
||||||
// and rewrites its descriptors; then enough frames for the glyphs shaped
|
// whose new images the frames then write into their copies; then enough
|
||||||
// on the first frame to have reached the atlas texture.
|
// frames for the glyphs shaped on the first frame to have reached the
|
||||||
|
// atlas texture.
|
||||||
for (int i = 0; i < Window::numFrames * 2; ++i) window.Render();
|
for (int i = 0; i < Window::numFrames * 2; ++i) window.Render();
|
||||||
window.Resize(kResizedWidth, kResizedHeight);
|
window.Resize(kResizedWidth, kResizedHeight);
|
||||||
for (int i = 0; i < Window::numFrames * 3; ++i) window.Render();
|
for (int i = 0; i < Window::numFrames * 3; ++i) window.Render();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue