Skip to content

Cleanroom - #5

Merged
bv7dev merged 30 commits into
mainfrom
cleanroom
Oct 6, 2026
Merged

bv7dev merged 30 commits into
mainfrom
cleanroom

Conversation

@bv7dev

@bv7dev bv7dev commented Oct 6, 2026

Copy link
Copy Markdown
Owner

AI overhaul

bv7dev and others added 30 commits October 5, 2026 22:50
The lab was pinned to Dawn chromium/6670 (Aug 2024), fetched by a Python
script that cloned Dawn, glm and tinygltf into libs/ and built Dawn from
source. That setup only ever worked on Windows and no longer configures
with current toolchains.

Build system
- Dawn is pinned to release v20261002.154047 in cmake/LabDawn.cmake.
  LAB_DAWN selects where it comes from: the prebuilt archive Dawn's CI
  publishes (default), a source build, or an installed Dawn.
- GLFW 3.4+ is taken from the system if present, fetched otherwise.
- Only dawn::webgpu_dawn is linked; the Dawn-internal targets and the
  proc table setup are gone.
- CMake presets (dev, release, dev-asan) replace the manual steps.
  Samples are only built when the lab is the top-level project.
- The Python scripts are removed.

Port to the current webgpu_cpp.h
- Surfaces are created by our own glue (src/platform) from GLFW's native
  handles, for Wayland, X11 and Win32. No packaged Dawn ships webgpu_glfw.
- Webgpu no longer opens a hidden window to probe the surface format (it
  did so with an instance that was not created yet) and no longer needs
  GLFW at all.
- Vertex format sizes are a switch instead of a table indexed by enum
  value; the enum has been renumbered since.
- Surfaces are sized in framebuffer pixels and the resize callback
  reports pixels.
- StringView, ShaderSourceWGSL, TexelCopy*, the new callback signatures,
  SuccessOptimal/SuccessSuboptimal and reordered descriptor fields.
- A frame without a surface texture is skipped instead of rendered to.

The library builds without warnings with GCC 16 and Clang 23.
C++23 is now required.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closing the last window called glfwTerminate(), which on Wayland drops
the display connection. The device releases its swapchain later, when
Webgpu is destroyed, and then talked to the dead connection: every
windowed sample crashed on exit.

A closed window is now only hidden and unregistered. GLFW destroys the
native windows and shuts down at program exit, after all GPU objects.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With the environment variable set to N, lab::tick() closes all windows
after N calls. Every sample can then be run by a script or CI:

    LAB_EXIT_AFTER_FRAMES=120 ./sample_texture

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Missing designated initializers and unused lambda captures, reported by
GCC 16 and Clang 23 with -Wall -Wextra.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- launch.json gets a gdb configuration next to the MSVC one, so "F5 runs
  the sample that is open" works on both.
