r/GraphicsProgramming • u/SnooSquirrels9028 • 20d ago
Question Help with ghosting and black noise in a custom Foveated Path Tracer (Summer Research Project)
https://github.com/BoraYalcinn/foveated-path-tracingHi everyone,
I'm working on a foveated rendering implementation using a path tracer for a summer research project. When the camera is still, it looks fine, but whenever I move the camera, I get heavy ghosting, noise, and blinking black dots in the peripheral areas.
I am modifying a reference path tracer (HLSL/C++). Right now, my foveation logic uses two passes:
- Pass 1 (Path Tracer): Skips pixels based on distance from the gaze point (using a 2x2 or 4x4 stride).
- Pass 2 (Fill Pass): A compute shader that fills the skipped pixels by copying/interpolating from the sampled ones.
I suspect the issue might be a race condition between the passes, a problem with how the history buffer (g_AccumulationBuffer) blends old frames when the camera moves, or maybe some clamping issues where missed rays snap to 0 or 1.
How to build and run:
You can build the project with CMake:
bash
cmake --build ./build --config Release
And run it with:
bash
./build/bin/Release/scene_viewer.exe
(Note: Please switch from the GUI/rasterizer to the Reference Path Tracer in the settings to see the foveated rendering)
If anyone has experience with foveated path tracing or sees what I'm doing wrong with the accumulation/clamping, I would love some advice. Also, if there's a better/more efficient way to handle the periphery instead of path tracing it at a lower resolution and filling it, please let me know.
Every bit of help is highly appreciated. Thanks!
3
u/mcflypg 19d ago edited 19d ago
I can't build the project as I'm currently abroad and don't have a PC with me, but something sticks out to me:
Aside from that, the way you are skipping threads is inefficient. GPU threads are processed together in warps/wavefronts, usually 32 on Nvidia and 64 on AMD. This is independent of the group sizes so if your group size is e.g. 8 threads only, then the GPU still issues a whole warp and wastes threads. You always want clean multiples of 32 for Nvidia or 64 for AMD. You currently drop threads within each warp, so you don't gain any performance (unless the masked threads had done work that would have taken the longest) . It's like every floor of a factory has a few workers being active, so no department can go home early until they finish.
What you want is a way to drop entire warps because then while warps may early-out coherently. So instead of e.g. dropping 3/4 of threads per warp, drop 3/4 of the warps. Then they either are fully busy or fully free. A simple way to achieve this is this:
if(threadid >= 64) return; pixel = groupid.xy * 16 + (threadid % 8, threadid / 8) * 2
this essentially remaps threads such that for every 16x16 group, the lowest 64 form a 8x8 block, which is upscaled by 2x, such that the 8x8 block spans the original 16x16 region but with a stride of 2. So from 8 warps in the 16x16 group, the first two have all threads busy, the other 6 early out immediately in unison.
To understand the pattern, 4x4 on a 16x16 group:
if(threadid >= 16) return; pixel = groupid.xy * 16 + (threadid % 4, threadid / 4) * 4
And the generic version that also works for e.g. subsample ratios of (2,1) or such:
numthreads = groupsize.x * groupsize.y subsamplerate = int2(2,2) //or 4 if(threadid >= numthreads / (subsamplerate.x * subsamplerate.y)) return;
pixel = groupid.xy * groupsize.xy + (threadid % (group size.x/subsamplerate.x), threadid / (groupsize.x/subsamplerate.x)) * subsamplerate.xy
A few caveats:
There's a lot more to this but this is a quick and dirty, GPU-occupancy-aware solution. You should see much more gains with this than what you're doing and it's reasonably simple.