PAJAMA / BUILD
← Engineering series

01 / 12 · Boot the renderer

Start with your first pixel

Create the project, run a known milestone, and name who owns every part of the frame.

A viewer looks like a single application. Inside it are three jobs: the operating system provides a place to draw, the renderer turns data into pixels, and the interface supplies camera and time. We will build those jobs in that order. You do not need to know Gaussian calculus to clear a window.

1. Set up a reproducible starting point

Install Git, a current stable Rust toolchain and Node.js LTS. On Windows, Rust also needs the MSVC C++ build tools and Windows SDK. Run the commands below in a terminal. Use the repository lockfiles; this series uses wgpu 30 and winit 0.30, so an older tutorial’s method names may not compile here.

The reference repository already contains every milestone. The stage number selects the behavior you are studying; it is not a separate historical commit. To learn by rebuilding, add the source blocks introduced in each chapter to your own branch, then compare the resulting pixels with the reference lab. Full source snapshots are linked below each block.

2. Learn six Rust words as you meet them

A struct groups values, like a camera’s position and direction. An impl gives that group methods. &T borrows a value for reading; &mut T borrows it for changing. Option<T> means the value can be absent, such as a window before startup. Result<T, E> means an operation can fail; ? returns the error to its caller instead of silently continuing.

async/await lets browser GPU initialization finish later without blocking the page. Arc<Window> lets the native app and GPU surface keep shared ownership of the same window. Neither keyword draws anything. They solve lifetime and scheduling problems around the renderer.

3. Draw the ownership map before the pixels

The browser host owns a canvas, fetch and requestAnimationFrame. The native host owns a winit window and OS events. Both create a WorkshopSurface. It owns the device, queue and presentation target and calls WorkshopPass. That pass owns shaders and buffers, with no dependency on a DOM or window.

A buffer is GPU memory containing numbers. A shader is a program the GPU runs on those numbers. A render pipeline connects shader entry points to rules such as blending and output format. A command encoder records work; a queue submits it. Keeping these words separate makes a black screen diagnosable.

4. Your first success is intentionally black

Stage 0 acquires the next surface texture, opens a render pass with LoadOp::Clear(BLACK), stores the result, submits commands and presents it. It issues no geometry draw. In the browser, “Presented frames” must increase while the draw count stays zero. This distinguishes a valid clear operation from a page that never initialized.

RUN → OBSERVE

Make this step concrete

A black native window; the terminal prints the adapter backend. Press 1 to move to the triangle. For the online lab, no local setup is required.

Open GPU milestone 0 →
Terminal · repository root
git clone https://github.com/pajama-studio/4dgaussian.git
cd 4dgaussian
cargo run --locked --example workshop -- 0

Commands use the reference repository. Rust/WGSL changes need a native rebuild or npm run build:wasm, followed by a browser reload.

IDEA → IMPLEMENTATION

Follow the running code

Excerpts are extracted from the repository at site build time. Expand only the part you are studying; full files include context and line numbers.

src/workshop_surface.rs · L84–128

Read this as acquire → prepare → record → submit → present. Stage 0 skips the draw inside the pass.

Actual repository code
    pub fn render(&mut self, input: Frame) -> Result<Option<u32>, String> {
        let frame = match self.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(frame)
            | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
                self.surface.configure(&self.device, &self.config);
                return Ok(None);
            }
            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
                return Ok(None)
            }
            wgpu::CurrentSurfaceTexture::Validation => {
                return Err(
                    "Surface validation failed; reload to recreate the GPU resources".into(),
                )
            }
        };
        let count =
            self.pass
                .prepare(&self.queue, input, [self.config.width, self.config.height])?;
        let target = frame.texture.create_view(&Default::default());
        let mut encoder = self.device.create_command_encoder(&Default::default());
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("workshop-frame"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &target,
                    resolve_target: None,
                    depth_slice: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            self.pass.draw(&mut pass);
        }
        self.queue.submit([encoder.finish()]);
        self.queue.present(frame);
        Ok(Some(count))
    }
Open full source with line numbers ↗

Real-model render pass ↗ · Real-model shader ↗

Predict it, change it, explain it

Change BLACK to a dark red clear color. Predict which other stages will also get that background before running them.

Check your reasoning

Every stage uses this same render pass, so every background changes. A shader edit is unnecessary. If the triangle later appears but the clear color does not, you may be running an old binary or a different host.

Keep the theory beside the code

Open the related derivation or lab ↗

The Rust Programming Language ↗

wgpu 30 · API and backend model ↗