Renderer and browser

Renderer, blueprints, and browser execution

TinyFX rendering is driven by compiler-produced blueprints. The Rust runtime and the generated JavaScript/WebGPU core both consume those descriptions; they do not infer a new layout or binding scheme from the source language at runtime.

Compiler-produced blueprints

Every shader and compute site produces a blueprint containing the information needed to execute it:

  • WGSL source and entry points;
  • capture order and exact uniform layout;
  • resource bindings and their access roles;
  • mesh attributes, fragment outputs, blending, attachments, culling, and depth state where applicable;
  • compute domain, write targets, storage-format variants, and bounded workgroup candidates; and
  • source-oriented identity used by diagnostics, pass explanations, and traces.

Uniform layout follows WGSL’s uniform-address-space rules. The compiler calculates offsets and packing plans once. Runtime code writes bytes according to those plans; it never tries to rediscover structure alignment from a TinyFX type.

For the JavaScript target the compiler additionally translates internal blueprints to physical DTOs with stable capture names, byte offsets, binding numbers, vertex state, WGSL source/sidecar information, and strict schema shapes. Those DTOs contain no compiler-local type IDs.

Binding model

The Rust runtime uses two logical bind groups:

  • Group 0 is the packed uniform block. It includes explicit shader uniforms, numeric captures, narrowed environment fields, and hidden compute range/domain values. Per-frame uniform data is allocated from an aligned arena and selected with a dynamic offset.
  • Group 1 contains textures/samplers and storage resources in compiler-defined blueprint order.

Binding and pipeline caches include the identities that make reuse safe: blueprint/source, physical texture view generation, buffer front/back generation, target/depth formats, mesh/pipeline state, blend state, and filterability. A resource flip, mip reallocation, or uniform-arena replacement invalidates the appropriate cached object rather than risking a stale view.

Rust frame model

tinyfx-runtime::Runtime is surface-agnostic. The CLI or WASM embedding gives it a device, queue, surface format, and a surface texture for each frame.

A normal frame proceeds as follows:

  1. Harvest completed readbacks and profiling callbacks, then begin a new metrics/profile frame.
  2. Resize or reuse the persistent internal render target.
  3. Update native media frames when applicable.
  4. Snapshot input into env, applying native UI input capture before camera/environment updates.
  5. Poll setup once and loop on later frames when present. Asynchronous work can leave a logical continuation pending until a later poll.
  6. Encode authored draw and compute work in source order into one command encoder.
  7. Encode UI/overlay and paint work, blit the persistent target to the surface, enqueue requested snapshot copies, and submit.
  8. Start non-blocking map/readback work for a future frame.

The persistent target is intentional: a setup-only program keeps its rendered image across subsequent frames instead of being cleared by presentation.

If the VM reports an error, the runtime halts program execution but continues presenting the last valid target. This makes failures visible without issuing new unsafe GPU work.

Draws, depth, and feedback

Draw calls normally target the persistent main target. Main-target drawing uses alpha blending by default; offscreen texture targets replace by default unless an explicit blend mode is selected.

Depth conventions carry both clear behavior and default comparison behavior. An explicit compare can override pipeline behavior without changing the convention-owned clear. The runtime rejects incompatible uses such as mixed uncleared conventions or shader combinations whose fragment-depth behavior cannot be safely attached.

Texture feedback and GPU-written resources use generation-aware front/back state. A shader cannot read its own active render target, including through an operation whose apparent result only needs metadata. The runtime detects that conflict before it becomes a WebGPU validation failure.

Textures begin with one mip level. Mip generation allocates a complete chain lazily, preserves level zero, then performs the needed render or compute work. Writes invalidate higher levels. Sampled views span the valid chain; storage and render views address a single level.

Compute execution

parallel for has a VM fallback and a GPU implementation. The compiler provides a canonical workgroup shape plus a bounded set of semantically equivalent candidates. The native runtime:

  1. filters candidates against device limits;
  2. chooses deterministically from domain size, physical formats, and compact kernel characteristics; and
  3. may conservatively refine future choices from ordinary timestamp evidence when available.

It does not replay, reorder, or separately benchmark an authored pass. The generated JavaScript core performs the same safe filtering and deterministic selection without pretending that an adapter fingerprint is a portable contract.

Texture-domain kernels validate all outputs before dispatch and publish their front/back generations together. Fullscreen texture-step shaders can also carry a compute alternative; the fragment feedback path remains available when the compute form is not valid on the active device or for the authored shader.

Readback and profiling

CPU-originated texture/buffer values are immediately known to the VM. GPU readback instead uses the latest completed snapshot. Native gpu.sync() is the explicit blocking escape hatch for work that must see a just-submitted result; browser execution reports that capability as unsupported rather than blocking the event loop.

Runtime profiling separates logical author requests from physical GPU passes. Logical draw/dispatch counts remain useful even when timing is unsupported. Physical pass accounting includes authored work and necessary generated GPU work such as mip levels, reductions, and color/depth texture clears, while excluding presentation blits, transfer copies, overlays, and UI.

Profile scopes attach an immutable active-path snapshot to physical passes as they are encoded. Later timestamp readback updates the snapshot that produced the pass, not a mutable scope stack from a later frame.

Browser platform and generated JavaScript core

The shared browser platform has a single responsibility: turn browser state into normalized frames and call an execution core.

BrowserPlatformSession owns:

  • request-animation-frame scheduling and terminal cleanup;
  • backing-store resize using CSS size and device pixel ratio;
  • visibility/focus and normalized pointer, keyboard, text, and touch input;
  • browser asset/media/paint adapters; and
  • capability checks before a core starts.

The WASM core forwards those snapshots to the Rust runtime. The generated JavaScript core executes compiler-emitted CPU lifecycle code and physical WebGPU blueprints. Both can use the same platform session, DOM UI adapter, asset resolver, media completion queue, and paint compositor.

Browser UI differs from native UI by design. Native widgets are rendered by egui inside the runtime and therefore mask input from cameras while active. Browser widgets are DOM controls above the canvas; their events do not reach the canvas input listeners.

Assets and media

Native asset paths resolve relative to the entry source or compiled artifact. Browser hosts supply an asset resolver, usually backed by preloaded strings or bytes. Image and model loading use that resolver recursively for supported asset payloads, including glTF sidecars.

Media acquisition follows an asynchronous session/task boundary. Browser platform code owns browser tracks and video elements; native runtime code owns capture workers. Both publish coalesced RGBA frames into a stable TinyFX texture at frame boundaries, keeping permission prompts, decoding, and callbacks out of interpreter re-entry and the render hot path.

Validation boundary

Compiler and CLI checks validate emitted WGSL with naga. That is necessary but not sufficient for browser acceptance: Chrome’s Tint/WebGPU validation adds uniformity rules that may reject WGSL accepted by native tooling. Keep real-browser smoke coverage for texture sampling, control flow, and any feature that depends on browser WebGPU behavior.

Advanced compute ordering and indirect counts

Workspace and atomic bindings use their checked storage access role directly; they do not allocate or flip an ordinary buffer back generation. Append lists store a count/overflow/capacity header before their fixed-capacity elements. Source-order command encoding provides visibility between dependent kernels.

Producer-backed counts are converted to private indirect arguments by a tiny fixup kernel. Range kernels additionally bind the count header to guard a partial final workgroup. Draw and dispatch zero counts remain zero; no CPU readback or authored indirect buffer is involved.