It's been quite some time since I last posted here, despite reading some of the forums regularly. For this post I've gotten my work-in-progress Direct3D12 framework project into a good enough state that I thought I'd share it out (a zip file of the source is attached). The purposes of this project are:
Play around with Direct3D12 to get hands on experience with it.
Build a framework that uses compile time type checking to try to avoid invalid operations.
A natural extension of the previous point is that inclusion of d3d12 headers should be restricted to inside the framework and the consuming application should not include d3d12 headers or have them included indirectly from the framework's public headers.
For the current state of this project, it supports:
All the rendering stages except stream output
Placed resources (see note at the end of this post)
Heap tier 1 hardware (https://msdn.microsoft.com/en-us/library/windows/desktop/dn986743%28v=vs.85%29.aspx)
Supports vertex, index, and instance buffers
Handles the various texture types (1D, 1D array, 2D, 2D array, and 3D)
Allows for multiple viewports
Resizing the window and toggling full screen mode (note: there is a bug at the moment for resizing while in full screen mode)
Debug and release modes for x86 and x64
The compute pipeline is on my to-do list. The texturing support also needs to be improved to support mipmapping and MSAA textures. By not supporting MSAA at the moment, I've been able to cheat when it comes to dealing with alignment of resources in a buffer heap by only considering the size since they all use the same alignment without MSAA.
When I started this project, the first step was to upgrade to Windows 10 since I was still on 7 at that point, as well as updating to Visual Studio 2015. After that a driver updated was still necessary so that I could use Direct3D12 with the existing graphics card. Once I got to working with code, the starting point of this project was my Direct3D11 framework that I had discussed in a previous dev journal entry. With the new Visual Studio project, I was initially confused why it couldn't find d3d12.h for which the answer turned out to be that I needed to update my project settings. By default the a project's "Target Platform Version" (in the "General" section) is "8.1" which is an issue since Windows 8.1 doesn't support Direct3D12. After updating that field to "10.0.10240.0", the compiler was able to find the Direct3D12 headers without further issue. Another stumbling block I hit early on was figuring out that I was on heap tier 1 hardware, meaning that each resource heap could only support 1 type of resource whereas tier 2 allows for all types of resources in the same resource heap.
As I had mentioned, the starting point for my Direct3D12 project was my Direct3D11 framework, which had included only a few test programs since my approach was to update the existing test program until I was doing something incompatible with it and spin off a new one. When going to Direct3D12 I very quickly realized that approach was very problematic. The main test program was trying to use vertex, index, and instance buffers, along with constant buffers for the various cameras, texturing, and multiple viewports. While all of those are perfectly reasonable to do with a graphics API, when trying to start with the basics of initializing the graphics API that additional functionality was encumbering. So, with this project instead of constantly expanding a minimal number of test programs, there are a variety of them. This has the added benefit of basically being unit tests for previously implemented functionality. To keep with this idea of them being unit tests, each one is focused on testing the functionality in a simple way instead of implementing some graphics algorithm. This allowed me to focus on debugging the functionality instead of debugging the higher level algorithm as well. This is distinction is perhaps most clear in test program for the hull and domain shader stages. While most people would probably implement a LOD or terrain smoothing algorithm, I took the approach of rendering a wireframe of a square to visualize how the tessellation was subdividing the square. As the following list shows, the test programs were created in order of expanding functionality and complexity. Screenshots of the test programs are in the album at https://www.gamedev.net/gallery/album/1165-direct3d12-framework/.
Differences from the Direct3D11 Framework
Aside from which graphics API is being used, there are a few important differences from the Direct3D11 framework to the 12 one.
Working with Direct3D12
Since the significant difference between Direct3D11 and 12 is the resource management rather than graphical capabilities, that has pretty much been the main focus of the framework so far. Since the framework is using placed resources, this was a bit more complicated than it needed to be since the aligned size for a resource needed to be determined, a buffer heap created to store all the resources of that type, then a descriptor heap with enough entries for all the resources needed for the frame, and finally using all of that to create the resource and its view. My understanding of committed resources is that finding the aligned size and the buffer heap can be skipped if those are used instead of placed resources. Aside from resource creation, another resource management difference is needing to use a fence to allow for the graphics card to finish processing a command list. I've had to work with fences on non-graphics projects before, and with a little reasoning I found it pretty straight forward to determine where these needed to be added.
And speaking of command lists, I found them to be a very nice change from Direct3D11's contexts. The rendering processes with command lists is basically reset them, tell them which pipeline to use, setup your resources, issue your draw commands, update the render target to a present state, close the command list, and execute it. This keeps the pipeline in a clean and known state since there is no concern over what was bound to the pipeline for the previous frame or render pass.
One key area of difference between Direct3D11 and 12 that I need more practice with is the root signature. These are new with Direct3D12 and describe the stages used and the layout of resources to each stage. Since a program should try to minimize the number of root signatures, they should be defined in a way to cover the variety pipeline configurations the program will use. The key issues I had with them is figuring out when I needed a root signature entry to be a table vs a view, and if I have multiple textures in a shader if I should use 1 entry in a table where the register space covers all the textures or one entry per texture in the table (and in case you're curious, the one table entry per texture approach is correct). One project I've periodically thought about is creating a program that displays a GUI of the graphics pipeline and allows for the shaders to different stages to be specified, and then will output a root signature that is compatible with those settings. However, that is something I haven't started yet.
- host_computed_triangle: A very basic test of rendering a triangle. Instead of communicating a camera's matrices via a constant buffer so the vertices can be transformed in the vertex shader, this test is so simple that the projection coordinates of the triangle's vertices are set when creating the vertex buffer so that the vertex buffer is just a pass-through.
- Index_buffer: Like host_computed_triangle, this test has the projection coordinates set when creating the vertex buffer, but adds using an index buffer to use 2 triangles to make a quad.
- single_2d_texture: A copy of host_computed_triangle modified to use a green texture instead of per-vertex colors
- constant_buffer: A copy of host_computed_triangle modified to use a color specified in a constant buffer instead of the per-vertex color.
- two_textured_instance_cubes: Uses vertex and index buffers to render a cube, which have the same 2D texture applied to the each face. This uses a constant buffer to communicate a camera's matrices to the vertex shader. By adding in an instance buffer, two cubes are displayed instead of just the one specified in the vertex/index buffers. I also added in using the left and right arrow keys to rotate the camera around the cubes. Which after seeing a what should have been an obscured cube on top of the other one, lead to my realization that the previous test programs had depth testing turned off. So, while I originally intended this program to be just a test of instance rendering, it is also the first test of depth stencils.
- geometry_shader_viewports: This is 4 cameras/viewports showing different angles of two_textured_instance_cubes. The left and right arrow keys will only update the upper-left camera. This is bringing the test programs on par with the main Direct3D11 framework test program, though it does rendering to multiple viewports completely differently. The Direct3D11 framework test program would iterate over the different viewports to make them active and issue the draw commands to that particular viewport. Whereas this test program renders to all 4 viewports at once by using the geometry shader to create the additional vertices and send the vertices to the correct viewport.
- hull_and_domain: In order to exercise two more stages of the rendering pipeline that haven't been used yet, this draws a quad in wireframe mode so that the tessellation is visible. I plan to revisit this program in the future to make the tessellation factors dynamic based on user input, but for now hull_and_domain_hs.hlsl needs to be edited, recompiled, and the program restarted to view the changed factors.
- texture_type_tester: Draws a quad to the screen, where pressing spacebar cycles through a 1D, 2D, and 3D textures. The 1D texture is a gradient from red to blue. The 2D texture is the same one used by two_textured_instance_cubes. The 3D texture has 3 slices where the first is all red, the second is all green, and the third is all blue. The uvw coordinates are set such that some of each slice will be visible.
- texture_array_tester: Uses instance rendering to draw 3 quads to the screen, where pressing space cycles through a 1D or 2D texture array. The 1D textures are a gradient from red to blue, where to show the different entries in the array the green component is set based on which entry in the array it is (0 for the first, 50% for the second, and 100% for the last). The 2D textures take the same approach to the green component, but instead of being a gradient it has a red triangle in the upper left half and blue triangle bottom right half.
- render_target_to_texture: Draws to a render target then copies the render target to a texture. Since heap tier 1 in Direct3D12 does not allow a render target texture to be on the same heap as a non-render target texture, the same ID3D12Resource could not be used for a texture and render target with only different views created for the same resource like was done in Direct3D11. Hence this program creating two different ID3D12Resources and doing a copy between them.
- texture_multiple_uploads: The previous test programs uploaded textures from host memory to the graphic's card memory 1 at a time with a fence between each one re-using the same buffer in host memory. This program uploads all the textures at once with only 1 fence by using 1 buffer per texture. Otherwise it's the same as texture_array_tester.
- One of the goals of the Direct3D11 Framework was to be similar to XNA. While the Direct3D12 framework has a nearly identical Game base class, similarity to XNA is not a goal of this project. In particular is the removal of the ContentManager from the framework, which includes loading image formats as textures. There were two motivating factors behind this. The first was file formats aren't really part of a graphics API. The second is having an asset loading library on top of the framework is more extensible and allows for new formats to be added if a particular project requires them instead of bloating the framework with a wide variety of formats. If you look through the source for the test programs, you'll notice I currently don't have an asset loading library and instead create my textures through code, so this thought of an additional library is more for if this framework would be used on a real project. But it's still filling in an in-memory buffer to pass to the framework along with the pixel format, so the only things really missing are dealing with file IO and file formats.
- This framework also has consistent error handling (aside from 1 more class to update). Rather than using the included log library to make note of the problem then return NULL or false, this one uses a FrameworkException class which inherits from std::exception. Improving that exception class is on my to-do list, but in its current state it has moved log library usage out of the framework and makes the application aware of the problem. Due to the simplicity of the test programs, they still call into the log library and bail out of the program, but a real application could potentially take steps to work around the problem (e.g. if you get an exception when trying to create a texture due to not enough space in the resource heap, make another resource heap with enough space).
- Another use of the FrameworkException class is argument validation. The Direct3D11 framework didn't consistently do argument validation and frequently just documented that it was the caller's responsibility to ensure a particular invariant. As part of C++'s pay for what you use philosophy, I made argument validation optional via a define in BuildSettings.h. In the attached zip file, it is turned on because while I try to use compile time type checking to avoid incorrect operations, that can't catch all cases and I found it useful in development of the test programs. For a real project, I would expect it to be turned on in debug mode but off in release mode.
- From a previous dev journal entry, the Direct3D11 framework used public header generation by using ifndefs and a processing program. This was a very messy solution and in retrospect, I'm not really sure why I did it that way, and didn't solve the issue (nor was it intended to) of the application indirectly getting the Direct3D11 header files included from including the framework headers. The Direct3D12 framework keeps the idea of public vs private headers, but doesn't use a processing program. The public headers are the generic interface that doesn't need to include the Direct3D12 headers. These have factory methods to create the actual instances from the private headers. The pimpl idiom could also have been used here instead of inheritance to solve this issue in a reasonable way. The main reason I thought to use factory methods and inheritance here is if the backing graphics API was changed to OpenGL, Metal, or something else then it is a matter of adding new factory methods and private header subclasses (along with argument validation to make sure all the private classes are using the same graphics API).
- For shaders, instead of continuing what I had done in the Direct3D11 framework of changing the name of the entry point function to be reflective of which stage the shader was for, it would have been less updating of file properties to go with Visual Studio's default of "main". Though when editing the file properties, updating the dialog to "all configurations" and "all platforms" does help mitigate this rename issue.
- Multiple test programs to check the functionality instead of implement a graphics algorithm worked quite well, though are visually less impressive.
- Multiple viewports in Direct3D12 seem to be best implemented by using the geometry shader to send the vertices to the correct viewport.
- Compile time type checking isn't enough to avoid invalid operations, hence the VALIDATE_FUNCTION_ARGUMENTS ifdefs.
- When I was first working on the hull_and_domain test program, as a result of copy and pasting a different project as the starting point the index buffer was initially setup for using 2 triangles to make a quad. Since the original code for that test program was modified to use an input topology of a 4 point control patch, that produced an incorrect result and allowed me to realize that I just needed to update the index buffer to be the indices of the 4 vertices in order. While this is obvious in retrospect and didn't take me long to realize, since the online resources I looked at didn't include this detail I figured I'd mention it here.
- Setting the platform to build/run to x64 helps find more spots where argument validation is needed. This is mainly due to my using the stl where the return type of size is a size_t which can vary by platform, whereas the Direct3D12 API is uses fixed sizes such as UINT32 across all platforms.
- After the last Windows Update there were additional messages generated for incorrect states. I'm glad that these messages were added and I took care of resolving them inside the framework. It was things like the initial state of depth stencil and render targets needing to be D3D12_RESOURCE_STATE_DEPTH_WRITE and D3D12_RESOURCE_STATE_PRESENT respectively instead of D3D12_RESOURCE_STATE_GENERIC_READ which I had simply as a copy and paste from the texturing and constant buffer heaps.
- While most examples that I saw online for Direct3D12 used committed resources which are simpler to setup, the framework currently uses place resources. This is due to my wanting to get hands on experience with the interactions between the resource, resource heap, and descriptor heap. However, since the framework currently prevents creating overlapping resources, which is the primary benefit of placed resources, and since Nvidia recommends committed resources over placed, I will likely revise this in the future.