I'm running following compute shader in Vulkan. worldImage is device local memory that holds the world. resultImage is reference to the memory of the swapchain image:

#version 450

layout (binding = 0, rgba8) uniform writeonly image2D resultImage;
layout (binding = 1, r16ui) uniform readonly uimage3D worldImage;

layout(push_constant) uniform PushConstants {
    vec3 rayOrigin;
    float viewGridTopLeftCornerX;

    vec3 viewGridRight;
    float viewGridTopLeftCornerY;

    vec3 viewGridDown;
    float viewGridTopLeftCornerZ;

    float viewGridDensity;
} push;

void main() {
    vec3 viewGridTopLeftCorner = vec3(push.viewGridTopLeftCornerX, push.viewGridTopLeftCornerY, push.viewGridTopLeftCornerZ);
    vec3 samplePoint = viewGridTopLeftCorner + push.viewGridDensity * (gl_GlobalInvocationID.x * push.viewGridRight + gl_GlobalInvocationID.y * push.viewGridDown);

    vec3 ray = normalize(samplePoint - push.rayOrigin);
    vec3 inverseRay = 1 / ray;

    ivec3 voxelCoords = ivec3(floor(push.rayOrigin.x), floor(push.rayOrigin.y), floor(push.rayOrigin.z));
    ivec3 rayOrientation = ivec3(sign(ray));
    ivec3 rayPositivity = (1 + rayOrientation) >> 1;
    vec3 withinVoxelCoords = push.rayOrigin - voxelCoords;

    uint blockID = 0;
    int smallestComponentIndex;
    do {
        // Calculate how far each of the neighbouring voxels are
        vec3 distanceFactor = (rayPositivity - withinVoxelCoords) * inverseRay;

        // Pinpoint the closest voxel of the three candidates
        smallestComponentIndex = distanceFactor.x < distanceFactor.y
        ? (distanceFactor.x < distanceFactor.z ? 0 : 2)
        : (distanceFactor.y < distanceFactor.z ? 1 : 2);

        // Move to that voxel
        voxelCoords[smallestComponentIndex] += rayOrientation[smallestComponentIndex];

        // Advance the ray the distance to the closest voxel in all dimensions
        withinVoxelCoords += ray * distanceFactor[smallestComponentIndex];

        // The axis towards the next voxel will now have value -1 or 1. Next line resets it to 0.
        // It can be imageined as going jumping the side of the previus voxel to the side of the next one.
        withinVoxelCoords[smallestComponentIndex] = 1 - rayPositivity[smallestComponentIndex];

        blockID = imageLoad(worldImage, voxelCoords).r;
    } while (blockID == 0);

    switch (blockID) {
        case 1:
            vec3 normal = vec3(0, 0, 0);
            normal[smallestComponentIndex] = rayOrientation[smallestComponentIndex];
            imageStore(resultImage, ivec2(gl_GlobalInvocationID.xy), vec4(0.0, 1.0 * dot(ray, normal), 0.0, 1.0));
        case 2:
            imageStore(resultImage, ivec2(gl_GlobalInvocationID.xy), vec4(1.0, 0.0, 0.0, 1.0));
            imageStore(resultImage, ivec2(gl_GlobalInvocationID.xy), vec4(0.0, 0.0, 1.0, 1.0));

The code traces one ray per pixel until it hits a block (value 1) or the edge (value 2, every edge block has the value 2).

If the parameters are:

  • 32x32x32 world with player standing in the middle (so ray tracing itself goes through half of the voxels at worst)
  • 1280x720 resolution (gl_GlobalInvocationID x and y)

It takes about 50ms to render one frame on GTX 1050 or about 5ms on RTX 2080 Ti.

The main loop acquires the image, submits the command buffer which transitions the image to proper (general) layout, binds the pipeline and descriptor sets, pushes the push_constants, dispatches the compute shader and transitions image to present layout again. This is what happens in those 50ms/5ms.

I ran similar code on a single CPU core (i5 4760K) and it takes about 250ms. This would be equivalent to running just the shader code though.

I can't believe that something like this would be only five times faster on the GPU than on the CPU.

I have no experience in graphics, so I can't really tell. Is this really the time it takes for this, or could I expect (hopefully a much) better performance, once I fix whatever is bogging this down?

Could it be the memory access on the GPU? I don't know what else it could be.

  • 2
    $\begingroup$ At a guess I'd say that the way you're doing imageLoad in a loop with computed values is completely destroying the cache. The perf difference between the 1080 and the 2080 may have to do with how much the per compute unit cache can hold. A 32x32x32 16 bit image is 65k all on its own. I'm pretty sure that's way bigger than the SM L1 cache on a 1080. $\endgroup$
    – Jherico
    Aug 17, 2019 at 6:02
  • $\begingroup$ Any way I can deal with that? The goal is to have a much larger world loaded which would just make it worse... $\endgroup$ Aug 17, 2019 at 8:02


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.