- settings.json uses the CMake presets.
- c_cpp_properties.json is removed: it hard-coded a Windows/MSVC
  IntelliSense setup (see issue #3).
- .clangd points clangd at the compile commands of the dev preset.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- add_uniform_buffer() created the layout entry at binding 0 whatever
  binding index it was given. Unnoticed so far because every sample
  binds its uniforms at 0.
- The default render function passed firstVertex and dropped
  firstInstance when calling DrawIndexed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The label was kept as a const char* that could outlive its string.
- current_capacity was read uninitialized on an empty buffer.
- Buffer<T> copies the bytes of T to and from GPU memory, so T now has
  to be trivially copyable. test_buffers.cpp, which uploaded
  std::string objects, is removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
It used to print a message and carry on with an empty shader, which
surfaced much later as a confusing pipeline error. The exception names
the file and the working directory, since a relative path that does not
resolve is the usual cause.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A header should not add a using-directive to the global namespace.
Code that wants `100ms` writes `using namespace std::chrono_literals;`
itself. Also rename two template parameters that used reserved names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Makes the lab usable on CI machines and in VMs. The adapter in use is
printed at startup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- sample_uniform_buffer joined a thread running `while (true)`, so the
  process never ended after its window was closed. The timer is now
  restarted from the main loop.
- test_windows called destructors explicitly on objects that were
  destroyed again at the end of their scope, one of them from inside a
  callback owned by the object it destroyed.
- sample_buffer shared plain bools between two threads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- tests/ holds doctest tests that need a GPU or software adapter but no
  window. The render tests draw into a texture and compare pixels:
  a triangle over the clear color, vertex attributes, a uniform buffer
  at a non-zero binding, indexed and instanced drawing.
- Every sample is registered as a CTest test that has to run 60 frames
  without printing an error. These open windows and carry the label
  "samples", so `ctest -LE samples` runs headless.
- CI runs the headless tests on a software adapter.
- The dev-asan preset aborts on undefined behaviour. Its leak detection
  is off because GPU drivers leak a little when they are unloaded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
LAB_DAWN=source cloned Dawn with GIT_SHALLOW. CMake's shallow clone
still fetches the tip of every branch and tag, and Dawn has thousands
of them, so the clone took longer than building Dawn. The source archive
of the pinned tag is 27 MB.

Both Dawn downloads now fail after 60 seconds without data instead of
hanging the configure step forever.

Building Dawn from source this way takes about five minutes on a
32-core machine and all tests pass against it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Headers move from src/objects and src/extra to a flat include/ and
sources from src/detail to src/. Only include/ is on the include path
of projects using the lab, so they no longer see <objects/...>,
<extra/...> or the private lab_state.h.

No code changes besides the include paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The old Pipeline was shader, fixed state, the buffers to draw and the
bind groups in one mutable object, and render_frame() was the only way
to render: one pipeline, one pass, one frame. Anything more meant
copying fifty lines of raw WebGPU, as the graph samples did.

New shape
- lab::Gpu (was Webgpu), lab::Window, lab::Surface, lab::Shader,
  lab::Buffer<T>, lab::Texture, lab::Pipeline, lab::Frame,
  lab::RenderPass. Every object exposes the WebGPU object it wraps
  through handle().
- A Pipeline is immutable and built for one target format. Its
  description holds vertex layouts (lab::vertex<T>, lab::instance<T>)
  and fixed state. There is no finalize().
- What is drawn is a lab::Draw: vertex buffers, index buffer, bind
  groups, counts. Counts default to what the buffers hold.
- Bind groups are plain wgpu::BindGroup values made by
  pipeline.bind_group(); their layouts come from the shader.
- A RenderPass takes any number of draw calls, a Frame any number of
  passes, onto a Surface or into a Texture. render_frame() remains as
  the one-line form.
- Buffer<T>: write(), blocking read(), read_async(), fill in place at
  creation. The lab adds the usage flags these need. The jthread based
  mapping API and MappedVRAM are gone.
- Shader compiles on construction. Files are looked up from the working
  directory and from the directory of the executable.

Fixed by construction
- Objects share reference-counted state instead of holding references
  to each other, so they are movable and can be destroyed in any order.
  A native window is kept until no Gpu can still own a swapchain for it.
- The surface follows its window size when a frame is acquired and no
  longer takes over the window's resize callback. It handles outdated
  and lost surface textures.
- The surface format is chosen per surface, preferring an 8-bit format:
  an HDR capable desktop lists RGBA16Float first.
- Global state shrinks to one private runtime (GLFW, open windows, live
  Gpus, the clock). GLFW callbacks use the window user pointer.

Errors and logging
- Mistakes the lab can detect throw lab::Error where they are made and
  name the object: buffers that do not fit a pipeline's layout, a
  pipeline drawn into another target format, out-of-range writes.
- Creating shaders, pipelines and bind groups turns WebGPU validation
  errors into exceptions on the spot. Errors reported later are thrown
  by the next tick().
- Objects without a label are named after the source location they are
  created at.
- Log output goes through lab::log with levels, a sink and LAB_LOG.

Also new
- Window: polling for keys and mouse (key_pressed, mouse_delta, scroll)
  next to callbacks, framebuffer_size(), aspect().
- lab::delta_seconds().
- Texture::read(), Texture::save_png() and LAB_CAPTURE_DIR, which makes
  every surface save one frame as a PNG.

Samples are rewritten as a numbered sequence, one concept each, with a
shader named after its sample. test_uniform_buffers and test_tinygltf
became tests or went away, as did the unused shaders and the Blender
backup file.

Tests: 31 test cases render into textures and compare pixels, check
the error messages and exercise window, surface and gpu lifetimes in
every destruction order. Tests that open a window carry the CTest label
"display".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Depth
- SurfaceOptions::depth gives a surface a depth buffer that it keeps at
  the size of the window.
- A pass into a texture takes one through PassOptions::depth.
- TargetFormat carries the depth format, so a pipeline for a target
  with depth gets a depth test (PipelineDesc::depth_compare and
  depth_write, by default "less" and writing).

Samplers
- lab::sampler(gpu, {filter, address_mode}) returns a wgpu::Sampler to
  put into a bind group.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- lab::ComputePipeline(gpu, shader), with bind_group() like the render
  pipeline and run() to dispatch and submit in one call.
- lab::ComputePass dispatches within a lab::Frame, so that a render pass
  of the same frame can draw what was just computed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- 10_house finally renders the glTF house from samples/assets: depth
  buffer, orbit camera with mouse and scroll wheel, simple lighting, and
  a ground textured from an image file through a repeating sampler.
- 11_boids simulates a flock in a compute shader and draws it in the
  same frame. Simulation and rendering share the same two buffers.
- 12_offscreen renders into a small texture and shows it magnified in a
  second pass. S saves the texture as a PNG.
- 05_texture reads its texture through a sampler.

lab::find_file() locates files of a program from the working directory
or from next to the executable, the way lab::Shader already does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI gets a job that checks the formatting with a pinned clang-format.
The boids sample gets stronger alignment, which makes flocks form
within seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A render pass onto a surface asked for the depth view and the color
view in one argument list. Getting the color view is what makes the
surface adapt to a new window size, and when the depth view happened to
be evaluated first it was the one of the old size: WebGPU rejected the
frame after a resize. The color view is now fetched first.

Found by a new test that resizes a window with a depth buffer.

Also adds LAB_WINDOW_SYSTEM=x11|wayland to override the window system
GLFW picks. Unsetting WAYLAND_DISPLAY is not enough to get X11 in a
Wayland session, so this is what makes the X11 path testable there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The release build warned that the padded copy could overflow for
absurd sizes. Only the last 1 to 3 bytes need padding, the rest is
written as it is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`emcmake cmake --preset web` builds every sample as a web page, plus an
index page with thumbnails, into build/web/site. A Pages workflow
publishes that from main; the CI builds it on every push.

- WebGPU is the browser's own, through Dawn's Emscripten port of the
  very Dawn release the native build is pinned to
  (cmake/emdawnwebgpu.remoteport.py). GLFW comes from the contrib.glfw3
  port, and the canvas follows the size of the browser window.
- src/platform/lab_surface_web.cpp is the web counterpart of the GLFW
  surface glue: the surface is created from the canvas, presenting is
  left to the browser, and tick() awaits the next animation frame, so
  the `while (lab::tick())` loop of every sample runs unchanged. That
  needs ASYNCIFY, which also covers the lab's blocking waits.
- Exceptions are switched on for the web build; the lab reports
  mistakes with them.
- 09_multi_window is left out on the web, a page has one canvas.

Verified in a headless Firefox 157: the triangle, graph, instancing and
house samples render; the house loads its model and texture from the
packed files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dawn's D3D12 backend compiles shaders through dxcompiler.dll and
dxil.dll (or d3dcompiler_47.dll), and only looks for them in the
directory of the executable: nothing else is accepted by the way it
calls LoadLibraryEx. The Dawn archive does not ship them, so on CI
every test failed to create a device.

cmake/LabWindowsRuntime.cmake downloads the DirectX Shader Compiler
release, takes d3dcompiler_47.dll from the Windows SDK if there is
one, and lab_copy_runtime_dlls() copies them next to a target, which
happens for every executable the lab builds. Projects that use the lab
call it for their own executables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Windows does not delete a file that is still open, which made this the
one test that failed there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@bv7dev
bv7dev merged commit 6198967 into main Oct 6, 2026
10 checks passed
@bv7dev
bv7dev deleted the cleanroom branch October 6, 2026 17:16
Repository owner locked and limited conversation to collaborators Oct 6, 2026
Repository owner unlocked this conversation Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant