2
\$\begingroup\$

I'm playing around making a shadertoy style SDF raytracer in HLSL and to make it run faster on high resolutions (1080p and up) I'd like to spread the computation over multiple frames.

Right now I have a fragment shader that does raytracing for every pixel of a screen sized quad. I've noticed that if I discard (clip()) the fragment for every second pixel, I get 0 framerate improvement despite effectively only rendering half the image. But if I lower the render texture resolution to half, I see improvement obviously. Also if I clip a contiguous half of the fragment and render the other half, I get a similar performance boost.

This must mean that the gpu thread groups are by default allocated in a tiled fashion on the screen, and that all threads of a group must be done computing before the (other) resources of the thread group can be used again.

Does anyone know of any magic that can force a fragment shader to only allocate resources for say half the pixels of the screen? (but keep the render target size)


Alternatively I suppose I could assign my own thread groups in a Compute shader system where I manage all this stuff myself. Is that the only way to do it? And how would I handle the render target then? (know any resources?)

I'm also pretty sure I've seen people make this kind of grainy multi-frame raytracers years ago before compute shaders were a thing.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

clip() and discard() won't get you any performance increase, nor is it intended to. To understand why, consider how the GPU does things.

This must mean that the gpu thread groups are by default allocated in a tiled fashion on the screen, ...

Correct. Not only is this the "default," it is mandated by the underlying hardware in the fixed-function rasterizer stage.

...and that all threads of a group must be done computing before the (other) resources of the thread group can be used again.

Shaders are executed in waves of 64 (AMD) or 32 (NVIDIA) threads. These threads execute in SIMD fashion, meaning that they execute in lock-step.

The wave will execute at the rate of the slowest thread. If some threads clip() or return say, halfway through the shader, they will still stall until all threads in the wave have completed. They aren't like CPU threads. (I consider the term "thread" to be something of a misnomer in this case but, hey, it's the world we live in.)

Now, with shader model 6 on the horizon, or with Vulkan right now, you can use wave ballot intrinsics to early-out and get some of that execution time back. However, even with those tools, the wave will still take as long as the the slowest thread. So, turning off every other pixel won't get you anything (as adjacent pixels are typically assigned to the same wave).

Does anyone know of any magic that can force a fragment shader to only allocate resources for say half the pixels of the screen? (but keep the render target size)

You may be able to use a stencil to mask off every other pixel, but this wouldn't give you back 100% of the time you're looking to save as there's overhead for the stencil comparison. Also, that just seems like a kludge.

The best and easiest way to only draw half of the pixels is to render to a 1/4 resolution render target (1/2 on each side), then draw it scaled to match your screen-aligned quad. You can play with your sampler configuration when drawing to your screen-aligned quad to get the "grainy" pixelated image, if that's what you want.

You COULD do this in compute, but you'd be handling rasterization yourself. Then again, that may be a more appropriate place to implement raytracing. I've written raycasting code in both CUDA and HLSL compute, and it works pretty well. If you don't need the rasterizer, compute is a perfectly reasonable way to go.

\$\endgroup\$
3
  • \$\begingroup\$ Otoh, one could imagine that if consistently doing every 2nd pixel on all threads, the total should be faster? Then again, my impression is that if the code segment you early-exit holds for example a tex2D later on, the sampler will execute and consume time in any case? Splitting into smaller homogenous pieces sounds right. And ofc one might still be able to trick each halve into actually representing a fullscreen, with 1 px offset in between. With some mambojambo :-). But reading from those two into every other px in the result sounds ... hmmm \$\endgroup\$
    – Stormwind
    Commented Dec 16, 2016 at 15:08
  • \$\begingroup\$ Sorry for the late reply (was on vacation). Thanks a heap for the explanation. I don't think you can get the effect of rendering every other pixel every frame by using two 0.5x res render textures. The best I can do this way is split the screen into many small-ish quads but that would look patchy. So I think I have to do it with a compute shader, where my fullscreen texture is a 2D shared buffer, and the x available compute threads write to it at every other index. \$\endgroup\$ Commented Dec 31, 2016 at 9:30
  • \$\begingroup\$ @Twodordan Welcome back. I wasn't saying to use two render targets - just one that's half the width and height of your window. Then, draw the render target scaled 2x to a screen-aligned quad. In the fragment shader, check your pixel coordinates and discard even or odd pixels. You'll save the cost of lighting 3/4 of window, and get the effect you're looking for. \$\endgroup\$
    – 3Dave
    Commented Jan 2, 2017 at 15:01

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .