cubical-transport-hott-lean4/Topolei/RenderProbe.lean
Maximus Gorog c2e3ecb3e3
Some checks are pending
Lean Action CI / build (push) Waiting to run
Initial commit: topolei — cubical-transport HoTT in Lean 4 + Rust FFI
Implements the cells-spec vision: a computation space that preserves
auditability, correctness, interactivity. Phase 1 (Lean kernel +
naga-IR Rust backend) is closed; foundation hypothesis stack
(Selection H1+H2, Subobject H3, Trace H5, Obs.Ctx C2, Cubical.Trace)
landed.

Highlights:
- Cubical-HoTT syntax + value/eval/readback in Lean
- naga-IR pipeline (no GLSL string crosses FFI; 17/17 probes pass)
- Honesty audit: every non-transport (sealed cells, vertex shader,
  Y-flip, presentation conventions) is documented as such
- Polymorphic Trace α as free monoid; Cubical.Trace gives
  CTerm → Trace CTerm by structural fold (homomorphism = definition)
- Selection as Huet zipper; Subobject as Boolean algebra over WCell
- All theorems proven; the proof IS the implementation

See STATUS.md for the resume guide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:40:45 -06:00

158 lines
6.6 KiB
Text

/-
Topolei.Render.Probe
====================
Empirical check for `render_faithful` — the axiom in `Topolei.GPU.Spec`
that claims the GPU's runtime output equals the compiled shader's
`ShaderSemantic` at every pixel and uniform setting.
The Rust FFI `topolei_canvas_render_probe_path_pixel` accepts a Lean
`EMLPath` inductive structurally, builds the fragment shader as a
`naga::Module` directly via `emit_naga::build_probe_module` (no GLSL
text intermediary), submits the module to wgpu's SPIR-V backend, and
reads back a single pixel from an `Rgba32Float` offscreen target.
This module binds that FFI and exercises it on the demo paths
`plotExp` / `plotLn` / `plotTransp`.
## Tolerance
IEEE 754 rounding differs slightly between the CPU (Lean-side
`ShaderSemantic` evaluation via `Float.exp`, `Float.log`, `Float.cos`)
and the GPU (wgpu → Vulkan → driver → shader unit). The comparison
therefore uses an absolute tolerance rather than bit-exact equality.
`5e-3` is generous; a bit-exact probe on known IEEE 754 drivers
(e.g. SwiftShader) might tighten it to 1e-6 or better.
## Running
The probe requires a Vulkan/Metal/DX12 adapter (or a software
rasterizer like SwiftShader) on the host. When no adapter is
available the FFI returns a sentinel `RGBA { r := -1, g := -1, ... }`
and the test skips that comparison rather than asserting failure.
Run with: `lake build probe-test && ./.lake/build/bin/probe-test`.
-/
import Topolei.EML
import Topolei.EML.Path
import Topolei.GPU.Spec
namespace TopoleiProbe
/-- Rendered-pixel RGBA as four Lean `Float` values. Layout matches
the `topolei_canvas_shim_io_ok_rgba` helper in `canvas-rs/shim.c`:
tag 0, 4 boxed-Float fields. -/
structure RGBA where
r : Float
g : Float
b : Float
a : Float
deriving Repr, Inhabited
/-- **Structured probe.** `renderProbePath path width height time
pathParam x y` — passes the `EMLPath` inductive *structurally* to
the Rust side, which walks the object tree
(`eml::emlpath_from_lean`), builds a `naga::Module` directly via
`emit_naga::build_probe_module`, and runs the offscreen probe.
No shader-source string crosses the FFI boundary. See
`NAGA_IR_PLAN.md` for the construction; `compileEMLPath_correct`
in `Topolei.GPU.Spec` is the contract the IR builder must satisfy. -/
@[extern "topolei_canvas_render_probe_path_pixel"]
opaque renderProbePath
(path : @& EMLPath)
(width height : UInt32)
(time pathParam : Float)
(x y : UInt32) : IO RGBA
/-- True when `renderProbePath` returned its GPU-unavailable sentinel.
A channel of `-1.0` is outside the valid RGBA range `[0, 1]`, so
tests can use this as a reliable skip signal. -/
def RGBA.isSentinel (c : RGBA) : Bool :=
c.r < 0.0 && c.g < 0.0 && c.b < 0.0 && c.a < 0.0
/-- Bridge to `GPU/Spec.lean`: the CPU-side `PixelColor` that the GPU
is expected to match. One-to-one translation. -/
def RGBA.ofPixelColor (c : PixelColor) : RGBA :=
{ r := c.r, g := c.g, b := c.b, a := c.a }
/-- Absolute-tolerance comparison for `Float`. Both arguments must
differ by no more than `tol`. -/
def floatClose (a b tol : Float) : Bool :=
let d := a - b
let ad := if d < 0.0 then -d else d
ad <= tol
/-- Compare two RGBA values channel-wise under `tol`. -/
def rgbaClose (expected actual : RGBA) (tol : Float) : Bool :=
floatClose expected.r actual.r tol &&
floatClose expected.g actual.g tol &&
floatClose expected.b actual.b tol &&
floatClose expected.a actual.a tol
/-- Convert framebuffer pixel `(x, y)` at resolution `(w, h)` into the
`uv ∈ [0, 1]²` coordinates the probe shader sees at that pixel's
fragment center. Framebuffer y is top-down (wgpu origin) while
the vertex shader's uv tracks NDC, so we flip: `uv.y = 1 -
(y+0.5)/h`. `uv.x = (x+0.5)/w`. The `+0.5` lands us on the pixel
center where the rasterizer samples. -/
def pixelUV (x y width height : UInt32) : PixelCoord :=
let w := width.toNat.toFloat
let h := height.toNat.toFloat
let px := (x.toNat.toFloat + 0.5) / w
let py := 1.0 - (y.toNat.toFloat + 0.5) / h
{ x := px, y := py }
/-- Run a single probe: pass the `EMLPath` directly to the Rust
offscreen probe; compare the GPU-returned pixel `(x, y)` against
Lean's `EMLPath.toColor path (pixelUV ...) u`. -/
def runProbe
(path : EMLPath) (label : String)
(width height : UInt32)
(time pathParam : Float)
(x y : UInt32)
(tol : Float := 5e-3) : IO Bool := do
let actual ← renderProbePath path width height time pathParam x y
if actual.isSentinel then
IO.println s!" ⏭ SKIP [{label}] at ({x}, {y}) — no GPU adapter"
return true
let p : PixelCoord := pixelUV x y width height
let u : FrameUniforms :=
{ time, resWidth := width.toNat.toFloat, resHeight := height.toNat.toFloat,
pathParam }
let expectedColor := EMLPath.toColor path p u
let expected := RGBA.ofPixelColor expectedColor
if rgbaClose expected actual tol then
IO.println s!" ✅ [{label}] pixel ({x}, {y}): GPU matches Lean (r={expected.r} g={expected.g} b={expected.b})"
return true
else
IO.println s!" ❌ [{label}] pixel ({x}, {y}) divergent"
IO.println s!" expected: r={expected.r} g={expected.g} b={expected.b} a={expected.a}"
IO.println s!" actual: r={actual.r} g={actual.g} b={actual.b} a={actual.a}"
return false
/-- Full probe battery. Returns number of failures.
Each entry asserts `EMLPath.toColor path p u ≈ GPU_pixel(p, u)`
within tolerance — i.e. the inhabited form of `render_faithful`
on the naga-IR pipeline. -/
def runProbes : IO UInt32 := do
IO.println "── Topolei render_faithful probes ──"
IO.println " Naga-IR pixel probes (EMLPath → naga::Module → SPIR-V → GPU):"
-- Constant path (dim variable absent from body): `pathParam` is
-- irrelevant and the two endpoints coincide.
let n1 ← runProbe plotExp.toEMLPath "plotExp" 128 128 0.0 0.0 64 64
let n2 ← runProbe plotExp.toEMLPath "plotExp" 128 128 0.0 1.0 64 64
let n3 ← runProbe plotLn.toEMLPath "plotLn" 128 128 0.0 0.5 32 64
-- Parametric path: `pathParam` changes the color — sample across
-- values to cover the sweep.
let n4 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 0.0 64 64
let n5 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 0.5 64 64
let n6 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 1.0 64 64
let n7 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 0.5 32 96
let mut fails : UInt32 := 0
for ok in [n1, n2, n3, n4, n5, n6, n7] do
if !ok then fails := fails + 1
IO.println s!"── {7 - fails.toNat} / 7 probes passed ──"
return fails
end TopoleiProbe