HarperZ9/raw-nativeExplainer, built from commit f2cd6e9All repository explainers

raw-native

Render on the CPU, then check the fast lighting shortcut against ray tracing.

What it does for you

Real-time engines darken creases and corners with a cheap screen-space trick called ambient occlusion. raw-native renders a scene with that trick and with a slow ray-traced reference, measures how far apart the two are, and tells you whether the shortcut can be trusted for that view. You get the picture and the evidence in the same run.

Source: README.md at f2cd6e9 (release 0.5.1)

Watch

No concept film fits this tool closely yet. The walkthrough below covers it in text, with real commands and output.

Video walkthrough: coming with the next release.

How it works, one step at a time

Scroll, or use the step buttons. The renders, certificate values and timings come from the repository at f2cd6e9. The two cross-section drawings are a 2D slice that runs the same rules as the C++ code; their counts belong to the drawing.

  1. 01

    One run, four pictures, one verdict

    Run raw_native_cli --out ./out --width 512 --height 512 and you get the shaded frame, two occlusion maps and the difference between them. The ray-traced map shows dark contact shadows where the box meets the ground. The screen-space map misses most of them.

    For this default view the error is 0.1349 RMSE against a tolerance of 0.12, so the certificate says refuted. The shortcut is too weak for this camera, and the run says so.

    Source: README.md, the default render and its certificate; docs/examples/default-certificate.json

  2. 02

    Rasterize into a G-buffer

    The scene is built in: a 10 by 10 ground plane and a box two units tall at the origin, under one directional light. The rasterizer finds the nearest surface for each pixel and stores five channels: depth, normal, world position, motion and a coverage mask.

    Both occlusion estimators read this buffer. The mask matters later: only covered pixels are compared.

    Source: raw/renderer/gbuffer.hpp, src/scene, src/renderer/render.cpp

  3. 03

    The reference: shoot real rays

    For each covered pixel the reference sends 64 rays over the hemisphere above the surface, weighted toward the normal. A ray that hits geometry within 2.0 world units is blocked. The pixel's occlusion value is the share of rays that got out.

    The ray directions come from a hash of the pixel and the sample index, with no global random state. Run it twice and you get the same grain.

    Source: src/renderer/ray_ao.cpp, constants in raw/renderer/render.hpp

  4. 04

    The shortcut: look only at the screen

    The screen-space method takes 24 samples within 6 pixels of the pixel and reads their stored positions. A neighbour within 2.0 units that rises into the hemisphere (its direction scores above 0.15 against the normal) counts as blocking.

    It can only see what the camera sees. A face turned away from the camera is not in the buffer, and anything more than 6 pixels away is never sampled. That is why the drawing's two numbers differ.

    Source: src/renderer/ssao.cpp

  5. 05

    Reconcile: one number, one bound

    The reconcile takes the absolute difference at every covered pixel, keeps the largest, and computes the root mean square. If the RMSE is at or below the tolerance the claim is verified; above it, refuted.

    Default view: 151,984 covered pixels, RMSE 0.1349, tolerance 0.12. Refuted. The worst single pixel is off by 0.6406.

    Source: src/cert/reconcile.cpp

  6. 06

    The verdict depends on the view

    Same scene, same tolerance, five cameras. Two views stay inside the bound: high, looking down from above, and wide. The default, low and close views fall outside it.

    A pass on RMSE says nothing about the worst pixel: the high view passes at 0.0827 with a maximum error of 0.625.

    Source: README.md, "The verdict depends on the view"; evidence/wasm-vs-native-512.json

  7. 07

    Write it down with its evidence

    The certificate records the claim, the verdict, every parameter that changes the pixels, the sample counts, the exact float values and the SHA-256 of every file the verdict was judged from. Since 0.5.0 a receipt.json restates the same check in the shared superstack receipt format.

    Source: README.md, "What one run writes" and "The superstack receipt"; docs/examples/default-certificate.json

  8. 08

    Check it without trusting the renderer

    raw_native_cli verify ./out renders nothing. It re-hashes every listed file, recomputes the reconcile from the two float maps and the mask, and compares the pixel count, RMSE, maximum error and verdict with the recorded ones.

    Exit 0 means every check matches, 3 means a mismatch, 2 means a file is missing. scripts/recheck.py does the same in Python and shares no code with the C++ verifier, so a bug in one shows up as a disagreement. Try each case in the panel.

    Source: README.md, "Check a certificate without trusting the renderer"; src/tools/verify.cpp

  9. 09

    Render twice to prove the memory bound

    The first pass renders inside a generous slab and records exactly how many bytes the frame needed. The second pass renders again inside a budget of exactly that many bytes, with an allocator that never grows. If anything asks for more the second time, the render stops and the memory certificate says refuted.

    Source: README.md, "How the memory budget works"; app/main.cpp

  10. 10

    The GPU is checked against the CPU

    Two optional builds render the same frame on the GPU, through D3D12 on Windows or WebGPU in a browser, and compare every GPU frame with a CPU render of the same camera. The bounds were committed before the first line of GPU code.

    All 16 renders pass on both backends. 13 of 16 are byte-identical to the CPU frame. The other 3 differ by one ray on a few pixels and still pass.

    Source: README.md, "Render on the GPU, checked against the CPU"; raw/cert/gpu_tolerance.hpp; evidence/d3d12-rtx4090-checks.json, evidence/webgpu-rtx4090-chromium.json

