| Filename | Latest commit message | Latest commit date |
|---|---|---|
The atlas packed glyphs onto shelves and never gave a place back (#55). Once its 1024x1024 image was full, EnsureAndGet returned nullptr for each new glyph before caching anything, so every new string holding one rasterised it again, and ShapeText dropped it together with its advance: the rest of the line closed up over the gap, and stayed that way. FontAtlas - A glyph is measured, not rasterised, until it gets a cell. One that finds no free room keeps its advance and box and draws blank for a frame. EnsureAndGet always returns the glyph, now a reference; Glyph::Resident() says whether it has a cell, Glyph::handle which one (slot and generation). - Evictions wait for the next frame: its first EnsureAndGet, Touch or Update places the glyphs that waited, once every glyph the frame before drew has been marked. Evicting while a frame is still asking for glyphs took ones it drew further down, which then took others in turn: a page switched back to could rasterise most of itself again rather than what it was missing, and on the DOM backend an over-full view could do so every frame. - A glyph keeps its cell for framesInFlight + 1 frames after it was drawn. The frame loop waits frame u's fence at the start of frame u + framesInFlight, but Device::frameCounter reads that from the end of the frame before, and input callbacks and onBeforeUpdate run in between. - FontAtlas::Packer places glyphs. Until the rows run out it fills shelves exactly as before. Then a glyph takes the same columns of one shelf at least as tall, or of several in a row as tall together, the free rows below the last shelf counting as one more: free texels, and, when the frame begins, glyphs nothing in flight draws, the least recently drawn first. A glyph spanning shelves lets in one taller than every shelf, and a few glyphs drawn every frame pin only their columns. Merging whole shelves instead, tried first, left a random workload without room for a glyph while a median 43% of the atlas was in flight; spanning, at 65%. - A box that found no room in a frame is remembered, with every box at least as big, as a small set per mode, until the frame ends or (without eviction) something is evicted: a saturated atlas no longer searches every shelf for each of its missing glyphs. - An evicted glyph is forgotten, and its texels are zeroed and uploaded with the glyph that takes its place: a neighbour's filtering at its edge must read padding or zero, not a stale glyph. - Touch(handles) marks every glyph whose handle still holds its cell, and says whether all did. InvalidateFont(font) forgets the font's glyphs and those waiting for a cell, whose Font* the next frame would read. The glyph index is looked up at rasterisation, so a Font replaced in place never gets one past its glyphs. GlyphsRasterized() counts, for tests. UIRenderer - A shaped run keeps a handle per glyph it draws. A hit touches them all, so the atlas keeps what is drawn every frame, and shapes the run again when one was evicted or one had no room. A glyph without room draws blank but advances the pen. - InvalidateFont clears the atlas's glyphs of the font too, as its comment and AGENTS.md always said it did. - Record calls the atlas's Update before onBuild, every frame, so the waiting glyphs are placed and uploaded first. Dispatch uploads the atlas before any kernel, a Tier 1 one sampling FontAtlasImageSlot() included: a glyph placed earlier in onBuild can sit in a cell another glyph had. DispatchText and DispatchFused no longer do it themselves. Device, Window (DOM) - Device::frameCounter and Device::framesInFlight exist on the DOM backend too, the counter bumped at the end of Window::Render, with one frame in flight: a frame's writeTexture lands before that frame's submit but after every earlier one. HelloUI - One set of item buffers and slots per frame in flight, by frameIdx. A frame still on the GPU read the items the next frame's onBuild was rewriting; with cells changing hands, those can name cells it drew other glyphs in. Tests - FontAtlasPacker: the packer with made-up boxes and no device: the fill order, least recently drawn first, framesInFlight + 1 frames held, nothing without evict, wide glyphs, tall glyphs spanning shelves and the free rows, Touch past a stale handle, stale handles, dirty spans, and the invariants over a random workload placed the way the atlas places. - FontAtlasEviction: fills the atlas through ShapeText frame by frame. The line that no longer fits keeps its advance and its glyphs' places; its missing glyphs aren't rasterised while nothing can go; when only line 0 is cold, only its glyphs give way; a frame's own misses evict nothing in it; a frame later the line and twenty more are whole, pixel for pixel, line 0 losing a larger share than the glyphs drawn after it; a stale cached run is shaped again; no stale texel is left; InvalidateFont reaches the atlas. Against master, with its two calls to the new API stubbed out, an earlier version failed ten checks. - UIDispatchAtlasFlush: a custom ui.Dispatch after ShapeText leaves the atlas uploaded. Needs a compositor. - ShapeTextCache: a hit rasterises nothing; after InvalidateFont the re-shaped run draws the same, from new cells. Docs - README and AGENTS.consumer.md: the atlas's eviction, the frame a glyph waits, shaping text every frame into per-frame item buffers, InvalidateFont, EnsureAndGet's signature and Lookup's lifetime, and that the atlas, like the deletion queue, assumes one rendering window and evicts nothing without one. - AGENTS.md: FontAtlas::Packer among the pure-logic helpers, eviction without a Window, and InvalidateFont. Breaking: FontAtlas::EnsureAndGet returns const Glyph& and never fails; check Glyph::Resident() before drawing from its UVs. FontAtlas::Touch is no longer noexcept. A GlyphItem is good for the frame it was shaped for, once the atlas has filled up, and item buffers must be per frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
| .. | ||
| CustomShader | ||
| Decompression | ||
| HDRBloom | ||
| HelloDom | ||
| HelloUI | ||
| HelloWindow | ||
| InputSystem | ||
| RayQueryPick | ||
| RTMultiShadow | ||
| RTStress | ||
| RTVolume | ||
| Sponza | ||
| SponzaBench | ||
| VulkanTriangle | ||
| LICENSE | ||
| README.md | ||
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.