An article on the beauty of DirectX 12

Keywords: Windows Programming less github

DirectX 12 is Microsoft's latest graphics API library, which is only supported on Win10 system (if it is not Win10, it must be upgraded to Win10). My biggest feeling about this API is that it is particularly complex, because it exposes many underlying methods. Even though I have learned DX9, I still feel confused when facing DX12, not only because there are too many methods exposed in DX12, but also because of the way it encapsulates them. Well, that's all. I'll introduce the DX12 drawing process with my understanding and give a set of executable code.

The drawing process of DX12 is as follows:

First of all, painting requires a painting environment. Drawing environment is an abstraction of hardware environment in the code layer. All drawing is done in this drawing environment. It can be said that the drawing environment is our "hardware". The abstraction of DX12 for drawing environment is ID3D12Device.

Then, draw the natural needs of resources. Resources here include buffers for staging rendering results, including render buffers and depth buffers.

Finally, drawing requires drawing instructions. GPU must know how we want to draw.

The idea is so simple, but how to realize the drawing idea of DX12 is the focus we care about!

 

Drawing environment

The COM technology used by DX12, what is COM technology, is simply understood as a subclass inherited from a unified common base class. The greatest advantage of this technology is that the allocation and release of resources can be done without too much concern. Detailed content readers can baidu.

When creating a Device, we need to do two things. One is to directly create the Device of the default display adapter. The other is to create a Device of the software adapter if the Device of the default display adapter cannot be created. The code implementation is as follows:

/*
 * DirectX 12 
 */
 
ComPtr<IDXGIFactory4> 			g_pDXGIFactory = nullptr; // dxgi factory
ComPtr<ID3D12Device>			g_pDevice = nullptr; // Pointer to Device

D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS	g_msQualityLevels;	// Multisampling support

 void CreateDevice() {
	// Create a hardware based Device
	HRESULT hardwareResult = D3D12CreateDevice(
		nullptr,	// Which display adapter? nullptr represents the default display adapter
		D3D_FEATURE_LEVEL_11_0, // Feature set you want to support
		IID_PPV_ARGS(&g_pDevice));  // Output comptr < id3d12device > object
	
	// If you can't create a hardware based Device, create a
        // Device of WAP (Windows advanced rasterization platform) software adapter
        // DXGI is a basic library provided by Microsoft. It provides the functions related to graphic API but not suitable for drawing API
        // For example, enumerate the display adapter, display, supported display resolution refresh rate and so on in the system.
        // An important interface we use here is IDXGIFactory4. Because of the long-standing relationship, Microsoft has to use this interface
        // Magic number. This interface can enumerate display adapters. Similarly, it is its function to create a switch chain.
	ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&g_pDXGIFactory)));
	if (FAILED(hardwareResult)) {
		ComPtr<IDXGIAdapter> pWarpAdapter; // Abstraction of display adapter
		ThrowIfFailed(g_pDXGIFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));  // Enumerate display adapters
		
                // Create an adapter related Device
		ThrowIfFailed(D3D12CreateDevice(
			pWarpAdapter.Get(), // Get the original pointer of COM
			D3D_FEATURE_LEVEL_11_0,
			IID_PPV_ARGS(&g_pDevice)));  // Get the address of the COM pointer
	}

	// Check quality level of MSAA supporting 4X
	// All Direct3D11 compatible devices support 4X MSAA, so we just need to check the level of support
	g_msQualityLevels.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // Buffer format
	g_msQualityLevels.SampleCount = 4; // Sampling number
	g_msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE; // 
	g_msQualityLevels.NumQualityLevels = 0;
	ThrowIfFailed(g_pDevice->CheckFeatureSupport(
				D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
				&g_msQualityLevels,
				sizeof (g_msQualityLevels)));
}

In order to enumerate all the adapters owned by the machine, we need to use the Microsoft DXGI (DirectX Graphics Infrastructure) library, which contains the functions needed for graphics programming but not suitable for inclusion in DirextX, such as enumerate display adapters, displays, resolution supported by the machine, refresh rate, etc. The variable type of gdxgifactory is comptr < idxgifactory4 > and the creation function name is CreateDXGIFactory1, which is embarrassing. In the process of continuous development, even Microsoft has to use this magic number to distinguish versions.

CheckFeatureSupport is used to check the multisampling level supported by the device. The maximum number of multisampling is 32, which is defined by the d3d11 "Max" multisample "sample" count macro. It is usually set to 4 or 8 for performance reasons. The reason to check this is that you also need this data when creating render and depth buffers.

Create command queues and command lists

Unlike the previous DX, DX12 exposes a lot of the rendering process. Command Queue and Command List are among them. The main purpose of the Command Queue is to make full use of the computing power of CPU and GPU, so that they can be in the computing at the same time, reducing the waiting time. The Command Queue is mainly responsible for receiving the command (list) generated by CPU, and then notifying GPU to execute. The Command List is mainly used to let the CPU generate commands and save them in. When it is saved to a certain extent, it can be put into the Command Queue for GPU to execute (not necessarily after all commands are generated).

ComPtr<ID3D12CommandQueue>	        g_pCommandQueue;	// Pointer to the command queue
ComPtr<ID3D12CommandAllocator>		g_pCommandAllocator;	// Pointer to the command allocator (buffer used to allocate commands)
ComPtr<ID3D12GraphicsCommandList>	g_pCommandList;		// Save the list of commands that will be sent to GPU for execution

void CreateCommandObjects() {
	// To create a command queue, a description structure is needed to define the command queue
	D3D12_COMMAND_QUEUE_DESC queueDesc = {};
	queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
	queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	ThrowIfFailed(g_pDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&g_pCommandQueue)));
	
	// The creation of command list requires a front Allocator, which is mainly for resource reuse
        // Multiple command lists can be associated with the same allocator, but only one command list can operate on one allocator at the same time
	ThrowIfFailed(g_pDevice->CreateCommandAllocator(
		D3D12_COMMAND_LIST_TYPE_DIRECT,
		IID_PPV_ARGS(g_pCommandAllocator.GetAddressOf())));
		
	ThrowIfFailed(g_pDevice->CreateCommandList(
		0,
		D3D12_COMMAND_LIST_TYPE_DIRECT,
		g_pCommandAllocator.Get(),	// Relevant distributor
		nullptr, // Initial pipeline state object
		IID_PPV_ARGS(g_pCommandList.GetAddressOf())));
		
	// Close command list
        // When the command list is created or reset, the status of the command list is Open, but the status of the Open command list is Open
        // Can't be executed, so it's a good habit to set the command list to Close after the operation
	g_pCommandList->Close();
}

Create exchange chain

The exchange chain is a kind of resource, but it is quite special. It is the storage place for the data currently being displayed and the data to be displayed in the next frame. In modern rendering, there are usually two buffers in a switching chain, i.e. the front buffer and the back buffer. The data in the front buffer is the data currently displayed (can be seen), and the data in the back buffer is the data to be displayed in the next frame. When it is necessary to display the data in the back buffer, it is OK to set the back buffer to the "front" buffer, which is very special Smart setting.

The difficulty in creating an exchange chain is to describe what kind of exchange chain to create. Many members of this description structure are very complicated. Please refer to the official [description document]( DXGI_SWAP_CHAIN_DESC ) The process of creating the exchange chain is very simple, with only three steps:

1. Release the existing exchange chain.

2. Fill the description structure to indicate what kind of exchange chain to create.

3. Call the CreateSwapChain function of Factory to create the exchange chain.

See the following code for details:

ComPtr<IDXGISwapChain> 	g_pSwapChain = nullptr;		// Pointer to the exchange chain interface

void CreateSwapChain() {
	// Release the exchange chain, we need to regenerate.
	g_pSwapChain.Reset();
	
        // The exchange chain describes the structure used to define what kind of exchange chain to create.
	DXGI_SWAP_CHAIN_DESC sd;
	sd.BufferDesc.Width = g_ScreenWidth;
	sd.BufferDesc.Height = g_ScreenHeight;
	sd.BufferDesc.RefreshRate.Numerator = 60; // Refresh rate, molecular
	sd.BufferDesc.RefreshRate.Denominator = 1; // Refresh rate, denominator
	sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // DXGI_FORMAT enumeration, used to define the display format, here represents a 32-bit unsigned normalized integer format, each channel has 8 bits, including transparent channels
	sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; // The DXGI? Mode? Scanline? Order enumeration is used to define the scanline drawing format. In this case, the scanline drawing format is not specified.
	sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; // DXGI? Mode? Scaling enumeration to indicate how the image is stretched to fit the display resolution. It is also not specified here.
	sd.SampleDesc.Count = 1; // Multiple samples can only be 1, otherwise they cannot match DXGI? Swap? Effect? Flip? Discard
	sd.SampleDesc.Quality = g_msQualityLevels.NumQualityLevels - 1; // Quality level of sampling
	sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // What does cache do
	sd.BufferCount = g_FrameCount; // Cache quantity
	sd.OutputWindow = g_hWnd; // associated window
	sd.Windowed = true; // Windowing or not
	sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;  // Indicates that it is a flip display model. After IDXGISwapChain1::Present1 is called, the cache data is discarded
	sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // The behavior flag of the exchange chain. Allows the application to toggle the window mode, and the display mode (display resolution) matches the size of the window.
	
	// Exchange chain needs command queue to refresh
	ThrowIfFailed(g_pDXGIFactory->CreateSwapChain(
					g_pCommandQueue.Get(),
					&sd,
					g_pSwapChain.GetAddressOf()));
}