Walkthrough

Install it, run it once, then use the main feature. Each command below is real, and so is its output.

  1. Get it

    Download a binary from the latest release (Windows x64, Linux x64 or WebAssembly) and check it against SHA256SUMS.

  2. First run: render and reconcile

    Render the default scene. Both occlusion estimators run, and the reconcile compares them against the tolerance.

    $ raw_native_cli --out ./out
    reconcile: pixels=37996 rmse=0.1294 maxError=0.6094 verdict=DIVERGENT
  3. Verify the output

    Re-check every file the render wrote.

    $ raw_native_cli verify ./out
    verify: all checks match
  4. Try another view

    Change the size and camera.

    $ raw_native_cli --out ./high --width 512 --height 512 --eye 0,9,3 --target 0,0.5,0
  5. Or build from source

    CMake 3.24 or newer and a C++23 compiler.

    $ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
    $ cmake --build build --config Release
    $ ctest --test-dir build -C Release --output-on-failure

Without size flags the CLI renders 256 x 256, so its numbers differ from the 512 x 512 values above. Output lines are excerpts from raw_native_cli built from f2cd6e9 with MSVC on Windows.

Run it in this page

This loads raw-native 0.4.0's WebAssembly build from harperz9.github.io, checks both files against their SHA-256 before running them, and renders on your CPU. The CPU render code is unchanged from 0.3.0 through 0.5.1. Smaller frames give different numbers from the 512 x 512 table above. At 256 x 256 the default view matches the native CLI run shown above: rmse 0.1294 over 37,996 pixels.

Not run yet. A 256 x 256 render took 622 ms in a desktop browser when this page was tested.

What it does not do

Source: README.md, "Limits, stated plainly" and "Measured results", at f2cd6e9

Check what stuck

Answer each one in your head before you open it.

Which of the two occlusion maps is the reference, and why?

The ray-traced one. It tests real rays against the geometry within 2.0 units, so it sees occluders the camera cannot. The screen-space map only reads what is in the G-buffer.

The default view reads refuted. What two numbers decide that?

The RMSE over covered pixels, 0.1349, and the tolerance, 0.12. The RMSE is above the bound.

Why can the high view be verified while one of its pixels is off by 0.625?

The verdict is on RMSE across 239,854 pixels. A few large errors barely move a root mean square, so a pass says nothing about the worst pixel.

What does verify do that a second render would not?

It renders nothing. It re-hashes the recorded files and recomputes the reconcile from them, so it checks the evidence without trusting the renderer that made it.