Crafter.Graphics/examples
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jorijn van der Graaf 852ae8abe3
fix(descriptor-heap): round the buffer region up, align the reserved range for buffers too, and never size a heap at zero
DescriptorHeapVulkan
- The buffer region started at images * imageDescriptorSize /
  bufferDescriptorSize, rounded down. A descriptor_heap array indexes
  the heap from byte 0 in strides of its descriptor size, so buffer slot
  0 has to sit on a whole buffer descriptor past the images. Where the
  image descriptors were not a whole number of buffer descriptors, the
  first buffer descriptors overwrote the last image descriptors. Where
  they were less than one, the start was bumped to descriptor 1, but the
  heap was still sized as if the buffers began right after the images,
  so the last buffer descriptors went into the implementation's reserved
  range. Every descriptor size is a power of two, so both cases need
  image descriptors smaller than buffer descriptors, which NVIDIA (32
  and 16 bytes) and llvmpipe (64 and 16) do not have. The start now
  rounds up and the heap is sized from it (#163).
- The reserved range sat at a multiple of imageDescriptorAlignment only,
  where vkCmdBindResourceHeapEXT also wants bufferDescriptorAlignment
  (VUID-vkCmdBindResourceHeapEXT-pBindInfo-11435 and -11436). It now
  starts on a multiple of the larger. The buffer descriptors need no more
  than the rounding: each alignment is at most its descriptor size, so a
  whole number of descriptors is aligned.
- bufferStartElement is 16 bits and took the division unchecked, so an
  image region past 65535 buffer descriptors (16384 images on llvmpipe)
  wrapped the buffer region around onto the images. Slot counts whose
  buffer indices do not fit 16 bits, 0xFFFF being the empty slot, now
  throw, naming the counts. A heap without buffer slots has no buffer
  region, which starts at 0 as in descriptor-set mode, so image-only
  heaps have no index to fit.
- On a driver that reserves nothing (llvmpipe), a heap without sampler
  slots was a zero-size buffer, which every Initialize(1, 1, 0) made,
  and Window::Render bound its address each frame. Each heap now holds at
  least one descriptor, as each binding does in descriptor-set mode. That
  grows NVIDIA's sampler heap by one descriptor when no sampler slots are
  asked for; the resource heaps of the slot counts in the tree keep their
  layouts byte for byte.
- ComputeLayout holds the arithmetic, pure over the descriptor-heap
  properties, and Initialize goes through it. GetBufferOffset and
  GetBufferOffsetElement, which had their own copies of the old
  arithmetic and never had a caller, are gone.
- Bind(cmd, frame) binds a frame's heaps with the reserved ranges
  Initialize stored, or in descriptor-set mode the frame's descriptor
  set. Window::Render and the RTVolume example worked the reserved-range
  offset out from the heap's size with the image-alignment mask, and
  call Bind now. The window's per-heap cache of bind structs goes with
  it: keyed on the heap pointer, it kept binding the old buffers after a
  re-Initialize that reallocated them. The heap still ends with its
  reserved range, so code that derives the offset from the size keeps
  getting the right one.

Tests
- DescriptorHeapLayout: the issue's two layouts, the reserved range where
  buffer descriptors are aligned more strictly than image ones, every
  legal combination of descriptor sizes and alignments up to 256 bytes,
  no heap empty, the NVIDIA and llvmpipe layouts, the 16-bit limit and
  image-only heaps. Pure CPU, no device. Against the old arithmetic it
  fails 12 of its 18 checks.

Docs
- README.md's heap bullet and AGENTS.consumer.md say how to bind the
  heap outside the frame loop, and the consumer notes that Initialize
  throws when buffer indices would pass 16 bits. AGENTS.md lists
  ComputeLayout among the functions the pure-logic tests drive.

Closes #163.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:06:44 +01:00
..
CustomShader fix(webgpu): drop the removed UICustomBinding::_pad from the CustomShader example 2026-10-31 08:07:00 +01:00
Decompression SPDX license update 2026-07-22 18:09:06 +02:00
HDRBloom SPDX license update 2026-07-22 18:09:06 +02:00
HelloDom SPDX license update 2026-07-22 18:09:06 +02:00
HelloUI SPDX license update 2026-07-22 18:09:06 +02:00
HelloWindow SPDX license update 2026-07-22 18:09:06 +02:00
InputSystem SPDX license update 2026-07-22 18:09:06 +02:00
RayQueryPick SPDX license update 2026-07-22 18:09:06 +02:00
RTMultiShadow SPDX license update 2026-07-22 18:09:06 +02:00
RTStress SPDX license update 2026-07-22 18:09:06 +02:00
RTVolume fix(descriptor-heap): round the buffer region up, align the reserved range for buffers too, and never size a heap at zero 2026-10-31 16:06:44 +01:00
Sponza fix(rt): split Sponza's combined image sampler into a heap texture and sampler 2026-10-31 08:25:52 +01:00
SponzaBench fix(rt): split Sponza's combined image sampler into a heap texture and sampler 2026-10-31 08:25:52 +01:00
VulkanTriangle feat(device): applications ask for the device features their shaders use 2026-10-31 06:40:02 +01:00
LICENSE SPDX license update 2026-07-22 18:09:06 +02:00
README.md test(webgpu-rt): RTMultiShadow example exercising N shadow rays/pixel/bounce (#30) 2026-06-09 22:45:33 +00:00

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 swapchain views, 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 via Rect::SubRect for resize-safe layout.
  • Tier 2 standard shaders: DispatchQuads for the background and components, DispatchCircles for a cursor-tracking dot, DispatchText for the button label (with the FontAtlas wired up to UIRenderer).
  • Tier 1 is available too — any custom ComputeShader registered 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 / BindingFromString round-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 std430 struct in GLSL.
  • #include "../../shaders/ui-shared.glsl" for the bindless heap declarations + UIDispatchHeader push-constant contract.
  • ComputeShader::Load for the .spv, UIRenderer::RegisterBuffer for your SSBO, FillHeader to populate the standard prefix, and UIRenderer::Dispatch to 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 DispatchQuads and 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.