Skip to main content
GameDev.net gamedev.net
🔒 Locked

[D3D12] Use compute shader to write to swap chain backbuffer?

Started by Drilian Dec 6, 2015 at 10:01 AM 13 replies 21.3k views
Original Post
Drilian
Drilian

Hey! I was hoping to do all of my postprocessing (tonemapping, bloom, etc) in my D3D12 renderer using compute shaders, Thus, I think, in an ideal world I'd just write to it as a RWTexture2D in the compute shader and blast the processed colors out directly.

However, IDXGIFactory4::CreateSwapChain fails if I try to specify a BufferUsage that contains DXGI_USAGE_UNORDERED_ACCESS ("DXGI ERROR: IDXGIFactory::CreateSwapChain: The BufferUsage field of the swapchain description contains some DXGI_USAGE flags that are not supported.")

Without that, I can't create an unordered access view to it, or transition it into the proper state using a resource barrier, so I can't seem to write to it using a compute shader at all!

Is there a way around this (another way to write to a texture from compute, or another way to create a swap chain that plays nice), or should I just go back to postprocessing using pixel shaders like a caveman?

backstep
backstep

Perhaps someone more knowledgeable can comment on how correct or optimal this is, but in both D3D11 and now D3D12 I've always used an intermediate texture2D resource for my compute output (bindflags uav and srv usage, accessed as RWTexture2D in compute shader).

That's then transitioned to D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE and copied into the actual back buffer by rendering a fullscreen triangle using the normal pipeline (accessed as Texture2D in pixel shader), with the backbuffer bound as the RTV. The UI is then rendered into the backbuffer etc. There's probably a way to do a more direct copy if the intermediate resource and backbuffer share the same dxgi_format, although usually my compute output is still in a HDR format so the pixel shader is required for tone mapping anyway.

Adam Miles
Adam Miles

D3D12 now disallows UAV usage on swap chains. You'll need to copy into the swap chain or ensure the last event in your frame can be performed using a pixel shader that can write directly to the swap chain. By all means keep the rest of your post-processing in Compute though.

Adam Miles - Principal Software Development Engineer - Microsoft Xbox Advanced Technology Group
Drilian
Drilian

Thanks for the info. Yeah, that kinda sucks (it's a sad regression from D3D11), but I've at least worked around it in the short term by having a simple copy postprocess (via a pixel shader) that gets things onto the backbuffer and still allows all of my postproc to run as compute (as you suggested).

Thanks again!

EDIT: I suppose I could use CopyTextureRegion instead and use that fancy DMA hardware that may or may exist, if I'm already using a backbuffer-format-compatible texture by the time I'm at the end of the postproc chain. But a pixel shader works well enough for now.

pcmaster
pcmaster

Side note: Watch out for the "fancy" DMA; for exaple on consoles it can have an unexpectedly shitty performance. Be sure to profile it.

Dingleberry
Dingleberry

As an aside, the vast majority of post processes will be slower on a compute shader. Doing it with a pixelshader isn't doing it like a caveman. You're rasterizing colors in a very coherent manner which GPUs have been designed to do quickly, leveraging more hardware than a compute shader by itself can do. It can do things like compress the output of your colors to a render target to reduce bandwidth, and there's just no way to do that in a compute shader.

Given that most of your texture fetches are going to be cached in a post process shader, you need to be doing a lot of cooperative work in a compute shader to realize a benefit.

Adam Miles
Adam Miles

As an aside, the vast majority of post processes will be slower on a compute shader. Doing it with a pixelshader isn't doing it like a caveman. You're rasterizing colors in a very coherent manner which GPUs have been designed to do quickly, leveraging more hardware than a compute shader by itself can do. It can do things like compress the output of your colors to a render target to reduce bandwidth, and there's just no way to do that in a compute shader.

Given that most of your texture fetches are going to be cached in a post process shader, you need to be doing a lot of cooperative work in a compute shader to realize a benefit.

What do you mean by that (bolded)? Are you suggesting that writes to UAVs can't be made to formats smaller than 32bits per channel?

Adam Miles - Principal Software Development Engineer - Microsoft Xbox Advanced Technology Group
Dingleberry
Dingleberry

I mean that the rasterizer writes to memory in a different way than a compute shader writes to a UAV. If you're writing coherent color values to a giant rectangular render target texture meant for display purposes, a pixel shader is going to be very fast at this relative to writing from a compute shader to a UAV. If you're profiling your code you're probably going to realize this immediately as doing even a trivial post process effect with a compute shader is going to measure considerably slower than the pixel shader equivalent.

Adam Miles
Adam Miles

That's not been my experience on hardware of the last few years at least.

In fact, since Compute can bypass traditional limits surrounding fill-rate (pixels per clock) by avoiding the use of ROPs entirely you may even find that Compute is faster than Raster in trivial shaders that would otherwise have hit that bottleneck before any other. As the shaders get more complicated you either run into bandwidth or ALU being the limiting factor, neither of which are inherently faster or slower in Compute or Raster. In the class of post-processing shaders that share taps between threads (blurs, for example), use of Compute/LDS is likely to offset any mild performance cost that using Compute might have had.

The definition of "considerably" is up for debate, but I'm not sure I've seen Compute ever come out more than about 10% slower than Raster in anything I've tried.

Adam Miles - Principal Software Development Engineer - Microsoft Xbox Advanced Technology Group
Infinisearch
Infinisearch




That's not been my experience on hardware of the last few years at least.

This is the impression I was under as well, from reading at least.

-potential energy is easily made kinetic-
Dingleberry
Dingleberry

This article is from a year ago but it matches all my past observations and things I've been told: https://software.intel.com/en-us/blogs/2014/07/15/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms

The constant time compute shader blur is over twice as slow in a small kernel and still very slow until the kernel size is huge. Also consider that when you're writing to a UAV that represents a render target, you're either doing non-coherent writes because the destination texture is swizzled, or you're writing to linear memory in which case you probably need to re-swizzle it later to be useful.

Things could certainly be different now which would be great but my general rule has always been that unless I'm doing a whole lot of work with shared memory, the pixel shader's gonna be faster.

E: I don't mean to be a downer or anything, if you write a compute shader that's faster than a pixel shader I would definitely be interested in seeing it though.

Drilian
Drilian

This article is from a year ago but it matches all my past observations and things I've been told: https://software.intel.com/en-us/blogs/2014/07/15/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms

The constant time compute shader blur is over twice as slow in a small kernel and still very slow until the kernel size is huge. Also consider that when you're writing to a UAV that represents a render target, you're either doing non-coherent writes because the destination texture is swizzled, or you're writing to linear memory in which case you probably need to re-swizzle it later to be useful.

Things could certainly be different now which would be great but my general rule has always been that unless I'm doing a whole lot of work with shared memory, the pixel shader's gonna be faster.

That's an apples to horseshoes comparison, though, unless I'm misreading - they're comparing the timings of two different techniques (both of which are running in compute from the sound of it) and comparing the timings of each. Yeah, the constant-time shader is slower, but it's solving a different problem. You wouldn't use that for a small kernel because of all the extra overhead involved.

There are other ways to make compute blur faster (like using groupshared memory which doesn't seem to be mentioned in that article, which can drastically reduce your memory accesses), but it doesn't say anything about comparing a 1:1 pixel shader:compute shader version of the same operations.

Also, how you decide to break your compute shader up can make a difference with performance (including cache coherency) - if your compute shader's thread groups load/store things in BLOCKS as opposed to in scanlines, you're likely to get better coherency group to group because you'll be writing into places that are related in memory, as I understand it.

I don't think that compute postprocessing is NECESSARILY slower than a pixel shader, but it does likely depend at least partly on how you've got it set up.

Dingleberry
Dingleberry
There's an obvious way to figure this out, I'll try some tests later.
Dingleberry
Dingleberry

I made a test program: https://github.com/stanlo/CsVersusPs

It's in directx 11 because I'm on a win7 machine at the moment but that shouldn't really matter too much. I'm not doing anything fancy.

It writes some test values from a PS and some different test values from a CS, and verifies their values by reading it back and looping over them. It also times both (the execution, not the readback)

On my GTX 960, I get results that look like this

Measuring PS:
0 - 63520 ticks elapsed
1 - 61408 ticks elapsed
2 - 62336 ticks elapsed
3 - 61440 ticks elapsed
4 - 62240 ticks elapsed
Measuring CS:
0 - 419872 ticks elapsed
1 - 105472 ticks elapsed
2 - 105280 ticks elapsed
3 - 104480 ticks elapsed
4 - 104320 ticks elapsed

That's a lot slower doing basically nothing. You can try different thread group sizes in the config, it defaults to 32x32x1. I fiddled around a little and 16x16x1 seemed to be a sweet spot yielding around 82944 ticks, but that's still about 30% slower.

Of course if there were more arithmetic operations going on the numbers would start to converge, but the point is that there's an intrinsic overhead when comparing basic cs to ps post processing. Making the cs equal in performance is a waste, what you really want is to make the cs faster, and you can only do that by leveraging shared memory and a completely different algorithm.

Additionally there's always a large overhead when first transitioning from ps to cs -- I can speculate that the GPU is flushing its 3D pipeline somehow, I'm not really sure why or why this overhead isn't present when switching from compute->3D.

Adam Miles
Adam Miles

I'm out of the country at the moment so only have my laptop's Nvidia Quadro to hand but can make a couple of observations based on running it on this GPU.

The performance delta when using R8G8B8A8_UNORM is around a factor of 2 in Pixel Shader's favour in the simple "fill a texture with some value" test. However on an 8 byte/pixel format (R16G16B16A16_FLOAT) the difference between the two is almost zero, and actually comes out in favour of the Compute Shader by a small margin. While I still advocate use of 32bit HDR formats such as R11G11B10_FLOAT where possible (over the 64bit FP formats), it's actually quite a popular format now that everyone is rendering in HDR and are Physically Based, especially on PC.

I'd have to try running the same test on GCN hardware which is what I'm more familiar with, but I wouldn't expect the same delta on 32bit formats there.

As size of the format increases up to 128bit (R32G32B32A32_FLOAT) the Compute Shader approach puts in times that are more than 2x faster than the pixel shader, suggesting perhaps that Nvidia hardware is hitting 1/4 rate fill-rate in the PS at 128bit formats and 1/2 rate at 64bit (a penalty that Compute bypasses completely).

I'd like to spend some time converting 'ticks' to actual time to figure which of the hardware's stated bottlenecks we might be running up against there, but that'll have to wait for the weekend.

If Nvidia hardware is running up against a bottleneck around how quickly it can export UAV writes then as you point out, it may well be that even a relatively simple (but non trivial) post processing pass will have a different bottleneck that is shared by both methods and result in equivalent times on 32bit formats. Given that the performance of the Compute Shader approach remains constant across 32, 64 and 128 bit formats would at least suggest that on the Kepler archtecture that there is some export limit that clocks in at half the peak fill-rate of the ROPs.

Quadro K1100M

R8G8B8A8 - PS 2x faster

Measuring PS:
0 - 1998240 ticks elapsed

Measuring CS:
0 - 3859648 ticks elapsed

R16G16B16A16_FLOAT - Equivalent performance

Measuring PS:
0 - 3924096 ticks elapsed

Measuring CS:
0 - 3885280 ticks elapsed

R32G32B32A32_FLOAT - Compute Shader 2x faster

Measuring PS:
0 - 8136256 ticks elapsed

Measuring CS:
0 - 3933824 ticks elapsed
Adam Miles - Principal Software Development Engineer - Microsoft Xbox Advanced Technology Group

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.