Cleaning House: Architecture, Logging, and 67 New Tests
Sometimes the best feature work is the work nobody sees.
I’d been moving fast, core engine, renderer, editor, output modes, all in a few weeks. The code worked, but it was getting messy. Crate boundaries were blurred, god methods were growing, and I had exactly zero logging.
Time to clean house.
Crate boundaries
Lux has four crates with strict dependency rules:
lux-core (no deps on other lux crates)
lux-render (depends on lux-core only)
lux-ui (depends on lux-core only)
lux-app (depends on everything, it's the composition root)
The key rule: lux-render and lux-ui don’t know about each other. The renderer doesn’t know about egui panels. The UI doesn’t know about wgpu. lux-app is the only place where they meet.
I’d let some things leak, a render type used in UI code, a UI concept referenced in the renderer. Fixed all of it. Boring work, important work.
Structured logging
This one’s a core tenet now. Every new code path gets logging. Why? Because when someone posts a bug report, the first thing I’ll ask for is their log output. Logs need to contain enough context to diagnose the problem without access to the running app.
I use Rust’s log crate with targeted messages:
log::trace!(target: "lux_core::eval", "auto-spread node {}: {} iterations", id, n);
log::debug!(target: "lux_app", "frame {}: avg {:.1} fps", count, fps);
log::error!(target: "lux_core::eval", "node {} panicked: {}", id, msg);
The target strings enable granular filtering. Want just evaluation timing? RUST_LOG=lux_core::eval=trace. Want everything? RUST_LOG=debug.
F9 toggles between Info and Debug level at runtime. No restart needed. You’re performing live, something looks wrong, hit F9 and the debug output starts flowing. Hit F9 again to quiet it down.
On startup, the app logs GPU adapter info, driver version, surface format, window size, scale factor, OS, and architecture. Every log paste starts with enough context to diagnose platform-specific issues.
GpuContext extraction
This was the sneaky-important change. I pulled all GPU resources out of RenderState into a standalone GpuContext struct with a new_headless() constructor.
Why? Testing.
let gpu = GpuContext::new_headless(800, 600).await;
// No window, no display server, no Xvfb
// Uses wgpu's software adapter (Mesa llvmpipe)
// Same shaders, same code paths, just software-rasterized
Now my GPU tests run everywhere, CI, Docker, headless servers, my laptop at a cafe. cargo test --workspace just works. The VelloBackend, BlitPipeline, render targets, all tested against the real GPU API, just software-rasterized.
67 new UI tests
The editor UI was untested. Inspector panel, browser panel, search popup, canvas interactions, all visual, all tested by “does it look right?” That’s fine for prototyping, not fine for maintenance.
I added 67 tests covering:
- Inspector rendering for each pin type
- Browser category grouping
- Search scoring and fuzzy matching
- Editor state management
- Node and wire rendering
These aren’t screenshot tests, they’re logic tests. Does the fuzzy scorer rank “LFO” higher than “AudioFilter” when you type “lf”? Does the inspector create the right widget for a Color pin? Does the browser group nodes by category alphabetically?
Was it worth stopping?
Absolutely. The foundation is solid now. The tests will catch regressions. The logging will help debug issues. The crate boundaries will keep the code reviewable as it grows.
Next up: spreads, wire cutting and undo. The fun stuff. But it’ll go in cleaner because I did this work first.
Not glamorous. Very necessary.