0
$\begingroup$

I'm writting a real-time unidirecional path tracer that runs entirely on the GPU. After successfully implementing it using a "megakernel" approach, I decided to switch to a "Wavefront" or "Streaming" path tracer, as described here: Van Antwerpen, Aila et al., Jacco Bikker.

The idea is to divide the algorithm into smaller kernels, improving resources usage and decreasing control flow divergence. For the most part it works fine, but the performance takes a turn for the worse when it comes to more complex scenes, compared to the former single kernel one.

My implementation is written in C++ and GLSL, and in pseudo-code terms it's something akin to these:

void Update()
{
    ...
    /* Generate primary rays and store these into a SSBO to be used by the next kernel */
    generateKernel.Dispatch();
    ...
    for(auto i = 0; i < MAX_BOUNCES; ++i)
    {
        /* Extend rays (for the first iteration these are WIDTH * HEIGHT camera rays) and store results */
        extendKernel.Dispatch();
        ...
        /* Read results and check for path termination. Shade result accordingly and request for the next interation extended ray and shadow ray, if needed  */
        shadeKernel.Dispatch();
        ...
        /* Similar to extendKernel, but for shadow rays */
        connectKernel.Dispatch();
    }
    /* Display result */
    outputKernel.Dispatch();
    ...
}

Since we no longer have a single kernel to which fetch data from, we need to store path states and intersection results into the GPU global memory, in order to access it from the other micro-kernels. However, I've noticed that writing to this global memory is what is supposedly causing this performance issue, even when the threads within a work group are writing to sequencial slots in a given buffer. For exemplification sake, let's consider the extend kernel and nothing else:

/* Extend kernel */
...
/* MIN_WORK_GROUP_INVOCATION_X = 1024, minimum allowed in the specification */
layout(local_size_x = MIN_WORK_GROUP_INVOCATION_X) in;
...
layout(std430, binding = 0) buffer RayQueues
{
    Ray extendRay[MAXWIDTH * MAXHEIGHT];
    Ray shadowRay[MAXWIDTH * MAXHEIGHT];
} Queue;

layout(std430, binding = 1) buffer Results
{
    /* SoA approach yields better performance */
    vec3 N[MAXWIDTH * MAXHEIGHT];
    vec3 V[MAXWIDTH * MAXHEIGHT];
    float t[MAXWIDTH * MAXHEIGHT];
    uint matid[MAXWIDTH * MAXHEIGHT];
} Intersection;
...
void main()
{
   uint tid = gl_GlobalInvocationID.x;
   ...
   Ray extendRay = Queue.extendRay[tid];
   Hit hit = ClosestHit(extendRay);  
   ...
   /* Write results to be used in the Shade kernel */
   /* Multiple writes per iteration to the SSBO is what is (?) causing the performance issue */
   Intersection.N[tid]     = hit.N;
   Intersection.V[tid]     = hit.V;
   Intersection.t[tid]     = hit.t;
   Intersection.matid[tid] = hit.matid;
   ...
}

Since there's no path regeneration, we trace extension rays until there's no rays left in the buffer or we reach the maximum depth allowed (or the ray is terminated by Russian Roullete method). But like I said, in more complex scenes these rays might not terminate as early, resulting in giant buffers being written in each bounce.

Is there a better way to approach this problem? How can I make this more performant or what crucial mistake am I doing?

$\endgroup$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy