WASM GPU Blue Sky
This is a proposal for building a virtual GPU that runs in the browser via WebAssembly, plus a compatibility shim so existing WebGL apps can target it.
1) Goals
- Keep the project educational but still practical
- Run fully in-browser (no native install)
- Support a subset of real graphics APIs early
- Grow from a functional model to a cycle-aware model over time
2) Architecture choices
There are two good starting paths.
Path A: software-model-first (recommended)
Write the GPU simulator as software components (Rust or C++) and compile to WASM.
Pros:
- Fastest path to a working browser demo
- Easier debugging and profiling
- Easier to evolve ISA and memory model
Cons:
- Not a true RTL source of truth
- Harder to map directly onto FPGA/ASIC later
Path B: RTL-first (Verilog/Chisel -> simulator)
Use Verilog or Chisel for RTL, then generate a browser-usable simulator.
Common flow:
- Chisel/Verilog -> Verilator C++ model
- Compile the C++ model to WASM (Emscripten)
- Wrap with JS APIs for command submission and framebuffer reads
Pros:
- Strong hardware lineage
- Closer to cycle-accurate behavior
Cons:
- Larger and slower WASM output
- More complex toolchain
- Harder to iterate rapidly on frontend API behavior
3) A hybrid strategy that usually wins
Use a two-layer model:
- Architectural model in Rust/C++ for fast execution in WASM
- Optional RTL model for selected blocks (scheduler, interpolation, texture units) for validation runs
This gives:
- Fast interactive mode in-browser
- Slower reference mode for correctness checks
4) Core simulator design
Define a stable command processor boundary.
- Command FIFO: draw calls, state changes, resource binds
- Resource model: buffers, textures, samplers, render targets
- Shader model: simple IR (SSA-like or stack VM)
- Pipeline blocks:
- vertex processing
- primitive assembly and clipping
- setup/raster
- varying interpolation
- fragment execution
- depth/stencil/blend
For browser responsiveness:
- Run simulation in a Web Worker
- Chunk work by tiles or draw batches
- Yield frequently so the UI thread stays responsive
4.1) Validate the toolchain first
Yes, using one or more RISC-V 32-bit cores as an early validation target is a good idea.
It is a clean way to prove the toolchain before committing to the full GPU:
- compiler backend works
- linker and startup code work
- memory map and MMIO wiring work
- simulator runtime works in native form and in WASM
- test harnesses can drive deterministic execution
If you already have a simple Hazard-style core plus your own core, that is even better. Two implementations let you separate concerns:
- one core can be the known-good baseline
- the other can exercise your custom ISA, microarchitecture, or peripherals
What to test
Keep the first programs boring and deterministic:
- integer ALU smoke tests
- branches, calls, and stack frames
- memory load/store alignment
- MMIO reads/writes
- interrupts or traps, if you support them
- a tiny freestanding C runtime if that is part of the target
Why multiple cores help
Multiple cores are useful if you want to validate more than the compiler:
- lock-step or differential testing between implementations
- multicore scheduling and contention
- shared memory ordering and atomics
- cross-core message passing or interrupts
Suggested sequencing
- Get one core running natively.
- Compile that same core to WASM.
- Add a second 32-bit core and run the same test corpus against both.
- Only then start plugging in GPU-specific blocks.
The point is not to build a complete RISC-V product. The point is to establish a trustworthy execution and build pipeline that will carry over to the GPU simulator.
5) Compiling to WASM
If you build in Rust
- Compile with
wasm32-unknown-unknown - Use
wasm-bindgenfor JS bindings - Keep hot loops in plain Rust, avoid excessive host calls
If you build in C/C++
- Compile with Emscripten
- Export a small C ABI for command submission and memory mapping
- Prefer bulk command buffers over per-call JS/WASM crossings
If you start from Verilator
- Generate C++ from RTL
- Build with Emscripten as above
- Add deterministic tick APIs:
gpu_tick(n_cycles)gpu_submit(cmd_ptr, cmd_len)gpu_read_color(ptr, len)
6) Performance model in browser
Do not chase full hardware realism first. Define quality levels.
- Level 0: functional only (frame-correct)
- Level 1: coarse timing model per stage
- Level 2: wave occupancy and cache penalties
- Level 3: optional cycle model for chosen units
Use tile-based raster by default for cache locality in WASM linear memory.
7) How to shim WebGL apps to your virtual GPU
Use an interposition layer that mimics WebGLRenderingContext and WebGL2RenderingContext.
Shim shape
- Replace
canvas.getContext("webgl")/("webgl2") - Return a proxy object implementing WebGL methods
- Record and translate WebGL state into your command stream
- Execute in simulator worker
- Present output via:
ImageDataon 2D canvas, or- texture upload to a real WebGL canvas for display only
What to support first
- Core draw path:
createBuffer,bufferData,vertexAttribPointer,useProgram,drawArrays,drawElements - Basic textures:
texImage2D, nearest/linear, clamp/repeat - Core blend/depth states
- GLSL subset with a translator to your shader IR
Shader handling options
- Fast path: parse GLSL ES subset and lower to your IR
- Compatibility path: route unsupported shaders to a fallback
- Debug path: dump lowered IR and intermediate varyings per draw
State virtualization
Maintain a WebGL-like state machine inside the shim:
- Bound buffers/VAOs/programs/textures
- Uniform values and blocks
- Framebuffer attachments
Each draw call snapshots needed state and emits a compact internal packet.
8) Practical compatibility plan
Phase 1:
- Run tiny demos you control
- No extensions
- Deterministic render tests
Phase 2:
- Support common engine subset (Babylon/Three basic materials)
- Add texture formats and precision rules
- Add
OES_element_index_uint,OES_standard_derivativesequivalents
Phase 3:
- Add extension emulation policy table
- Add fallback paths for unsupported features
- Publish conformance-style test matrix
9) Suggested stack
- Simulator core: Rust
- Worker boundary: postMessage + shared ring buffer where possible
- Display: HTML canvas 2D initially, optional GPU blit path later
- Shader frontend: GLSL ES parser to custom IR
- Validation: offline traces and image-diff tests
If RTL is a must-have:
- Keep a Verilator build as a reference backend, not the default runtime backend
10) Risks and mitigations
- WASM/JS boundary overhead:
- Mitigation: batch commands and shared memory buffers
- Shader compatibility complexity:
- Mitigation: strict supported-subset docs and feature probes
- Browser memory limits:
- Mitigation: tile rendering, transient buffers, texture residency limits
- Latency from single-threaded fallback:
- Mitigation: worker execution and incremental frame submission
11) Minimal proof-of-concept target
A good first milestone is:
- One spinning textured cube
- Depth test + backface culling
- Perspective-correct varyings
- 60 FPS at 640x360 in at least one desktop browser
If you can hit that with command tracing and deterministic replay, you have a real platform to iterate from.
12) Bottom line
Yes, this is achievable. Use software-model-first for speed, keep RTL/Chisel/Verilog as a validation path, and expose a WebGL-compatible shim that translates API calls into your own command stream. That gives immediate browser usability while preserving a path toward deeper hardware realism.