Create resource descriptor

In DX 12, Microsoft abstracts back buffer, depth / template buffer into resources. When we submit a drawing command, we need to bind the resource to the rendering pipeline so that the GPU can get the resource. However, GPU resources are not bound directly, but through a way of reference. This resource descriptor is the tool used to reference resources. We can think of it as a lightweight structure, describing what resources are used for GPU.

Why is there such an extra middle layer? The official explanation is that GPU resources are originally memory blocks, which can be accessed at different stages of the rendering pipeline in this original way. From the perspective of GPU, resources are data in a block of memory, and only descriptors know what this memory is for, whether it is rendering target or deep cache.

For historical reasons, descriptor s and views are usually equivalent, so you often see some functions or variables named view.

ComPtr<ID3D12Fence>	g_pFence;	// fence pointer
uint32_t g_rtvDescriptorSize = 0;
uint32_t g_dsvDescriptorSize = 0;

void CreateDescriptorHeaps() {
	// The function of fence is to control the synchronization between CPU and GPU.
        // When the GPU executes the fence command, the CPU can know to prevent the CPU from operating the resources GPU is using.
	ThrowIfFailed(g_pDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&g_pFence)));
	
	// RTV for Render Target View
        // This shows the equivalence of view and descriptor, that is, they are the same thing.

        // Gets the size of the handle increment for the given descriptor heap type. This value is used to offset the resource handle.
	g_rtvDescriptorSize = g_pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
	// Descriptor heap, which can be simply understood as an array of descriptors.
        // To create a descriptor heap, you need to first explain what the heap is for. We are here to use it as a Render Target View
        D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
	rtvHeapDesc.NumDescriptors = g_FrameCount; // Number of descriptors in the heap
	rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; // Type of descriptor heap (RTV)
	rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; // Flag for descriptor heap (NONE indicates default usage)
	rtvHeapDesc.NodeMask = 0; // Single adapter set to 0
        // Create descriptor heap
	ThrowIfFailed(g_pDevice->CreateDescriptorHeap(
					&rtvHeapDesc,
					IID_PPV_ARGS(g_pRtvHeap.GetAddressOf())));
					
	// DSV for depth stencil view
	D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
	dsvHeapDesc.NumDescriptors = 1; 
	dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; // Type of descriptor heap
	dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; // Descriptor heap flag (or default)
	dsvHeapDesc.NodeMask = 0;
        // Create descriptor heap
	ThrowIfFailed(g_pDevice->CreateDescriptorHeap(
					&dsvHeapDesc,
					IID_PPV_ARGS(g_pDsvHeap.GetAddressOf())));
}

Create buffer

Resource descriptors only describe what resources a block of memory is, but we still need to allocate this memory. This is the real resource. Buffer is a kind of resource. We put the creation buffer in a separate function.

uint32_t g_currentBackBuffer = 0;
ComPtr<ID3D12Resource> g_SwapChainBuffer[g_FrameCount]; // Render buffer, there are 2 render buffers, one before and one after
ComPtr<ID3D12Resource> g_DepthStencilBuffer; // Depth / template buffer

