There were two main pieces of cleanup work that I wanted to take care of in my Direct3D12 framework. The first was to break apart the LoadContent functions of the various test programs. The second was to minimize object lifetimes. Previously all the various framework wrapper objects and their internal D3D12 resources would live for nearly the entire program regardless of how long they were actually needed for.
LoadContent
In each test program the LoadContent function was a mess due to them doing more than they were originally intended to do, which was to load the models or other content needed for the program. Since I have been trying to minimize dependencies in these test programs to keep the D3D12 code as clear as possible, I'm not using a model library and am instead filling in the vertex, index, and other buffers directly. On top of that, D3D12 also requires setting up the graphics pipeline, which was also being done in those functions. To clean this up and make the code more understandable at a glance, I've introduced new TestModel and TestGraphicsPipeline classes in each test program (some have an additional pipeline class as needed for the particular test case). These new classes take a lot of the burden off the Game subclass by having them be responsible for managing just a just a model or a graphics pipeline respectively (even if the model is still hard-coded for demonstration purposes). The LoadContent functions now take care of creating and configuring instances of these classes. So, it is still doing graphics pipeline setup as needed by Direct3D12, but it is encapsulated and offloaded to an appropriate class. Where before a typical LoadContent function was a few hundred lines, now a typical LoadContent function now looks like:
void GameMain::LoadContent(){
GraphicsCore& graphics = GetGraphics();
m_pipeline = new TestGraphicsPipeline(graphics);
m_model = new TestModel(graphics, m_pipeline->GetShaderResourceDescHeap(), m_pipeline->GetCommandList()); m_pipeline->SetModel(m_model); // setup the cameras for the viewport
Viewport full_viewport = graphics.GetDefaultViewport();
m_camera_angle = 3 * XM_PI / 2;
m_camera = new Camera(full_viewport.width / full_viewport.height, 0.01f, 100.0f, XMFLOAT4(0, 0, -10, 1), XMFLOAT4(0, 0, 1, 0), XMFLOAT4(0, 1, 0, 0));
m_pipeline->SetCamera(m_camera);
}
For the various fields that were part of the test program Game subclasses, those have been moved to either TestModel or TestGraphicsPipeline as appropriate. One field of note is the descriptor heap. Due to a requirement of ID3D12GraphicsCommandList::SetDescriptorHeaps, where it can use only 1 D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV and only 1 D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER heap for rendering, the descriptor heap needs to be shared between the model and pipeline classes. The models just need it while creating their texture resources, and the pipeline needs it for both creating non-model resources (e.g. a constant buffer to hold the camera matrices) and for binding the descriptor heap when rendering a frame. So, the TestGraphicsPipeline class owns the descriptor heap instance and provides an accessor method for the TestModel to use it. This means a real game using this approach would need to compute the number of unique textures for its models and add in the number of constant buffers the rendering process requires, then either pass that information into the pipeline for creation of the descriptor heap and allow the models access to the same descriptor heap, or move creation of the descriptor heap outside of the pipeline and pass it to both the pipeline and model along with deal with managing its lifetime alongside the pipeline and models.
Minimizing Object Lifetimes
For the fields that were moved to the TestGraphicsPipeline class, there were a few that were being kept for nearly the whole duration of the program that weren't needed for that long. In particular are the various shader instances and the input layout. Those are used to create a framework's Pipeline instance, which internally creates a ID3D12PipelineState. Once that instance is created, there is no need to keep the shaders or input layout around any longer.
For the TestModel classes, they didn't need to keep around the TextureUploadBuffer instances once their content had been copied to the Texture subclass that is actually used for rendering. So, after the TextureUploadBuffer's PrepUpload function has been called for all the textures to upload, the command list executes, and a fence is waited on, then the TextureUploadBuffer should be safe to delete. However, I would occasionally get an exception and message in the debug window of:
D3D12 ERROR: ID3D12Resource::: CORRUPTION: An ID3D12Resource object (0x0000027D7F65D880:'Unnamed Object') is referenced by GPU operations in-flight on Command Queue (0x0000027D7D3BB130:'Unnamed ID3D12CommandQueue Object'). It is not safe to final-release objects that may have GPU operations pending. This can result in application instability. [ EXECUTION ERROR #921: OBJECT_DELETED_WHILE_STILL_IN_USE]
So, this exposed a bug in my fence implementation that had been hiding there all along. While the code for D3D12_Core::WaitOnFence is a conversion from Microsoft's D3D12HelloWord sample, it turns out I had forgot to initialize value passed along to the command queue's Signal function. In debug builds this was leading to the value to start at 0, which also was the fence's initial value. This caused D3D12_Core::WaitOnFence to go into the case of thinking the fence was already complete and allow execution of the program to continue. Sometimes my system would be fast enough that was okay for this data set, other times I'd get this error. Once I initialized the initial signal value to 1, D3D12_Core::WaitOnFence would properly detect whether the fence needed to be waited on. Technically any value greater than the fence's initial value would work.
Miscellaneous
- I also tweaked D3D12_TextureUploadBuffer::PrepUploadInternal to only do state changes in the subresource being uploaded to instead of all subresources in the target texture.
-
When I had initially written the D3D12_ReadbackBuffer, my starting point was copying D3D12_ConstantBuffer. As a result of that the 256 byte alignment required for a constant buffer was also in the readback buffer code for the past couple of times I've uploaded the framework's source to a dev journal/blog. Since that isn't actually a requirement for a readback buffer, it has been removed.