Some checks are pending
Lean Action CI / build (push) Waiting to run
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>
158 lines
6.6 KiB
Text
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
|