Library M5: the shared WGSL and henad-build¶
M5 is the fifth of the eleven milestones of #48, which turns Henad into a library published on crates.io. The session first folded in the maintainer's review of M4, then moved the five shared WGSL modules into henad-core and renamed their import root from
shared::tohenad::. A new crate, henad-build, runs from the build scripts of henad-compute, henad-models and henad-app, andinclude_shaders!brings the generated code into each. henad-models finds its shaders by itself, a binding line in an unread form fails the build, and the grid engine'sDimsis generated rather than written by hand. Every shader the engines compile is byte for byte the shader 773a7a5 compiled, once the import names are mapped. A review of M5 before its commit then closed ten findings, from a stale stamp to names the generated code shadows.
State before¶
48-library stood at cdaa134, M4's commit, with a clean tree.
The shared WGSL sat in crates/henad-compute/src/gpu/shared/, imported as shared::prelude, shared::space and so on, and henad-models' build script read it through ../henad-compute/src/gpu.
Three build scripts each drove wgsl_bindgen by hand, henad-models listed its seventeen entry points in ENTRY_POINTS, and only henad-models generated binding_decls.
The binding parser kept lines that opened with @group(0) and skipped any other without a word.
henad_compute::shader_bindings was public, the grid engine's Dims was written by hand, and henad-models asserted it against the generated struct.
The maintainer's review of M4 listed five items.
What was done¶
The M4 review¶
lookup_messagereturns the capitalised error with no full stop. The four slots that embed it ("Spec load failed: ...", "... start failed: ...", "Resume failed: ..." and the runs table's refusal) stay as they were. The first pass added the stop inopen_run, whose message stands alone in the Results status line, and the M5 review took it out again.select_modelloads the defaults fromself.models.get(id), and an id the set lacks leavesparam_valuesandpending_reloadempty.load_default_params, whose one caller it was, is folded into it.ModelSet::runnable(gpu)lists the entries a host with or without a device can run.lookupfinds its entry through it, and the app's and the CLI'sofferedfunctions are gone in favour of it.AppState::offered_modelscalls it.- The CLI's crate doc is rewrapped within 120 columns, and its
ModelSetlink no longer needs an import.wgpu_configurationanddevice_descriptoropen with "Returns". - A GPU model without compute reads "Model 'x' needs a GPU with compute support, and this device has none", and
NO_MODEL_RUNSends "GPU models need a GPU with compute support." The test, the CHANGELOG, AGENTS.md and the design's [4.4.6] say the same.
The shared WGSL in henad-core¶
dims,rng,spaceandreduce_treemoved bygit mvintocrates/henad-core/src/authoring/primitives/wgsl/, andprelude.wgslbecamedispatch.wgsl. Each declareshenad::<module>, and all 31#import shared::lines in henad-compute and henad-models now importhenad::.wgsl/mod.rsholdsSharedModule,SHARED_WGSL_MODULESandSHARED_WGSL_FNV1A64. The hash runs at compile time throughFnv1a64, whosewrite,write_u64,write_strandfinishbecameconst fn. Two tests check each module's first line and the const hash against one computed at run time.parity.wgslmoved tocrates/henad-compute/src/gpu/tests/, lost its import path, and declaresCODES, an array referencingTORUS,BOUNDED,MOORE_ROW_MAJOR,MOORE_COLUMN_MAJORandVON_NEUMANN. All five now appear inshader_bindings::henad::space, where the parity test reads them ascodes::*.
henad-build¶
ShaderBuild::discover(root)walks the root and takes every.wgslfile without a#define_import_pathline as an entry point.ShaderBuild::new(root).entry_point(path)names them one by one, as henad-compute does.paths.rsrefuses a path component that is no ASCII identifier or is a keyword, ahenad.wgslfile or ahenaddirectory holding a.wgslfile, and a root namedhenad, each in any case. It refuses a first path component the generated code uses at its root:wgpu,bytemuck,std,core,alloc,_root,ShaderEntry,layout_assertsandbytemuck_impls. It refuses two entries that give one module path, oneShaderEntryvariant (computed with heck'sto_pascal_case, aswgsl_bindgendoes) or one binding constant, and an entry whose module holds another's. Ahenaddirectory with no.wgslfile is allowed, and a#define_import_path henad::...line elsewhere draws acargo:warning.generatecopies the shared modules toOUT_DIR/henad_wgsl/henad/, hands that folder towgsl_bindgenas a scan directory, and runs it withemit_rerun_if_change(false). It prints onecargo:rerun-if-changedfor the absolute shader root, plus one per entrynewnamed, in the single-colon form, which keeps a downstream crate's MSRV below 1.77. It skipswgsl_bindgenwhen an FNV-1a hash of its inputs (henad-build's version, the pinnedwgsl_bindgenrelease, the source ofoutput.rs, the shared hash, the root, the entries and every.wgslfile under the root) matchesshader_bindings.stamp.scripts/check_packaging.shholdsWGSL_BINDGEN_VERSIONequal to the workspace's=0.23.3pin. It writes every file only when its bytes change, and callsgenerate_stringitself in place ofwgsl_bindgen's own write. A crate with no entry points gets an emptyshader_bindings.rsand abinding_decls.rswith the hash and an emptybindingsmodule, and its stamp is removed.binding_lines.rsis the private reader. It strips block comments (nested ones included) and line comments, then refuses every line holding@binding(or opening with@group(that is not@group(G) @binding(N) var<...> name: Type;on one line, naming the line and the reason. It readsuniform,storage,storage, read,storage, read_writeandtexture_storage_*, refuses a sampled texture or a sampler, and skips groups other than 0. A module that is no entry point and holds a@binding(line fails withModuleBinding, which says bindings belong in the entry shader.output.rswritesbinding_decls.rswithuse super::{BindingDecl, BindingKind}and, for every entry with group-0 bindings, aconst _: () = ::core::assert!holding its list toWgpuBindGroup0::LAYOUT_DESCRIPTOR.entries.len().wgsl_bindgenis pinned at=0.23.3in the workspace table, and henad-build alone depends on it. heck joins the workspace table, already in the lock throughwgsl_bindgen.ShaderBuildError'sDebugwrites itsDisplay, and a build script that returns it asBox<dyn Error>shows the guidance.- Thirteen tests in
src/tests/: the four path tests the design names,a_path_starting_with_a_generated_name_is_refused,an_import_path_under_henad_elsewhere_draws_a_warning, three reader tests witha_binding_in_a_module_is_refused, anda_second_build_reruns_nothing,a_shader_added_between_builds_is_generated,a_crate_without_shaders_buildsandshaders_removed_and_restored_are_generated_again. None runs Cargo. The parser test reads the generated layout's@binding(N): "name"doc lines and binding types back out ofshader_bindings.rs, and found thatwgsl_bindgenlists layout entries in declaration order, not by index.
henad-compute, henad-models and henad-app¶
include_shaders!and the hidden__shader_supportmodule sit in henad-compute'slib.rs, as the design's [4.6.3] gives them, and henad-compute calls the macro on itself. The macro namesinclude!,concat!andenv!through::core, and both allow lists carrysingle_use_lifetimesbeside the design's list.shader_bindingsis therefore private to each crate.- henad-compute's build script names seven entry points under
src/gpu: the five primitives,tests/parity.wgsl, and the newgrid_dims.wgsl, a never-dispatched pass whose only job is to referencehenad::dims::Dims.grid_engine.rs'sDimsis apub(crate)alias of the generated struct, and henad-models' layout assertion against it is gone. - henad-models' build script is
ShaderBuild::discover("src")?.generate()?, in ashader_buildregion the docs include, andENTRY_POINTSwith itsentry_pointsregion is gone. - henad-app's build script runs
discover("src/ui")beside its commit stamp, which M7 moves. Itsagents.wgslandedges.wgslnow have binding declarations too, unused. scripts/check_packaging.shlost its exemption for henad-models' climbing path, anddeny.tomlties the fxhash advisory to henad-build's pin.
The gate¶
Shader check.
A worktree of 773a7a5 and the M5 tree were each built --release into a scratch target directory.
A script extracted every SHADER_STRING from the three crates' OUT_DIR/shader_bindings.rs, decoded each X_naga_oil_mod_X<base32>X suffix, mapped shared::prelude to henad::dispatch and shared::<m> to henad::<m>, and encoded it again.
All 24 shipped entry points (5 primitives, 17 model shaders, agents and edges) matched byte for byte.
The differences were the expected ones: grid_dims is new, shared::parity became tests::parity, and the old shared::prelude, shared::dims and shared::space entry points are gone.
The two parity shaders differ only in the CODES array and the order of two imported constants.
HENAD_DUMP_WGSL.
Both release CLIs ran each GPU model for three steps with --export-stats and a dump directory.
Each wrote the same 23 files, and with the same name mapping every file matched byte for byte.
The raw dumps therefore differ only inside the mangled import names.
Codes. shader_bindings::henad::space holds all five codes the parity test reads, and the parity test passes on the Metal device.
Verified packaging. cargo package --workspace --exclude henad-tutorial --locked --allow-dirty packaged and built all seven crates from their tarballs, henad-build included (16 files, 29.0 KiB compressed).
The first two runs failed to verify henad-build with E0432 on primitives::wgsl.
Cargo had kept its extraction of the overlay registry's henad-core 0.2.0 from M1's run, at ~/.cargo/registry/src/-706874a096166f48/, and never extracted the new tarball over it.
Removing that copy was not enough: the new extraction held wgsl/, and the verify failed the same way.
The likely cause is the henad-core the earlier verify had compiled into target/, since Cargo never rebuilds a registry crate on a source change (inferred, not traced).
A run with a fresh CARGO_TARGET_DIR passed.
The cargo-level checks¶
The downstream job's steps 7, 13 and 15 [5.4] ran by hand on a scratch crate in the session's scratch directory, outside any git work tree.
The crate depended on henad-core, henad-compute and henad-build at =0.2.0, patched through [patch.crates-io] to the unpacked tarballs, and cargo metadata --locked showed all three with a null source.
It held a CPU model and a GPU model copied from Game of Life, a src/bin/my-model-cli.rs, ShaderBuild::discover("src") and include_shaders!(), under unsafe_code, unreachable_pub and unused_qualifications denied.
- Step 7. Two
cargo build -v --lockedruns in a row: the first built with no warning, and the second reported every unit Fresh, with no Dirty line, no Compiling line and no build script run. A comment appended tosrc/vote.rsthen madevotealone Dirty and reran its build script, which rewrote nothing: the generated files kept the first build's time. - Step 13. A new
src/tally/count.wgsl, referenced by no code, compiled undercargo clippy --all-targets -- -D warningsand underRUSTFLAGS="-D warnings", andshader_bindings::tally::countandTALLY_COUNTappeared. - Step 15. With the GPU model, its insert line and the extra shader deleted, the crate built clean with
ShaderBuildandinclude_shaders!still in place, and both liveOUT_DIRs held an emptyshader_bindings.rsand abindingsmodule with no constants. With theShaderBuildline andinclude_shaders!deleted as well, it built and ran. The sweep, resume andBuildChangedhalf of step 15 waits for M7's stamps.
After the M5 review, the three steps ran again on a fresh scratch crate against tarballs packaged from the reviewed tree.
The build script printed cargo:rerun-if-changed=<root>/src alone, the second build was Fresh throughout, the unreferenced shader compiled under clippy with -D warnings, and the CPU-only crate built with an empty shader_bindings.rs and no stamp.
With the GPU model and the shader put back, the next build regenerated the full bindings and the CLI listed both models.
Docs¶
authoring/shaders.mdshows the build script andinclude_shaders!by include, states the naming rules, the reserved root, the import-path rule and the one-line binding form, and corrects line 28: an imported constant reaches the bindings exactly when an entry point references it. A note says to denyunsafe_coderather than forbid it.- The GPU Game of Life page lost its "Add our three entries" step and the
entry_pointsinclude, and shows the build script instead. The GPU ants page lost its four-entry list, and both pages importhenad::. reference/primitives.md,authoring/gpu-grid-models.md,authoring/gpu-agent-models.mdanddeveloping/gpu-backend.mdname the new paths.developing/architecture.mdand the home page count seven crates, and the graph draws henad-build as a build dependency.- AGENTS.md has henad-build in its diagram with a crate bullet, seven crates, the rule with henad-build in it, the new home of the shared WGSL,
grid_dims.wgslandCODES, the three meanings ofprimitives, and the generated-bindings paragraph rewritten for henad-build andinclude_shaders!.
The M5 review¶
The maintainer reviewed M5 before its commit and reproduced ten findings on a scratch crate.
- A crate whose shaders all went kept its stamp, and their return matched it and left
shader_bindings.rsempty. The empty branch now removes the stamp, pinned byshaders_removed_and_restored_are_generated_again, which fails with the removal disabled. - A first path component of
wgpu,bytemuck,std,core,alloc,_root,ShaderEntry,layout_assertsorbytemuck_implsis refused asReservedName, which now carries the name. henadis compared witheq_ignore_ascii_casefor the root, directories and file stems, and for the warning on a#define_import_pathline.- A
@binding(line in a module fails the build. ShaderBuildError'sDebugdelegates toDisplay.- The rerun line and the warning use the single-colon form.
single_use_lifetimesjoins both allow lists,output.rs's own source joins the stamp, andscripts/check_packaging.shtiesWGSL_BINDGEN_VERSIONto the pin. A changed constant fails the script, as a run with0.23.4showed.open_runreports the lookup message without a full stop, as every other slot does.include_shaders!and the generated assertions name their macros through::core.- The AGENTS.md lines over 100 columns are rewrapped, and [5.5] says why the verified packaging needs a fresh
CARGO_TARGET_DIR.docs/developing/releasing.mddoes not exist yet.
Edited tree¶
.
├── AGENTS.md ~ M4 review messages, henad-build, the shared WGSL, generated bindings
├── CHANGELOG.md ~ Unreleased: runnable, the reworded message, M5's Added, Changed, Removed
├── Cargo.toml ~ henad-build and heck, wgsl_bindgen pinned at =0.23.3
├── Cargo.lock ~ henad-build
├── deny.toml ~ the fxhash reason
├── zensical.toml ~ nav entry #34
├── scripts/check_packaging.sh ~ no climbing exemption, the wgsl_bindgen pin check
├── dev-docs/48-library/design-v6.md ~ [4.4.6] wording, [5.5] fresh CARGO_TARGET_DIR
├── crates/
│ ├── henad-core/src/
│ │ ├── explore/fingerprint.rs ~ const fn writes
│ │ └── authoring/
│ │ ├── model/gpu_agent_model.rs ~ henad:: imports in the docs
│ │ └── primitives/
│ │ ├── mod.rs ~ pub mod wgsl
│ │ └── wgsl/ + mod.rs, and dispatch, dims, rng, space, reduce_tree (moved)
│ ├── henad-build/ + Cargo.toml, README.md, licences
│ │ └── src/
│ │ ├── lib.rs + ShaderBuild, ShaderBuildError
│ │ ├── paths.rs + the walk and the name checks
│ │ ├── binding_lines.rs + the binding reader
│ │ ├── output.rs + the generated files and the stamp
│ │ └── tests/ + mod.rs, support.rs, paths.rs, binding_lines.rs, generate.rs
│ ├── henad-compute/
│ │ ├── Cargo.toml, build.rs ~ henad-build, ShaderBuild::new
│ │ └── src/
│ │ ├── lib.rs ~ include_shaders!, __shader_support
│ │ ├── entry/set.rs ~ runnable, the NeedsGpu message
│ │ └── gpu/
│ │ ├── grid_dims.wgsl + Dims into the bindings
│ │ ├── grid_engine.rs ~ Dims a generated alias
│ │ ├── shared/ - moved to henad-core and tests/
│ │ ├── primitives/*.wgsl ~ henad::dispatch
│ │ ├── primitives/dispatch.rs ~ WORKGROUP from henad::dispatch
│ │ └── tests/parity.{wgsl,rs} ~ moved, CODES, henad:: paths
│ ├── henad-models/
│ │ ├── Cargo.toml, build.rs ~ henad-build, discover
│ │ └── src/
│ │ ├── lib.rs ~ include_shaders!, no Dims assertion
│ │ ├── gpu_*/*.wgsl ~ henad:: imports
│ │ └── tests/tutorial/gpu_foraging.rs ~ henad::rng
│ ├── henad-app/
│ │ ├── Cargo.toml, build.rs ~ henad-build, discover
│ │ └── src/
│ │ ├── lib.rs ~ include_shaders!
│ │ ├── init.rs ~ "Returns" docs
│ │ ├── state.rs ~ runnable, select_model, lookup_message, the test
│ │ └── ui/model.rs ~ NO_MODEL_RUNS
│ └── henad-cli/src/main.rs ~ runnable, the crate doc rewrapped
└── docs/
├── index.md ~ seven crates
├── authoring/{shaders,gpu-grid-models,gpu-agent-models}.md ~
├── developing/{architecture,gpu-backend}.md ~
├── developing/agent-record/20261001-34-library-shaders.md +
├── guide/first-model/{gpu-game-of-life,gpu-ants}.md ~
└── reference/primitives.md ~
Hot paths¶
M5 changes no kernel and no tick.
The compiled shaders are byte-identical once the import names are mapped, and Dims has the layout the hand-written struct had: [u32; 2] twice, now with the generated align(8).
The build adds a directory walk and a hash to each rerun of a crate's build script.
State after¶
M5 is implemented and uncommitted on 48-library, on top of cdaa134.
The workspace version stays 0.2.0.
HENAD_REQUIRE_GPU=1 ./check.shpasses: 991 tests, none failed, with the packaging, cargo-deny and docs steps and the web build. That is M4's 975, plus henad-build's thirteen tests and its doc test, and the two tests ofwgsl/mod.rs. Before the M5 review it passed at 988. The first run after the review stopped at clippy, on two assertions in the new tests without a closing;.uv run --locked zensical buildpasses with its path checks.- The shader check and the dump comparison match every shipped shader, and the codes are in the bindings.
cargo package --workspace --exclude henad-tutorial --locked --allow-dirty, in a freshCARGO_TARGET_DIR, verifies all seven crates from their tarballs, before the M5 review and after it.- Steps 7, 13 and 15 of the
downstreamjob pass by hand on a scratch crate, before the M5 review and after it. - A release app with
--features inspection, driven through the egui MCP server, switched from SIR Epidemic to Boids Flocking (GPU) with that model's parameters loaded, and Build built it with the GPU pacing controls and no fault. The app's saved state was backed up before and restored after.
Issues found & future directions¶
- A stale overlay extraction.
cargo package --workspaceextracts its overlay registry under~/.cargo/registry/src/at a path that depends on the target directory alone, and keeps that extraction across runs of one version. A verify of a later tree then compiles the earlier tarball's henad-core, and the compiled copy intarget/survives deleting the extraction. The design's release checklist [5.5] now runs the hand-run verification in a freshCARGO_TARGET_DIR, anddocs/developing/releasing.mdtakes the line when it is written. - A sampled texture is refused. The reader knows the three
BindingKinds, and a downstream render shader with atexture_2dor asamplerunder the shader root fails the build. henad-compute's ownview/display.wgslstays outside its entry list for that reason. A render layer of a model's own would need either another root or a fourth kind. - The app's binding declarations are unused.
binding_declsforagents.wgslandedges.wgslexist because every crate now gets them, anddead_codecovers them. - Next. M6 (the programmatic API) depends on M3, and M7 (the stamps) on M3 and M5, and puts
stamp_commitinto henad-build.