Part 2 of the Metal PathTracer Architecture series, based on the renderer documentation snapshot created on May 14, 2026.
The first path-tracing implementation in my Metal renderer was a megakernel. It was a natural place to start: most of the path logic lives together, the control flow is easy to follow, and there are fewer moving parts between generating a ray and accumulating its contribution. That path remains important. As I investigate more interactive workloads on Apple GPUs, however, I need to see not only what a path does, but how much active work is waiting at each stage. That is the reason for the wavefront path.
Wavefront rendering is a staged alternative to the megakernel integrator. Instead of asking one large kernel to carry a path through many kinds of work, it makes queues and dispatches explicit. Rays can be generated, intersected, shaded, sent toward lights, and accumulated through separate steps. The attraction is not a promise of automatic speed. The attraction is visibility: active counts, queue capacity, overflow, and producer/consumer relationships become data I can inspect.
Why keep the megakernel?
It would be tempting to treat the wavefront implementation as a replacement and remove the older path. I do not think that would help. The megakernel is a useful correctness and parity reference while the staged path matures. It also provides a simpler scheduling path when a wavefront feature is not yet ready. If the two modes disagree, I have a concrete comparison instead of a moving target.
Keeping both modes also changes the tone of optimization work. A wavefront dispatch can look successful because it produces an image, while still doing the wrong amount or kind of work. Comparing it with the megakernel helps distinguish a scheduling bug from an ordinary image-noise difference. The fallback is therefore part of the experimental method, not an admission that the newer path has no value.
Queues as a contract
The current queue model is deliberately typed. It includes primary, diffuse, glossy, specular, transmission, and shadow rays, as well as material evaluation, reservoir updates, medium work, cache queries, cache training, and guiding training. Each queue carries more than a counter. Its contract includes capacity, an overflow counter, queue type, frame tag, producer and consumer metadata, peak active count, and a storage policy.
That information gives me a vocabulary for asking what a dispatch actually did. A queue can have a large capacity but a small active count. It can be dense, sparse, indexed, or mostly metadata. An overflow counter can tell me that a successful-looking frame exceeded an assumption. A frame tag can help catch stale work crossing a frame boundary. These are not decorative statistics; they describe the state that scheduling decisions depend on.
There is one PathState for each path index. At the moment it is an array-of-structures layout, which keeps indexing straightforward while I validate the wavefront path against the megakernel. The state carries throughput and accumulated radiance, first-hit AOV data, random-number state, medium-stack metadata, spectral-sampling fields, the last BSDF PDF, and path flags. That is enough context to move a path between stages without relying on a single monolithic invocation.
The conservative bridge
The current bridge between the queue model and Metal dispatches is intentionally incomplete. Intersection supports active-count indirect dispatch: when fewer rays are alive, the dispatch can reflect that count. Shading still uses a direct grid in the documented state. Shadow work also uses a direct grid because shadow storage is sparse by path index.
The shadow layout makes the issue especially clear. A shadow slot is derived from a path index and a local slot:
shadowSlot = pathIndex * kWavefrontShadowCapacityMultiplier + localSlot
This pattern is safe for associating shadow work with its originating path, but it does not create a dense array of valid shadow rays. shadowRayCount alone is therefore not enough to iterate over every valid slot. A future closure step needs either dense shadow storage or a dense index queue that points to the sparse slots.
This is exactly the kind of detail that can disappear behind a successful render. A direct-grid fallback keeps the current behavior visible while the compact representation is designed and checked. The goal is not to make every dispatch indirect immediately. The goal is to move to active-count scheduling without allowing queue bugs to hide behind an apparently efficient path.
What wavefront makes measurable
The real-time motivation is about wasted work as much as raw ray throughput. If a stage has a small active population but still launches a full grid, the cost of inactive lanes becomes part of the frame. If a queue overflows, the renderer needs to report that rather than silently dropping a path. If a producer writes a storage format that a consumer interprets differently, the resulting image may be noisy without revealing why.
Wavefront scheduling gives me places to put those observations. I can track active and peak counts, compare capacity with demand, and retain the full-grid path as an explicit fallback. I can then ask whether a proposed dispatch change reduces actual work while preserving the image produced by the reference mode. On an Apple GPU, where the renderer has to operate within the behavior and tooling of Metal, that feedback loop matters more than a vague claim that staged execution is “more real time.”
There are still tradeoffs. More queues mean more state and more synchronization. An array-of-structures path state is simple to address but not necessarily the final layout for every access pattern. Sparse shadow storage preserves associations but complicates compaction. Every stage boundary is an opportunity to expose useful metrics and an opportunity to introduce a new failure mode.
The work ahead
The next wavefront questions are concrete. I need to close the faults in shade indirect dispatch, convert shadow dispatch to a compact or indexed active-count model, and distinguish compact, sparse, indexed, and metadata-only storage in the public metrics. I also need to keep the full-grid fallback visible during that work so a queue issue cannot be mistaken for a successful optimization.
For now, the megakernel and wavefront paths have complementary roles. The megakernel gives me a dependable baseline and a clear path through the integrator. Wavefront gives me a staged model in which active work, queue pressure, and dispatch waste can be examined directly. The transition is not a one-time rewrite; it is a series of measured changes.
That is the part I find most useful. A wavefront renderer is not defined by the number of queues in a diagram. It is defined by whether those queues have clear contracts, whether their consumers agree with their producers, and whether the resulting scheduling decisions can be checked against a trustworthy reference. Building that foundation on Apple GPUs is slower than declaring the architecture finished, but it gives the experiments a chance to survive contact with real scenes and real frame budgets.
Continue the series
Previous: Building a Metal Path Tracer into an Interactive Research Renderer.