A path tracer in sysl, in the scope of Peter Shirley's Ray Tracing in One Weekend and Ray Tracing: The Next Week.
- Shapes: spheres (moving ones too, for motion blur), parallelograms, triangles (Möller–Trumbore)
and triangle meshes from a small OBJ reader (
obj.sysl), plus translated and rotated instances. - Materials, each a trait object: Lambertian, metal with fuzz, dielectric (Snell's law, Schlick's reflectance, total internal reflection) and diffuse lights.
- Textures: solid, a 3D checker, Perlin marble (turbulence), and images read with
png. - A positionable thin-lens camera with defocus blur.
- A BVH split along the longest axis.
- Paths end at a maximum depth, or optionally earlier by Russian roulette (
--roulette). - Deterministic: every sample has its own PCG32 stream (
sysl.rand), keyed by its pixel and its number, so the image is bit-identical across runs and across thread counts. - Rows are rendered on a pool of
sysl.threadsworkers that take rows from an atomic counter. - Output is PNG (encoded by
cairo) or binary PPM.
sysl run . -- showcase -w 480 -h 270 -s 128 -o showcase.png
sysl run . -- cornell -w 300 -h 300 -s 256 -o cornell.png
sysl run . -- showcase -w 1600 -h 900 --rays primary-ray throughput, one thread
Options: -w/-h set the size, -s the samples per pixel, -d the longest path, -t the number of
threads (by default, every core), and -o the file (.ppm for a PPM, anything else for a PNG).
It needs cairo (brew install cairo) for the PNG encoder.
sysl test . runs 46 tests. Each one checks against something the tracer did not compute itself.
- Intersections against closed form:
- a sphere hit head-on, off-axis (
5 − √(1 − 0.36)), from inside, and missed; - a moving sphere at two instants;
(u, v)at the poles and meridians;- a triangle's barycentric coordinates, and a slanted ray onto
x + y + z = 1; - a quad's edge fractions on its front and back faces;
- the slab test, including parallel rays and the interval's end.
- a sphere hit head-on, off-axis (
- Optics:
- refraction at 45° into glass against Snell's law (the values are numpy's);
- normal incidence;
- Schlick's formula at 0°, at grazing incidence and at
cos = ½; - total internal reflection past the critical angle;
- metal absorbing below the surface;
- emission.
- White furnace:
- lossless Lambertian, metal and glass surfaces in a uniform white sky must come out exactly white (to 1e-6, at 200 bounces);
- the same scene with Russian roulette must converge to white, which shows it is unbiased;
- with paths cut at two bounces, energy must go missing.
- BVH against brute force: 20,000 random rays through 320 random spheres, triangles and quads must give a bit-identical nearest hit from the BVH and from a linear list.
- Threads: the showcase must be bit-identical at 1, 3 and 8 threads, and a different seed must give a different image.
- The final image:
- the showcase at 160×90, 16 spp, must match the committed
assets/reference.pngwith a PSNR above 40 dB; - each channel's mean and standard deviation must be within 0.5 of the values numpy measured from that PNG.
- the showcase at 160×90, 16 spp, must match the committed
- Formats:
- PPM and PNG must round-trip byte for byte (PNG is written by cairo and read back by
sh.sysl.png); - the PPM header parser must accept comments and refuse what it cannot read;
- the gamma encoding is checked.
- PPM and PNG must round-trip byte for byte (PNG is written by cairo and read back by
- Error paths: an OBJ with a bad number, a missing vertex, index 0, or a face with two corners must be refused at the right line, and an empty image texture or an unknown scene name must be handled.
bench/rt.c ports the showcase to C, step for step: the same PCG32 streams, the same order of random
draws, the same BVH, and the materials behind function-pointer tables. Its image is byte-identical to
the sysl one (400×225 at 32 spp, cmp finds no difference; the planet's image is generated in C
by the formulas of assets/make_assets.py). That makes the C port a second, independent check on the
whole renderer.
cc -O2 -o rt-c bench/rt.c -lm && ./rt-c 400 225 32
Measured on an M-series Mac, single-threaded, with the sysl program built at optimization = "2":
| C -O2 | sysl | ratio | |
|---|---|---|---|
| showcase 400×225, 32 spp | 732 ms | 1239 ms | 1.7× |
| primary rays, 1600×900 | 80 ms | 170 ms | 2.1× |
The showcase row is a single run, on the scene with the textured planet. The primary-ray row was measured on the previous scene (a checkered ball) as the median of seven runs, interleaved with the C port's; shading does not enter it. Thread scaling, 400×225 at 32 spp (single runs, planet scene):
| threads | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| ms | 1239 | 747 | 487 | 449 | 379 |
The rest of the gap is the compiler's. Ray and Aabb already travel between sysl functions in
registers, as LLVM aggregates, and Aabb.hit is inlined into BvhNode.hit as the C port's box test
is. What remains is:
Option[Hit]is returned through a hidden pointer. It is too large to come back in registers, so every level of the BVH writes its answer to memory and its caller reads it back. The C port fills in onehit_recordthrough a pointer that it passes all the way down.- Retain and release pairs survive inlining.
Hitcarries its material as a&sync Material, and each copy of theOption[Hit]that the BVH passes up still retains and releases that material with an atomic add. Threeldadds are left inBvhNode.hit, and they are where the threads contend.
- There is no list of hittables in a scene, for the same reason. The BVH is a tree of
&syncnodes, so it does not need one.HitListexists for the brute-force test, which runs on one thread.
The algorithms, the scene layout and the Cornell box are Shirley's (Ray Tracing in One Weekend and
The Next Week, v4). assets/planet.png and assets/icosphere.obj are generated by a short Python
script.
ISC — see LICENSE.

