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>
5.7 KiB
Examples
Each example is a self-contained crafter-build project that depends on
the parent Crafter.Graphics via LocalProject. To build and run any
of them:
cd examples/<name>
crafter-build -r
Index
HelloWindow
Minimum viable program: open a window, run the event loop. No Vulkan
rendering. Useful as a smoke test for Device::Initialize + Window +
the platform backend.
VulkanTriangle
Ray-traced single triangle through vkCmdTraceRaysKHR. Shows the full
ray-tracing setup: DescriptorHeapVulkan with image and buffer slots,
PipelineRTVulkan from raygen / miss / closesthit SPIR-V, BLAS via
Mesh::Build, TLAS via RenderingElement3D::BuildTLAS, direct
vkWriteResourceDescriptorsEXT for the per-frame TLASes,
AddSwapchainSlot for the output image, RTPass on window.passes.
Smallest test of the bindless ray-tracing path.
HelloUI
Compute-shader UI demo using all three UI tiers:
- Tier 3 components:
DrawButton,DrawSlider,DrawProgressBar, composed viaRect::SubRectfor resize-safe layout. - Tier 2 standard shaders:
DispatchQuadsfor the background and components,DispatchCirclesfor a cursor-tracking dot,DispatchTextfor the button label (with the FontAtlas wired up toUIRenderer). - Tier 1 is available too — any custom
ComputeShaderregistered on the same heap can be dispatched alongside the standard ones.
Hit-testing and animation are user code (see the EventListener
subscriptions on window.onMouseMove / onMouseLeftClick); the
library does not track widgets or focus.
Drop a TTF in this directory as font.ttf before running (the example
loads it via Font("font.ttf")).
InputSystem
Guided tour of Crafter::Input: name actions ("Jump", "Move", "Fire",
"Look", "Zoom"), bind them to keys / mouse / gamepad (with composite
bindings for WASD-as-Vector2 and analog sticks), and consume them as
events. Demonstrates:
- The compile-time
Key(CrafterKeys::Space)helper that folds to a per-platform raw scancode — bindings stay cross-platform-readable in source while runtime data stores raw codes only. - All four action types (Button, Axis1D, Vector2) with multiple bindings per action (any-of semantics).
Map::StartRebind— press R, then press any input to remap "Jump" at runtime. Captured input is filtered out for that frame.BindingToString/BindingFromStringround-trip — print the default bindings as the on-disk format.- Gamepad hot-plug events: plug a controller in mid-run and the bindings start firing immediately.
Console-driven (no UI rendering needed); focus the window and watch stdout.
CustomShader
Tier 1 demo: a user-authored compute shader (inverse-circle.comp.glsl)
running alongside the shipped drawQuads. The custom shader inverts RGB
under each item-circle — exactly the kind of effect attempt #2's closed
shader couldn't express. Shows:
- Defining your own item POD struct in C++ + matching
std430struct in GLSL. #include "../../shaders/ui-shared.glsl"for the bindless heap declarations +UIDispatchHeaderpush-constant contract.ComputeShader::Loadfor the.spv,UIRenderer::RegisterBufferfor your SSBO,FillHeaderto populate the standard prefix, andUIRenderer::Dispatchto launch — the same pattern the standard shaders use under the hood.- The inter-dispatch SHADER_WRITE → SHADER_READ|WRITE barrier is
inserted automatically, so the custom shader sees the colored stripes
drawn by the prior
DispatchQuadsand reads/writes the swapchain image safely.
RayQueryPick
Regression test for the WebGPU software ray-query shim. Builds a
512-instance TLAS and shoots one ray through a rayQuery=true
PlainComputeShader, reading the committed hit back to the host and
checking it against the analytically-known answer. Guards against the
hardcoded-leaf-start TLAS-traversal bug (issue #25) that made every
rayQuery pick miss for realistic instance counts. WebGPU/DOM only.
HDRBloom
Cross-backend-shaped HDR bloom on the WebGPU backend (issue #27). The RT
pipeline's RESOLVE writes linear radiance into a user rgba16float
texture (StorageImage2D, PipelineRTWebGPU::Init(..., RGBA16Float)),
two PlainComputeShader passes threshold + blur it through float storage
targets (UICustomBindingKind::StorageTexture), and a UI custom shader
composites scene + bloom with a Reinhard tonemap onto the canvas.
Exercises the three primitives an HDR post-process chain needs: float
textures, write-only storage-texture bindings, and pre-tonemap radiance
out of the wavefront. The threshold/blur passes run from onBeforeUpdate
so each gets its own queue submit — the storage-write → sampled-read
barrier WebGPU only provides between submits (or between passes), never
within a single compute pass. WebGPU/DOM only; the same chain is wireable
on Vulkan today via an offscreen HDR heap image + a composite RenderPass
(the present path records passes generically and barriers between them).
RTMultiShadow
Multi-light shadowing through the wavefront RT pipeline (issue #30). Five
pillars on a checkered ground, four colored point lights; the closest-hit
emits one shadow ray per light from the same invocation, so up to four
rays per pixel resolve in a single SHADE pass. Exercises both halves of
the >1-ray-per-pixel-per-bounce widening: the atomic rtAccumulate
(per-channel f32 CAS — concurrent same-pixel adds don't race) and
RTPass::raysPerPixel (scales the ray/hit/payload buffers so the
per-light emits aren't capacity-dropped). Any regression shows up as
flickering dark noise or a missing shadow color in the overlap regions.
WebGPU/DOM only.