DirectX 12 few notes and questions

Started by
3 comments, last by Spinningcubes 8 years, 2 months ago

So, I've finally found time to start playing around with DirectX and Direct3D 12 - I've went through documentation, basic samples and even a simple engine sample. Now as I've been OpenCL/OpenGL guy for most of the time, I don't have that much experience with DirectX (last one I've used was version Direct3D 9). If somebody would be kind enough to read through my thoughts and possibly tell me if I'm thinking about it right, or wrong.

I will not go into basic samples, as I've went through them - I'm now trying to add Direct3D 12 renderer into my framework which still has OpenGL support (which I'm trying to replace). First of all, let me approx. state out how my Scene works - I have a graph structure (scene-graph like) holding entities, some of these point to one or more Model class instances (along with pointing to Transformation - which is actually just a 4x4 matrix). A Model class points a set of Mesh instances (plus it holds additional things like bounding box, etc. etc.). A Mesh points to Material and VertexBuffer/IndexBuffer, and also Texture, contains a descriptor for the VertexBuffer (describing how the data are interlanced), and Texture (describing what each texture represents), and ShaderParams - which is a constant buffer (along with pipeline state items). Material points only to a Shader.

Note. I know I could move ShaderParams up to Entity and I most likely will at some point, but I wanted to keep the description simple for start.

So, once I'm actually rendering with a camera - I obtain a set of pointers to Mesh instance that are going to be rendered (of course with matrices). This is all processed outside of actually doing rendering with OpenGL right now. For D3D12 I'm still thinking about structures and how to work with them. Let's jump ahead, so I have this set of pointers (with some additional info for each record). In OpenGL when I don't do any sorting (most basic rendering), I just loop through all these, set pipeline state, attach shader, bind textures, set shader parameters, draw, and continue in loop.

In D3D12 this will be a lot different though:

1.) I am not doing any immediate changes like in OpenGL case - once I start application, I create CommandQueue, which I will fill with CommandLists every frame and let it draw (I can either pre-build CommandLists, or build them each frame - when necessary). Anyways I think I understand the command queue and command list concept - so the problem is not here.

2.) Each of my Mesh instance should contain a PipelineStateObject, which will be attached during the rendering. This will completely describe which shader is attached and part of the ShaderParams that hold info about F.e. Blending.

3.) I don't fully understand RootSignature - as far as I got it, this is a object holding some parameters description I'm sending to shader. So technically for each shader I should have a RootSignature (or respectively have one, but change its content)..

4.) How am I passing uniforms into shader - only through RootSignature? (Assuming I don't want to use constant buffer)

If you got here and have an answer on my curious questions I'd be glad. Thank you!

My current blog on programming, linux and stuff - http://gameprogrammerdiary.blogspot.com

Advertisement

2) If your meshes can be drawn in different ways (e.g. a forward shading pass, and a depth-only pass for shadow mapping), then you'll need multiple PSO's per mesh.

3) The root signature is a description of the inputs to a shader -- much like a typical function signature.

e.g. void foo( float a, void* b) is a function signature that accepts a float argument and a pointer argument. The signature itself doesn't hold any values, it just describes what kind of values can be provided.

If two different shaders have the same inputs (e.g. two textures and one cbuffer), then can have the same root signature.

The equivalent in GL is all the shader reflection logic, where you programatically discover the signature for a shader by querying for the location of different uniforms/etc (or the modern GLSL features of being able to hard-code locations into your shaders).

4) When D3D10 introduced the constant buffer, they completely got rid of uniform variables outside of cbuffers (unlike GL, which kept legacy uniforms as well as newer UBOs)... so long-time D3D developers would be used to just using cbuffers.

However, D3D12 gives you a tiny bit more flexibility now, where you can try to fit a small number of uniforms directly as "root constants"...

The root signature has a maximum size -- this would be similar to if C++ only allowed you to pass 4 parameters to a function. If C++ did have that restriction, we would get around it by passing 4 pointers into the function, which each could point to large structures. So if you can't fit all your uniforms into the "root constants" because your root signature becomes too big, then you should instead put those uniforms into a cbuffer, and put a pointer to the cbuffer into the root arguments (this would be a "root descriptor", or a descriptor within a descriptor table).

To explain a bit more in-depth... AMD GCN hardware only allows you to pass 16 DWORD values (64 bytes) as the arguments to a shader. The root signature describes a structure that can fit into this 64byte area. Inside that area, you can embed uniforms directly ("root constants"), embed texture/buffer views directly ("root descriptors"), or embed a pointer to an array of texture/buffer view ("descriptor tables").

The root signature only describes the structure of that bit of memory, not the contents/values. To set a value within this "root" area, you use functions on the command list, such as SetComputeRootConstantBufferView to embed a cbuffer view descriptor into the root area.

To explain a bit more in-depth... AMD GCN hardware only allows you to pass 16 DWORD values (64 bytes) as the arguments to a shader. The root signature describes a structure that can fit into this 64byte area.


It's maybe worth to add, that the root signature has a limit of 64 DWORDs. So it can easily overflow the 64 byte limit GCN hardware sets for user data registers. Therefore it's a good idea to keep the root signature size small (at least on this hardware). Otherwise parts are spilled in 'slower' memory and a additional indirection is needed for resolve.

It's probably worth bearing in mind that you might not even get all 16 DWORDs if the hardware needs some for another purpose like storing BaseVertexLocation, though this might depend on how the compiler implements the fetch shader. If you do spill by even 1 DWORD then it may have to take a couple of DWORDs back from your 16 to store an address where it can find the spilled DWORDs. So don't necessarily assume that you always get all 16 all the time.

I've yet to see this extra level of indirection making any measurable difference in performance. In terms of numbers of levels of indirection, you can refer to the following table. Avoiding the '3' case is not that difficult (just make sure descriptor tables are early in your root signature), and the "1" and "2" cases for spilling are no worse in terms of indirection count to using a root descriptor or descriptor table when it doesn't spill.

For example, a Root Constant outside the first 16 will spill and require an extra indirection to get at. However, a spilled root constant is no worse than fetching a DWORD from a constant buffer was on DX11, so it's not the end of the world.

j1Y8hob.png

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

I have not gotten as far as you have with DX12 but my notes might help and i also have some links. Check out http://www.gamedevpensieve.com/graphics/graphic-api/directx12.

@spinningcubes | Blog: Spinningcubes.com | Gamedev notes: GameDev Pensieve | Spinningcubes on Youtube

This topic is closed to new replies.

Advertisement