void CreateBuffers() {
	// Release the buffer resources and we will recreate them
	for (int i = 0; i < g_FrameCount; ++i)
		g_SwapChainBuffer[i].Reset();
	g_DepthStencilBuffer.Reset();

	// Reallocate buffer size based on window size
	ThrowIfFailed(g_pSwapChain->ResizeBuffers(
		g_FrameCount, // Number of buffers
		g_ScreenWidth, g_ScreenHeight, // Width and height of window
		DXGI_FORMAT_R8G8B8A8_UNORM, // Buffer format
		DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH)); // Behavior specification of the exchange chain. Allows apps to switch modes through ResizeTarget. When switching from window to full screen
														  // The display mode (display resolution) matches the size of the window.

	g_currentBackBuffer = 0; // Current back buffer remade to 0

	/** Create render target view */
		// In order to bind the post buffer to the output merge phase of the pipeline, we need to create a render target view for the post buffer.
		// Gets the descriptor handle at the start of the heap to use the descriptor heap.
	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(g_pRtvHeap->GetCPUDescriptorHandleForHeapStart());
	for (int i = 0; i < g_FrameCount; ++i) {
		// To get buffer resources and save them to global variables, you need to get them from the exchange chain.
		ThrowIfFailed(g_pSwapChain->GetBuffer(i, IID_PPV_ARGS(&g_SwapChainBuffer[i])));
		// Create a Render Target View for the buffer, which is a resource that needs to be associated with a descriptor
		g_pDevice->CreateRenderTargetView(g_SwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
		// Next resource descriptor entry on heap
		rtvHeapHandle.Offset(1, g_rtvDescriptorSize);
	}

	/** Create depth / template buffers and views */
		// Description of the depth / template buffer, defining the resource format created.
	D3D12_RESOURCE_DESC depthStencilDesc;
	depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; // Buffer dimension (1D texture or 2D texture?)
	depthStencilDesc.Alignment = 0; // alignment
	depthStencilDesc.Width = g_ScreenWidth; // Buffer width
	depthStencilDesc.Height = g_ScreenHeight; // Buffer height
	depthStencilDesc.DepthOrArraySize = 1; // The depth of the texture, in pixels; or the size of the texture array
	depthStencilDesc.MipLevels = 1; // Multi level texture series
	depthStencilDesc.Format = DXGI_FORMAT_R24G8_TYPELESS; // Grain format
	depthStencilDesc.SampleDesc.Count = 1; // Multiple samples
	depthStencilDesc.SampleDesc.Quality = 0; // Multisampling sampling quality
	depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; // Texture layout, do not care for the moment.
	depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; // Other resource flags. Allow depth / template views to be created as assets,
																		  // And the resource is allowed to convert to d3d12 ﹣ resource ﹣ state ﹣ depth ﹣ write or d3d12 ﹣ resource ﹣ state ﹣ depth ﹣ read state

	// Default when clearing cache
	D3D12_CLEAR_VALUE optClear;
	optClear.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; // Cache data format
	optClear.DepthStencil.Depth = 1.0f; // Depth value
	optClear.DepthStencil.Stencil = 0; // Template value
		// Create and submit a resource to a specific heap. This resource property is specified by us.
	ThrowIfFailed(g_pDevice->CreateCommittedResource(
		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), // Properties of a specific heap. This means that the heap is accessed by the GPU.
		D3D12_HEAP_FLAG_NONE, // Heap option, indicating whether the heap contains textures and whether resources are shared by multiple adapters. NONE means no additional functionality is required.
		&depthStencilDesc, // Resource descriptor. Generate this resource.
		D3D12_RESOURCE_STATE_COMMON, // The initial state of the resource. How do you want to use this resource?
											 // The official explanation for this enumeration is that resources must be transitioned to this state if they are to be accessed across graph engine types. But there is no detailed explanation.
											 // It is also pointed out that the texture must be in the COMMON state for the CPU to access. Does this conflict with the first parameter?
		&optClear, // Scavenging value
		IID_PPV_ARGS(g_DepthStencilBuffer.GetAddressOf()))); // COM address of the resource

	// Create a depth / template view, using a format that matches the previously created asset
	D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
	dsvDesc.Flags = D3D12_DSV_FLAG_NONE; // Options for depth / template view, which is represented by the default view
	dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D; // How to get resources? Get as 2D texture
	dsvDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; // Resource data formats, including full type and untype formats. 24 bit depth 8-bit template.
	dsvDesc.Texture2D.MipSlice = 0; // Layer 1 of multi-level texture
		// Create depth / template view, associate resources and descriptors
	g_pDevice->CreateDepthStencilView(g_DepthStencilBuffer.Get(), &dsvDesc, g_pDsvHeap->GetCPUDescriptorHandleForHeapStart());

	// Depth / template buffer resource state converted to depth write
	g_pCommandList->ResourceBarrier(
		1,
		&CD3DX12_RESOURCE_BARRIER::Transition(
			g_DepthStencilBuffer.Get(),
			D3D12_RESOURCE_STATE_COMMON,
			D3D12_RESOURCE_STATE_DEPTH_WRITE));
}

