PAJAMA / BUILD
← Engineering series

07 / 12 · Connect a learned model

Load a real model without guessing its meaning

Decode a specific model contract, upload persistent records, and render the pretrained fixture.

PLY tells you how fields are stored. It does not tell you whether “scale” means a physical length or its logarithm, or whether “color” means RGB or SH coefficients. A loader that guesses may produce a plausible but wrong picture.

1. Write the contract before the decoder

This fixture is STG-Lite: 32 little-endian float32 fields per record, 128 bytes. It stores position, temporal center/log-scale, unused normal slots, nine motion coefficients, three direct RGB features, opacity logit, three log-scales, quaternion wxyz and quaternion velocity. The field order is strict. This is not a universal 4DGS layout.

In this model the f_dc fields hold direct RGB features used by the lightweight renderer. Do not apply the static 3DGS SH-DC conversion just because the property names look familiar. The SH laboratory teaches a different appearance model; it does not add missing SH coefficients to this file.

2. Decode, validate, then activate

The strict parser checks header types/order, count limits, payload size, finite values and activation overflow. Only then upload the record array. Runtime scales use exp(log_scale); opacity uses sigmoid(logit). Passing log-scale directly into a covariance would square the wrong number.

s=eslog⁡,o=11+e−β,ρ=eρlog⁡s=e^{s_{\log}},\qquad o=\frac1{1+e^{-\beta}},\qquad \rho=e^{\rho_{\log}}

3. Keep the source on the GPU

StgPass::new uploads the source payload once. Each frame, prepare evaluates centers and opacity on the CPU, culls and sorts them, then uploads the ordered u32 indices. The shader uses each source index to read the original record and evaluates time-dependent geometry again. CPU and GPU evaluation must stay consistent or sorting will describe a different scene.

At 108,317 records, source storage is 13,864,576 bytes, about 13.22 MiB. One full u32 index order is 433,268 bytes, about 0.41 MiB. Compressed download size is a separate number; neither value measures the complete process or GPU memory footprint.

4. Compare under a known camera

Stage 7 gives an orbitable overview. Use the main viewer’s calibrated reference comparison when judging agreement with the research image. A different pose, field of view, crop or timestamp can dominate apparent error. The included asset is an official pretrained research fixture, not evidence that this project trained that scene.

The demo is for non-commercial research/evaluation; check the upstream model and dataset terms before reusing assets. A model loader should carry provenance and representation metadata alongside the byte layout.

RUN → OBSERVE

Make this step concrete

The Sear steak research scene, rendered by the existing StgPass inside the new shared host. Model download requires an explicit button online.

Open GPU milestone 7 →
Terminal · repository root
cargo run --locked --example workshop -- 7
# Online: choose stage 7, then Load research model.
cargo run --locked --example native_smoke

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.rs · L254–306

Stage 7 delegates to the real pass without changing the host.

Actual repository code
    pub fn prepare(
        &mut self,
        queue: &wgpu::Queue,
        frame: Frame,
        size: [u32; 2],
    ) -> Result<u32, String> {
        frame.validate()?;
        if size.contains(&0) {
            return Err("Cannot prepare a zero-sized target".into());
        }
        let (view, projection) = camera(frame, size[0], size[1]);
        self.stage = frame.stage;
        if frame.stage == 7 {
            let pass = self.real.as_mut().ok_or("Load the STG-Lite model first")?;
            self.count = pass
                .prepare(
                    queue,
                    frame.time,
                    view.to_cols_array_2d(),
                    projection.to_cols_array_2d(),
                    size,
                )?
                .visible_count as u32;
        } else {
            let data = instances(frame, view);
            self.count = data.len() as u32;
            queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&data));
            queue.write_buffer(
                &self.uniforms,
                0,
                bytemuck::bytes_of(&Uniforms {
                    view_projection: (projection * view).to_cols_array_2d(),
                    viewport_stage: [size[0] as f32, size[1] as f32, frame.stage as f32, 0.0],
                }),
            );
        }
        Ok(if frame.stage == 0 { 0 } else { self.count })
    }

    pub fn draw(&self, pass: &mut wgpu::RenderPass<'_>) {
        if self.stage == 0 {
            return;
        }
        if self.stage == 7 {
            if let Some(real) = &self.real {
                real.draw(pass);
            }
            return;
        }
        pass.set_pipeline(&self.pipeline);
        pass.set_bind_group(0, &self.bindings, &[]);
        pass.draw(0..if self.stage == 1 { 3 } else { 6 }, 0..self.count);
    }
Open full source with line numbers ↗

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

Predict it, change it, explain it

Why would a generic XYZRGB PLY loader be insufficient even if it can read every byte in this file?

Check your reasoning

It would miss activation, time, shape, quaternion order and the renderer’s appearance convention. Storage decoding and representation decoding are separate responsibilities.

Keep the theory beside the code

Open the related derivation or lab ↗

Spacetime Gaussian Feature Splatting · representation ↗