indicatrix-web
A browser build of indicatrix: upload a GemCAD .asc cutting schedule, get back an
interactive rendered stone. No design library, no remote worker, no database -- that
scoping is the whole reason this is tractable at all. Dropping the design library sheds
rusqlite/SQLite, HTTP, and TCP entirely; WebGPU consumes the same WGSL the native GPU
path already ships.
Building and serving it
This crate is not built by the rest of this workspace's own tooling (cargo
build/check/clippy --workspace skip its actual work entirely on a native host --
see src/lib.rs's crate doc comment). You need
trunk installed yourself:
Then, from this directory (apps/indicatrix-web):
wasm32-unknown-unknown must already be installed as a Rust target
(rustup target add wasm32-unknown-unknown); nothing else beyond trunk itself.
This build is WebGPU-only -- there is no CPU fallback
This is the single biggest way this crate differs from every other indicatrix consumer
in this workspace. apps/indicatrix-cut and apps/indicatrix-worker both go through
indicatrix::renderer::gpu_backend::GpuBackend, whose whole contract is "try the GPU,
silently fall back to the CPU tracer whenever it declines." indicatrix-web calls
indicatrix::renderer::gpu::frame::GpuFrameRenderer directly instead (see
src/render.rs's module doc comment for the full reasoning) and has no fallback of
its own, for two compounding reasons:
wasm32-unknown-unknownhas no OS thread.indicatrix's CPU tracers (the onesapps/indicatrix-cutandapps/indicatrix-workeruse, and the patternapps/indicatrix-worker/src/render_core/mod.rs::trace_intodocuments) parallelize withstd::thread::scope, which has no thread to spawn here -- real multi-threading in a browser tab needsSharedArrayBufferplus cross-origin-isolation response headers (Cross-Origin-Opener-Policy/Cross-Origin-Embedder-Policy) pluswasm-bindgen-rayon. This crate deliberately does not take that on: it is a real amount of infrastructure (server-side header configuration this crate doesn't control, since it's served as static files) for a browser demo, not a decision to make silently inside a rendering crate.- Even granting a single-threaded software ray tracer, it would be slow enough in a
browser tab to be a worse experience than a clear "this browser can't run
indicatrix-web" message. So there isn't one, not even a slow one.
What this means in practice: on a browser or machine with no usable WebGPU (an
older browser, WebGPU disabled behind a flag, or a machine whose driver stack the
browser declines to expose), indicatrix-web shows a first-class error explaining exactly
that, naming what to check (a recent Chrome, Edge, or Firefox build, or enabling
WebGPU) -- never a blank canvas, a silent stall, or something that could be mistaken
for a hang. See src/render.rs's RenderError and src/app.rs's render_loop for
where that message is produced and shown.
The same "no fallback" reasoning is also why this crate offers no HDR
environment-map picker, even though indicatrix supports one. An HDR environment is the
one scene ingredient the GPU megakernel has no env_mode for
(GpuFrameError::UnsupportedEnvironment); on every other target that's a routine
decline the CPU path picks up silently, but here it would just be a control that can
only ever fail. Material, by contrast, is not a limitation on any target any more:
GemMaterial::gpu_supported() has been unconditionally true since the biaxial WGSL
port, so Alexandrite/Topaz/Tanzanite-class materials would render here exactly as
well as Diamond/Ruby/Sapphire/Emerald -- this crate's combo box just doesn't happen to
list them (see src/scene.rs::material_for_index), to keep the control surface small.
Progressive accumulation
A render is not one shot at a fixed sample count -- it accumulates toward
src/render.rs::TARGET_SPP (256 samples/pixel, matching apps/indicatrix-cut's own
desktop default exactly) in src/render.rs::CHUNK_SPP-sized steps, presenting the
tone-mapped partial image after every chunk (src/app.rs::render_loop) so the stone
visibly refines rather than staying blank or frozen until the last sample lands.
ui/app.slint's progress badge ("128 / 256 spp") says exactly how far along the
current pass is.
This leans on GpuFrameRenderer::accumulate_async's sample-range additivity
contract (see indicatrix::renderer::gpu_backend::GpuBackend's "Sample-range
additivity" doc section, and src/render.rs::Accumulator's own doc comment): each
chunk is dispatched starting at exactly the sample count already folded in, never
0, so successive chunks extend the estimate instead of biasing it. The one place
this matters for correctness, not just visuals: a camera move, a material or
lighting change, or a newly loaded file always resets the accumulator to zero before
the next chunk (src/render.rs::Accumulator::reset, called from
src/app.rs::render_loop) -- continuing to accumulate into a buffer whose earlier
samples were traced against a different scene would silently blend two physically
different images together.
src/render.rs::CHUNK_SPP is currently a reasoned default (8), not a
hardware-measured one -- see that constant's own doc comment for exactly why the
live measurement this was supposed to be based on could not be completed in the
session that built this (the available browser-automation tooling had no
functioning WebGPU adapter to measure against) and for the byte-budget math that
default IS grounded in. accumulate_chunk already logs each real chunk's wall-clock
time to the devtools console ("indicatrix-web: chunk: N spp in Xms (...)") on whatever
machine actually runs this, so retuning it with a real measurement is a one-line
change once someone has a browser with a real adapter.
What this deliberately omits, and why
- No design library. No catalogue browsing, no search, no saved designs. Loading a
design library means
indicatrix-vault'srusqlite/SQLite, andlibsqlite3-sys/SQLite's C source cannot targetwasm32-unknown-unknownat all -- this is the hardest blocker in the brief this crate was built against, and the reason "no library" isn't a UI simplification, it's the thing that makes a browser build possible in the first place. You upload one.ascfile per session; there is nowhere to save it back to. - No remote worker.
indicatrix-net's wire protocol runs over mutual-TLSTcpStreams (rustls); raw TCP sockets don't exist in a browser sandbox (WebSocket/WebTransportare the browser-native analogues, andindicatrix-netspeaks neither). This crate'sCargo.tomldepends onindicatrixandindicatrix-formatsand nothing else from this workspace --cargo tree --target wasm32-unknown-unknown -p indicatrix-webshows neitherindicatrix-netnorindicatrix-vaultanywhere in the graph. - No export presets, no batch rendering, no attachments. All of that lives in
apps/indicatrix-cut'sbridge::export_thread, which this crate does not depend on or reimplement. This is a viewer, not the desktop app's export pipeline. - A small, fixed control surface. Camera orbit (drag the stone), distance,
exposure, four material presets, four lighting presets. No custom-material editor,
no crystal-optics dialog, no tilt-profile curves -- see
ui/app.slint, which is a few hundred lines againstapps/indicatrix-cut/ui/app.slint's much larger tree of components. - A capped, but no longer fixed, render resolution. The render target now tracks
ui/app.slint's render-surface element's actual on-screen size (seesrc/scene.rs::clamp_render_dims), reset through the same dirty-flag/Accumulatormechanism a camera move already used (seeAccumulator::reset's doc comment) and debounced against drag-resize (src/app.rs::RESIZE_DEBOUNCE). It is still bounded, deliberately:src/scene.rs::MAX_RENDER_DIMandMAX_DEVICE_PIXEL_RATIOcap the physical pixel count a maximised, high-DPR window can push through this spectral path tracer, independent of how large the layout itself grows -- see those constants' own doc comments for the reasoning and the exact numbers.
How the GPU path avoids blocking the browser's one thread
pollster::block_on (what indicatrix::renderer::gpu::context::GpuContext::acquire and
the native GPU render loop use to wait on wgpu's async device/adapter/submission
futures) parks the calling OS thread until the future resolves. A browser tab's main
thread must never block like that -- there is nothing else that could ever unpark it,
since the very JavaScript event loop that would deliver wgpu's completion callback is
the thing being blocked.
crates/indicatrix/src/renderer/gpu/frame.rs therefore carries a second, wasm32-only
impl GpuFrameRenderer block (see that module's own doc comment) with genuinely
async fn new_async/accumulate_async that .await wgpu's futures instead of
blocking on them, all the way down to buffer readback: map_read_async awaits
wgpu::Buffer::map_async's own callback (resolved by the browser's microtask queue,
needing no Device::poll call on this backend at all) rather than native's
compute::finish_map_read, which calls Device::poll(Maintain::Wait) to block until
that same callback fires. apps/indicatrix-web's src/render.rs and src/app.rs are what
actually run this async path, via slint::spawn_local (backed by
wasm-bindgen-futures on this target).
This is purely additive on the native side: GpuFrameRenderer::new/accumulate (the
synchronous, pollster-based entry points every desktop/server caller uses) are
completely unchanged, and the new methods live in their own #[cfg(target_arch =
"wasm32")] block that native builds never even compile.
Verifying the build
From the repository root:
Or, from this directory, trunk build runs the full pipeline including
wasm-bindgen, which is a stronger check than cargo check alone (it also validates
the final link step cargo check skips).
Both of those were run to verify this crate; loading the actual served page in a real
browser and confirming pixels appear was not, end to end, in the session that built
this -- the sandboxed browser-automation tool available had navigator.gpu present
but no adapter behind it (request_adapter returned NotFound even from a
UI-independent diagnostic that bypassed ui/app.slint's canvas entirely), and no
connection to a real Chrome instance was available either. trunk build/cargo
check catch compile errors, not runtime/rendering ones -- if you have a browser with
a working WebGPU adapter, actually opening the page once is worth doing before
trusting this beyond "it compiles."