Let the CPU wait until the GPU is drawn. This is a very inefficient operation, but it is necessary for synchronization.

uint32_t g_CurrentFence = 0; // Current fence value

void FlushCommandQueue() {
	// Increase the value of fence to ensure that it is executed.
	g_CurrentFence++;
	
	// Add an instruction to the command queue to set a new fence value.
	// GPU can not reach fence until all instructions are processed.
	ThrowIfFailed(g_pCommandQueue->Signal(g_pFence.Get(), g_CurrentFence));
	
	// Until the GPU reaches the fence point.
	if (g_pFence->GetCompletedValue() < g_CurrentFence) {
                // Create an event
		HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
		
		// Set the event to fire when the fence object reaches the fence point
		ThrowIfFailed(g_pFence->SetEventOnCompletion(g_CurrentFence, eventHandle));
		
		// Wait for the event to be fired
		WaitForSingleObject(eventHandle, INFINITE);
		CloseHandle(eventHandle);
	}
}

Reassign size

When the window size changes, we have to reallocate the buffer size. The original resources must be released, and then resources can be created innovatively. Is there any way to reuse the previously written code? Of course, the previous function of creating buffer can be reused, so the code is much less. At the same time, we need to save the location of the viewport and crop region.

void OnResize() {
	// Buffer size can only be reallocated after Device, switch chain and command allocator are all created
	if (g_pDevice == nullptr || g_pSwapChain == nullptr || g_pCommandAllocator == nullptr) {
		return;
	}
	
	// Make sure that all resources are no longer in use.
	FlushCommandQueue();
        
        ThrowIfFailed(g_pCommandList->Reset(g_pCommandAllocator.Get(), nullptr));

	// Create buffer
	CreateBuffers();
	
	// Execute recreate command
	ThrowIfFailed(g_pCommandList->Close());
	ID3D12CommandList* cmdLists[] = {g_pCommandList.Get()};
	g_pCommandQueue->ExecuteCommandLists(_countof(cmdLists), cmdLists);
	
	// Make sure that the instructions created are complete
	FlushCommandQueue();
	
	// Update viewport changes
	g_ScreenViewport.TopLeftX = 0;
	g_ScreenViewport.TopLeftY = 0;
	g_ScreenViewport.Width = static_cast<float>(g_ScreenWidth);
	g_ScreenViewport.Height = static_cast<float>(g_ScreenHeight);
	g_ScreenViewport.MinDepth = 0.0f;
	g_ScreenViewport.MaxDepth = 1.0f;
	
	g_ScissorRect = {0, 0, static_cast<LONG>(g_ScreenWidth), static_cast<LONG>(g_ScreenHeight)};
}

Draw

Here, we simply fill the whole window with one color. As an entry-level project, this effect is more than enough.

