Forward-looking work, roughly ordered by what unblocks or de-risks the most. Present tense; this is intent, not history.
The toolchain is pinned to nightly-2026-05-01 (rustc 1.97). atomics + -Z build-std are nightly-only (still true in 2026 — unavoidable, not debt), so a nightly pin is required.
WASM threads on a current nightly require the full link-arg set in .cargo/config.toml. On modern nightlies lld no longer auto-exports the TLS-init symbols, and wasm-bindgen-rayon needs an imported shared memory. The wasm target therefore builds with target-feature=+atomics,+bulk-memory plus link args --shared-memory --max-memory=1073741824 --import-memory and explicit --export= of __heap_base, __wasm_init_tls, __tls_size, __tls_align, __tls_base — mirroring wasm-bindgen's own threading reference build. Without the --export=__wasm_init_tls family, wasm-bindgen fails with failed to find __wasm_init_tls; without --import-memory/--shared-memory, initThreadPool fails at load (DataCloneError: #<Memory> could not be cloned). The CI workflow's pinned nightly must stay in sync with rust-toolchain.toml: the workflow installs rustfmt/clippy/wasm components for its own pinned value, so a mismatch fails the build.
Remaining dep bumps, scouted but not yet done — each is independent and should be landed and browser-verified on its own:
- Drop the
indexmap = "=2.2.6"exact-pin (the current nightly accepts current indexmap; it's only a transitive of wgpu/naga). hecs0.9 → 0.11: query iterators now yieldQ::Iteminstead of(Entity, Q::Item), andEntityimplementsQuery— addEntityto the queries that need the id and flatten the bindings. Alsorand0.8 → 0.9 (touchessrc/simulation/rng.rs'sFastRng: RngCore/SeedableRngand the systems).wgpu24 → 29: mechanical churn in bothsrc/web/webgpu.rsandsrc/web/postprocess.rs(the bloom post-process) — by-valueInstanceDescriptor(noDefault),request_adapterreturnsResult,request_devicetakes one arg,bind_group_layouts: &[Option<&_>]+immediate_size,multiview→multiview_mask,get_current_texturereturns theCurrentSurfaceTextureenum, andRenderPassColorAttachment.depth_slice. wgpu 29 forceswasm-bindgen0.2.122 (the current 0.2.100 + 0.2.122's threading transform both still look up__wasm_init_tls, so the link-arg set above already covers it).
The world.get::<&T>() storm is gone: the grid rebuild also builds a per-tick NeighborCache (systems/mod.rs) of each entity's hot fields, so movement, interaction, and nearest-N selection read one cached snapshot per neighbour instead of a scatter of component fetches. The grid itself is a HashMap of cell → entities, rebuilt serially.
What remains is a smaller, more invasive refinement: replace the cell HashMap with a dense fixed grid built by counting sort (lock-free, contiguous per-cell runs, no hashing), keyed by a dense slot id so neighbour data is fully contiguous (true SoA). The world is bounded, so a flat grid fits. This is the step from "good locality" to "optimal locality" — measure it against the headless bench harness before taking on the churn.
The crate is cdylib-only, so there's no native way to profile or benchmark. Add "rlib" to crate-type, an examples/headless.rs that runs N ticks from a fixed seed (now possible — the sim is deterministic), and criterion benches over the per-tick hot loop. Prerequisite for measuring any of the performance work above.
Reaching 100K–1M is a separate GPU-compute engine, not an optimization of the current one: ping-pong storage buffers, a counting-sort spatial grid + prefix sum on the GPU, force/movement compute shaders, and indirect draw. CPU + rayon cannot reach that scale. This sim's per-entity logic (predation, energy transfer, births/deaths = structural mutation needing GPU compaction) makes the port heavier than a plain particle-life sim. Revisit only after the toolchain bump, and only if the scale is genuinely wanted.
- Replace the
sedcache-busting.scripts/build-web.shinjects a git-SHA?v=query via a chain ofsedrewrites — brittle string-surgery on generated output, and largely redundant withweb/_headers. As a first step, deduplicate it: run every rewrite onpkg/first, thencp -r pkg web/last so the web copy inherits the patched files (removes the duplicatedweb/pkgrewrite blocks). Longer term, fix the one load-bearing worker-import path viawasm-bindgen-rayon'sno-bundlerfeature, or move to content-hashed filenames / an import map. - One wasm-pack source. CI installs wasm-pack via
curlandpackage.jsonlists it as a devDependency;npm run buildwould prefer the npm copy. Pick one (drop the devDependency, or switch the script tonpx wasm-packand drop the curl step) so the build uses a single known version.
The public URL is the custom domain https://evo.tre.systems (in the README). The underlying Cloudflare Pages project is evo-dgc (Cloudflare suffixed evo because the name was taken) and evo-dgc.pages.dev also serves the app. Optional cleanup: rename the Pages project to reclaim evo.pages.dev, or leave it — the custom domain is the canonical entry point.
- WebGPU-unavailable UX. Replace the 5-second error toast with a persistent "WebGPU required" message, and request
downleveldevice limits so low-end adapters degrade rather than fail. (The renderer is WebGPU-only — thewgpuwebglfeature has been dropped — so a real WebGL2 fallback would be a deliberate re-addition, only worth it for broad reach.)
- Split
systems/movement/mod.rs(~390 lines, over the 200-line guideline). Extract the neighbour-accumulation loop and the flocking/solitary force helpers fromupdate_movement. Pure refactor — preserve arithmetic order so seed-determinism is unchanged. (simulation/mod.rsalready had its seeding helpers pulled intosimulation/rng.rs.) - Trim unused stats.
SimulationStats::from_worldcomputesentity_counts, per-colour classification, several averages, andworld_center_drifton everyget_stats(), but the UI consumes onlytotal_entities. Either surface them in the UI or trim the struct (crosses the WASM boundary, so updatelib.rs/JS/tests together). - Faster hot-loop RNG. The per-entity-per-tick RNG is
StdRng(ChaCha, cryptographic-grade). A small fast PRNG (thesplitmix64finaliser already inmix_seed, or wyrand/pcg) would cut hot-loop cost and shrink therand/getrandomfootprint, keeping determinism. Deferred deliberately: it changes every seed's output (the algorithm is unchanged, but each seed maps to a different run), so the curated default seed and any shared seeds need re-curating — a poor trade for a marginal speedup on a visual piece, and best done as its own visually-verified pass.
- Split the remaining event wiring. Sliders are now table-driven (the
SLIDERStable →setupSliders()), butsetupEventListenersstill inlines the panel-drag, keyboard, and camera wiring; extract those into focused methods too. - Consider TypeScript + prettier for
web/js/app.js(bundle via a build-step migration, not standalone).
- Convert the remaining drift/bias harnesses to assertions. The simulation-level ones (
test_simulation_clustering,test_drift_direction_analysis) now assert bounded drift via a sharedcentroidhelper, and the no-optest_simulation_entity_processingis gone. The movement/interaction drift + bias harnesses still onlyprintln!— give them the same treatment (assert "drift < ε over N ticks"; seed any that usethread_rng). - Property tests (
proptest) for gene mutation bounds, energy clamping, and HSV↔RGB round-trip. - A single Playwright smoke test in CI (page loads, canvas present,
#seed-displayset, no console errors).
Longer-horizon mechanics, in rough order of appeal: environmental terrain and localized resource patches; aging and disease/parasites; mating rituals and territorial behaviour; and multi-species symbiosis / food webs. Each extends the systems in src/systems/ and the gene model in src/genes/.
A change is done when:
- It passes the verification gate in AGENTS.md (the pre-commit hook enforces it).
- Docs describing affected behaviour are updated to match.
- For user-visible changes: pushed, CI green, and smoke-tested on the live site.