# The Lantern Room — CPU companion experiments This independent C++23 project connects replayable sampling, a sphere BVH, and an inspectable bounded diffuse path loop to a small actual image. The reference supports two-sided Lambertian spheres, two-sided emission, a constant environment, and a jittered pinhole camera. Scalar integral/statistics/MIS-weight experiments remain available separately. Richer materials, light sampling/MIS, progressive updates, and your final room composition remain original learner extensions. There are no external dependencies or GPU APIs. A suitable C++23 compiler and standard library (including `std::expected`) and CMake 3.25+ are required. CPU execution is intended for macOS and AMD/Linux systems; only the documented local macOS arm64 host was tested. No GPU support or cross-platform performance is inferred from that verification. ## Run From the project root: ```sh cmake -S native/advanced/pathtracing -B build-lantern -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug cmake --build build-lantern --parallel ctest --test-dir build-lantern --output-on-failure ./build-lantern/lantern-experiment ``` Sanitizer configuration with a supported Clang/GCC toolchain: ```sh cmake -S native/advanced/pathtracing -B build-lantern-sanitize \ -DCMAKE_BUILD_TYPE=Debug -DLANTERN_SANITIZERS=ON cmake --build build-lantern-sanitize --parallel ctest --test-dir build-lantern-sanitize --output-on-failure ``` The executable prints an estimate of the hemisphere cosine integral (analytic answer pi), the node count of a tiny index, and an independently known central sphere hit. This scalar experiment produces no image or performance score; lantern-render below writes an image. Exit 0 means the experiment completed; exit 1 reports a failed operation. ## Ordered renderer checkpoints Start with the original files. Each step has one concrete result before the next extension; the course's “The diffuse walk” chapter explains each transition and provides three revealable hints per exercise. 1. **Build and calculate.** Inspect `CMakeLists.txt` and `src/render.cpp`, run the commands above, then `./build-lantern/lantern-render --constant`. Expected and observed linear RGB are **1.2 0.3 0.075**: reflectance `(0.6,0.3,0.15)` times environment `(2,1,0.5)`. No display clamp belongs in this calculation. The isolated sphere and cosine-weighted diffuse model make this a zero-variance special case. Tests repeat it across 64 seeds. 2. **Build the frame.** Read `frame_from_normal` and `to_world` in `src/diffuse.cpp`. Local +Z becomes the facing unit normal through an orthonormal basis. Read the four basis cases in `tests/diffuse_tests.cpp`, then run `./build-lantern/lantern-diffuse-tests` (99 checks). Add normal `(0.6,0.8,0)` as your first independent case. If a pole fails, inspect the helper-axis branch before changing the PDF. 3. **Follow one path.** Run `./build-lantern/lantern-render --trace`. The fixed ray hits object 1, with cosine about `0.718880`, PDF `0.228827`, and updated beta `(0.8,0.2,0.1)`, then sees the environment: final L is `(0.28,0.09,0.065)`. Scatter records show updated beta; terminal records retain incoming beta and have no sampled direction/PDF. A miss adds environment, a hit adds emission, and the scatter cap is tested after that contribution. 4. **Make and change one image.** Run: ```sh ./build-lantern/lantern-render lantern.ppm 4 ./build-lantern/lantern-render lantern-zero.ppm 0 ./build-lantern/lantern-render lantern.ppm 9 ``` The first two write 96×64 P6 PPM files at 32 samples/pixel, seed 47. The last deliberately fails before writing. Four scatters reveal red/blue spheres, a curved floor, environment and emitter; zero scatters leave nonemissive surfaces black while camera-visible emission/environment remain. Files are replaced deliberately. Use a viewer with PPM support or an existing conversion tool. Change one reflectance in `study_scene`, rebuild, and keep the original analytic tests unchanged. `PathTrace` owns a fixed nine-record array; `max_bounces` permits 0–8 scattered rays, followed by a terminal visit. The loop evaluates `beta *= f*cosine/pdf` with the matched cosine sampler, records the terms, and keeps RGB radiance separate. The scene owns copies of material and geometry input. Its tighter transport bounds are centers ±32, radii 0.1–32, reflectance [0,1], and emission/environment [0,16]. The fixed `1e-4` normal offset is a stated small-scene approximation, not a floating-point error bound or a solution for arbitrary scales/touching geometry. The hard scatter cap truncates transport and is biased. A render validates dimensions 1–512, samples 1–128, and at most 2,097,152 camera samples before image allocation. Each pixel owns a deterministic stream derived from seed+pixel index; this does not prove statistical independence. Changing the bounce limit may consume a different number of random values and change later jitter within that pixel. Replay applies to unchanged settings on this implementation, not to bit-identical floating-point output on every compiler. Image storage contains the **linear mean**. The PPM writer validates storage and finite nonnegative channels, then clamps and applies a gamma-2 approximation; this is neither exact sRGB nor calibrated color management. Failed writes return an error and may leave an incomplete destination. The executable catches ordinary exceptions; an allocation-owning error string means `expected` is not a no-throw promise. No worker pool, direct-light sampler, specular model or RR is hidden in this bridge. ## Contracts - `Rng` owns explicit SplitMix64 state. `uniform()` uses the high 53 bits for a half-open [0,1) double. The algorithm is credited in `NOTICE`; it is not cryptographic, and distinct seeds are not a proof of independent streams. - `cosine_hemisphere(u,v)` validates finite half-open inputs and returns a local positive-Z unit direction with a PDF per steradian. It does not build a world-space frame or implement a material system. - `cosine_integral(count,seed)` estimates the integral of cosine using a uniform hemisphere density. Counts must be 1–1,048,576. - `Moments` accepts at most 1,048,576 finite scalar values with absolute value at most 1e12. It keeps a Welford mean and sample variance; this is not a universal convergence or error-bound API. Invalid input preserves prior state. - `power_weight(a,b)` takes finite nonnegative directional PDFs no larger than 1e6, with a nonzero total. It assumes one sample from each of two strategies using the same measure. Scaling before squaring avoids an all-zero denominator for tiny PDFs. This scalar helper is not an MIS integrator. - `SceneIndex::build(span)` accepts at most 128 finite spheres, unique ids, centers within ±1,000, and radii 0.01–100. It validates before copying and owns both reordered spheres and index nodes. Equal-count splits terminate even when centroids coincide. A binary node reservation and fixed traversal stack are bounded by the accepted primitive count. - `closest(ray)` and `linear_closest(ray)` require a finite origin within ±1,000 and a direction with components within ±1 and squared length within 1e-10 of one. Hits use positive t > 1e-5 and equal-distance ties choose the smaller stable id. Geometry is queried without borrowed caller storage. - AABB slab tests handle parallel signed-zero components without division and conservatively accept tangency. The primitive test uses a closest-point projection with `std::fma` to avoid large quadratic-term cancellation near a far-origin tangent. The tests cover adjacent representable grazing rays. The BVH is a bounded median-split sphere reference, not SAH, a wide BVH, moving geometry, transformed instances, or a complete floating-point robustness proof. Keep the linear oracle and independently calculated cases when extending it. There is no measured speedup claim. Beyond the bounded diffuse/emission bridge, delta-event accounting, area-light sampling, NEE/MIS integration, Russian roulette, progressive accumulation, color management and parallel scheduling remain guided learner extensions. ## Verification evidence Fresh independent review, 24 October 2026: Apple Clang 21.0.0 and CMake 4.4.4 on local macOS arm64: Debug, Release, and Debug with AddressSanitizer + UndefinedBehaviorSanitizer each build and pass 5/5 CTest targets, with warnings treated as errors. The original contract executable reports 37 checks and the new diffuse bridge reports 99 checks. The sanitizer run emits no diagnostics. No Linux, ThreadSanitizer, GPU, or performance result is claimed. Checks cover a known RNG word, replay/range, sampler domains and geometric invariants, a known distribution moment, the analytic integral, invalid count, MIS identities, zero-support strategies, and tiny/subnormal PDFs, known online statistics, parallel/tangent/behind box cases, empty/owned indexes after caller allocation release, exact primitive bounds, BVH-versus-linear deterministic rays, centroid and hit ties, known oblique/inside-sphere roots, grazing neighbors, invalid ids/geometry, and bounded construction/query input. The test near x = nextafter(1,2), z = 1000 caught a false linear hit from quadratic cancellation. The closest-point implementation rejects that miss; exact and inward-adjacent tangent cases remain hits. This regression is an independent geometric expectation, not merely agreement between two queries. An independent reviewer rechecked all three adjacent-ray outcomes against the final source and confirmed the reported cancellation case is fixed. ## Primary reading and credit The course explanations and assignments are original. Further reading: - [PBRT fourth edition · Monte Carlo Integration](https://pbr-book.org/4ed/Monte_Carlo_Integration) - [PBRT · Sampling Multidimensional Functions](https://pbr-book.org/4ed/Sampling_Algorithms/Sampling_Multidimensional_Functions) - [PBRT · A Simple Path Tracer](https://pbr-book.org/4ed/Light_Transport_I_Surface_Reflection/A_Simple_Path_Tracer) - [PBRT · Bounding Volume Hierarchies](https://pbr-book.org/4ed/Primitives_and_Intersection_Acceleration/Bounding_Volume_Hierarchies) - [SplitMix implementation and dedication, Sebastiano Vigna](https://prng.di.unimi.it/splitmix.c) Keep supplied-code and algorithm credit when adapting the reference. A finished learner renderer should identify its own integration, scene, tests, evidence, and limitations separately from these experiments. The 13 runnable C++ snippets across Light Transport and Renderer Engineering also compile independently as C++23 with strict warnings treated as errors. Their combined review harness passes 21 runtime checks under ASan/UBSan, including the restricted entry-only Schlick approximation. The exact scope and remaining limits are recorded in `audits/transport-concurrency.md` at the project root. The diagnostic-string error representation can itself allocate; `expected` is not a universal no-throw guarantee, and the executable catches exceptions. The new bridge verification and original-source fact checks are recorded in `audits/renderer-cpu-opengl-update.md`. A completed 96×64 image was visually inspected; its Debug and Release captures replayed identically on the local host.