< projects

Parallel Renderer

A Monte Carlo path tracer in C++20, built to compare how four ways of splitting an image across CPU threads scale.

when
Apr–Sep 2026
role
Solo

key results

demo

media

The medium scene, grainy at 16 samples per pixel. Coral, yellow, teal and clear glass spheres sit on a pale floor between a light blue wall and a dark blue wall, under a square ceiling light.
The medium scene at benchmark settings (800×450, 16 spp, depth 8, seed 123).
The same scene at 512 samples per pixel, almost free of noise, with soft shadows and light focused under the glass sphere.
The same scene at 1600×900 and 512 spp.

how it works

four schedules

The image is split into 32 px tiles, and each schedule hands tiles to threads differently:

benchmark

The medium scene renders at 800×450 with 16 samples per pixel, depth 8, 32 px tiles and seed 123. Each configuration runs 5 times and the median counts. The machine: an AMD Ryzen 9 9950X3D (16 cores, 32 threads) on WSL2 Ubuntu 24.04, GCC 13.3 at -O3.

In the 2026-09-01 reference run, serial takes 474.9 ms and dynamic 35.7 ms at 16 threads, a 13.29× speedup (83.1% efficiency); OpenMP reaches 13.16× (82.3%). A 2026-10-23 sweep added static on the medium scene: its efficiency falls from 90.9% at 2 threads to 55.6% at 16, where dynamic is 1.57× faster. Tiles cost different amounts to render, so a fixed split leaves some threads idle while others finish. The results page charts every run.

same image, any schedule

Each sample’s random numbers are seeded from the seed, the pixel and the sample index, so the image doesn’t depend on which thread renders which tile. CTest checks serial against OpenMP on an image split into 6 tiles, and the serial image against a golden FNV-1a hash; both pass. A serial-vs-dynamic check exists too, but its test image fits in a single tile, so it doesn’t yet exercise the atomic scheduler.

the rest

what i'd do next