void Draw() {
	// Reuse memory blocks that record commands.
	// The Reset function of Allocator can only be called after the GPU has executed the draw command.
	ThrowIfFailed(g_pCommandAllocator->Reset());
	
	// The command list can be reset after submission, without waiting for the command to finish executing.
	// Reuse the memory block of the command list.
	ThrowIfFailed(g_pCommandList->Reset(g_pCommandAllocator.Get(), nullptr));

	// Switch the resource state of the back buffer to Render Target.
        // The ResourceBarrier function creates a notification driven command to synchronize resource access, which is simply to switch the state of the resource.
	g_pCommandList->ResourceBarrier(1,
                // D3d12 ﹣ resource ﹣ barrier is a resource fence (temporarily Translated) used to represent the operation on resources.
                // Cd3dx12 ﹣ resource ﹣ barrier class is an auxiliary class of d3d12 ﹣ resource ﹣ barrier structure, providing more convenient
                // Use the interface. The function of Transition is the same as its function name, which creates the operation of resource state Transition and its return value
                // It is cd3dx12 "resource" barrier.
		&CD3DX12_RESOURCE_BARRIER::Transition(g_SwapChainBuffer[g_currentBackBuffer].Get(),
			D3D12_RESOURCE_STATE_PRESENT,
			D3D12_RESOURCE_STATE_RENDER_TARGET));
			
	// Set up the viewport and crop region. The viewport and crop region are reset as the command list resets.
        g_pCommandList->RSSetViewports(1, &g_ScreenViewport);
	g_pCommandList->RSSetScissorRects(1, &g_ScissorRect);
	
	// Clear the post cache and deep cache.
	g_pCommandList->ClearRenderTargetView(
                // Similarly, the cd3dx12 CPU controller handle class is also a helper class, which supports the d3d12 CPU controller handle structure
                // To access resources, we must pass d3d12 CPU descriptor handle, which is the handle of resources.
                // Here we pass in the id of the current post buffer and the size of the rendering view descriptor to get the handle of the corresponding resource.
		CD3DX12_CPU_DESCRIPTOR_HANDLE(
			g_pRtvHeap->GetCPUDescriptorHandleForHeapStart(),
			g_currentBackBuffer,
			g_rtvDescriptorSize), 
		Colors::LightSteelBlue, // Steel blue
		0, // Number of empty areas
		nullptr);  // Empty area rectangle array
	g_pCommandList->ClearDepthStencilView(
		g_pDsvHeap->GetCPUDescriptorHandleForHeapStart(), // Handle to depth / template descriptor
		D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, // Clear the flags, we need to clear the depth and template cache
		1.0f, // Depth value
		0, // Template value
		0, // Again, the number of empty areas
		nullptr); // Empty area rectangle array
		
	// Set CPU resource descriptor handle, including render target and depth / template cache
	g_pCommandList->OMSetRenderTargets(1,  // Descriptor number
		&CD3DX12_CPU_DESCRIPTOR_HANDLE(
			g_pRtvHeap->GetCPUDescriptorHandleForHeapStart(),
			g_currentBackBuffer,
			g_rtvDescriptorSize), // Got handle to rendering object descriptor
		true,
		&g_pDsvHeap->GetCPUDescriptorHandleForHeapStart()); // Handle to template descriptor
		
	// Switch the back buffer to the PRESENT state.
	g_pCommandList->ResourceBarrier(
		1,
		&CD3DX12_RESOURCE_BARRIER::Transition(
			g_SwapChainBuffer[g_currentBackBuffer].Get(),
			D3D12_RESOURCE_STATE_RENDER_TARGET,
			D3D12_RESOURCE_STATE_PRESENT));
			
	// After the command is saved, close the command list.
	ThrowIfFailed(g_pCommandList->Close());
	
	// Add a list of commands to the queue and execute.
	ID3D12CommandList* cmdsLists[] = { g_pCommandList.Get()};
	g_pCommandQueue->ExecuteCommandLists(_countof (cmdsLists), cmdsLists);
	
	// Buffer before and after swap
	ThrowIfFailed(g_pSwapChain->Present(0, 0));
	g_currentBackBuffer = (g_currentBackBuffer + 1) % g_FrameCount;
	
	// Force refresh to have GPU execute command. This is not funny, but for a simple example
	// That's enough.
	FlushCommandQueue();
}

The main functions have been completed, and we need to assemble the code. Here I do not provide the code for creating Windows on Windows, because it has nothing to do with DX12. The relevant window creation code can be directly Baidu. There are many examples, so don't worry. To put it bluntly, the process of assembling code is equivalent to a process of sorting out the drawing process of DX12:

void InitDirect3D12() {
	CreateDevice();
	
	CreateCommandObjects();
	
	CreateSwapChain();
	
	CreateDescriptorHeaps();
        
        OnResize();
}

Yes, that's the process. The beginning is Device, the "hardware" of our code. Then there are command objects, including command list and command queue, which are created here because command queue is used when creating exchange chain. The next step is to create the swap chain, followed by the descriptor heap, including rendering and depth, although the depth is only one element. Finally, the size is reallocated, because there is the process of creating buffer, which needs to match the resource descriptor when creating buffer. As for where the drawing is called, it is naturally in the message loop:

int Run() {
    MSG msg = {};

	while(msg.message != WM_QUIT) {
		if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else {
			Draw(); // Draw interface when not handling messages
		}
	}

    return msg.wParam;
}

Please refer to:

FlashPanda/Panda​github.com

 

reference material:

Introduction to 3D Game Programming With DirectX 12

Zhihu article: Chen Wenli: knock the next generation game engine from scratch (15)

Supporting code of Longshu: d3dcoder/d3d12book

 

 

 

 

 

 

Published 82 original articles, won 154 praises and 20000 visitors+
Private letter follow

Posted by javiqq on Sun, 23 Feb 2020 03:47:31 -0800