diff options
| author | 3gg <3gg@shellblade.net> | 2025-12-02 16:39:36 -0800 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2025-12-02 16:39:36 -0800 |
| commit | 6c8ae19be66cee247980a48e736a4e05d14de179 (patch) | |
| tree | d860767907bf0cbe17ec66422e11bea700cf56d9 /dxg/src/imm.c | |
| parent | 8f594c8ebd11f0e5f8a0c6369c3fe7383d250cbe (diff) | |
Diffstat (limited to 'dxg/src/imm.c')
| -rw-r--r-- | dxg/src/imm.c | 368 |
1 files changed, 368 insertions, 0 deletions
diff --git a/dxg/src/imm.c b/dxg/src/imm.c new file mode 100644 index 0000000..28baa99 --- /dev/null +++ b/dxg/src/imm.c | |||
| @@ -0,0 +1,368 @@ | |||
| 1 | /* Immediate-mode renderer. | ||
| 2 | |||
| 3 | Geometry is given by client code and buffered in an upload-heap buffer stored | ||
| 4 | in host memory. | ||
| 5 | When the buffer fills up or the client is done, a draw call is issued. The draw | ||
| 6 | call reads directly from the buffer in host memory; there is no intermediate | ||
| 7 | buffer copy. | ||
| 8 | The renderer double-buffers two host-side buffers so that the client can | ||
| 9 | continue specifying more data into a second buffer while the contents of the | ||
| 10 | first buffer are rendered. | ||
| 11 | If the first buffer is still being rendered while the client loops around, then | ||
| 12 | the client must wait before issuing further geometry. | ||
| 13 | Once the render of the first buffer completes, the process starts again, | ||
| 14 | ping-ponging between the two buffers.*/ | ||
| 15 | #include <dxg/imm.h> | ||
| 16 | #include <dxg/dxcommon.h> | ||
| 17 | |||
| 18 | #include <imm_vs.h> // generated | ||
| 19 | #include <imm_ps.h> // generated | ||
| 20 | |||
| 21 | #define WIN32_LEAN_AND_MEAN | ||
| 22 | #include <Windows.h> // OutputDebugStringA | ||
| 23 | |||
| 24 | #include <stdint.h> | ||
| 25 | #include <stdlib.h> | ||
| 26 | |||
| 27 | static ID3D12Resource* create_buffer(ID3D12Device* pDevice, size_t size) { | ||
| 28 | assert(pDevice); | ||
| 29 | const D3D12_HEAP_PROPERTIES props = { | ||
| 30 | .Type = D3D12_HEAP_TYPE_UPLOAD, | ||
| 31 | .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, | ||
| 32 | .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, | ||
| 33 | .CreationNodeMask = 0, | ||
| 34 | .VisibleNodeMask = 0 | ||
| 35 | }; | ||
| 36 | const D3D12_RESOURCE_DESC desc = { | ||
| 37 | .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, | ||
| 38 | .Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT, | ||
| 39 | .Width = size, | ||
| 40 | .Height = 1, | ||
| 41 | .DepthOrArraySize = 1, | ||
| 42 | .MipLevels = 1, | ||
| 43 | .Format = DXGI_FORMAT_UNKNOWN, | ||
| 44 | .SampleDesc = {.Count = 1, .Quality = 0}, | ||
| 45 | .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, | ||
| 46 | .Flags = D3D12_RESOURCE_FLAG_NONE | ||
| 47 | }; | ||
| 48 | ID3D12Resource* pBuffer = NULL; | ||
| 49 | TrapIfFailed(pDevice->lpVtbl->CreateCommittedResource( | ||
| 50 | pDevice, | ||
| 51 | &props, | ||
| 52 | D3D12_HEAP_FLAG_NONE, | ||
| 53 | &desc, | ||
| 54 | D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER, | ||
| 55 | NULL, | ||
| 56 | &IID_ID3D12Resource, | ||
| 57 | &pBuffer)); | ||
| 58 | return pBuffer; | ||
| 59 | } | ||
| 60 | |||
| 61 | typedef struct GraphicsState { | ||
| 62 | D3D12_VIEWPORT viewport; | ||
| 63 | D3D12_CPU_DESCRIPTOR_HANDLE hBackBufferView; | ||
| 64 | D3D12_CPU_DESCRIPTOR_HANDLE hDepthStencilView; | ||
| 65 | } GraphicsState; | ||
| 66 | |||
| 67 | // Set of per-draw resources. The renderer cycles between sets per draw. | ||
| 68 | typedef struct ResourceSet { | ||
| 69 | ID3D12Resource* pVertexBuffer; | ||
| 70 | CommandRecorder cmdRec; | ||
| 71 | } ResourceSet; | ||
| 72 | |||
| 73 | typedef struct DxgImm { | ||
| 74 | ID3D12Device* pDevice; | ||
| 75 | ID3D12CommandQueue* pCmdQueue; | ||
| 76 | ID3D12PipelineState* pPipelineState; | ||
| 77 | ID3D12RootSignature* pRootSignature; | ||
| 78 | GraphicsState graphicsState; | ||
| 79 | ResourceSet resources[2]; | ||
| 80 | int cur; // Index to current resource set. New geometry written here. | ||
| 81 | float* pCurrentBufferData; // Mapped region of current buffer. | ||
| 82 | size_t bufferSizeVerts; // Num verts per buffer. | ||
| 83 | ID3D12Fence* pFence; | ||
| 84 | HANDLE fenceEvent; | ||
| 85 | uint64_t fenceValue; | ||
| 86 | size_t vertsWritten; // Verts written to current buffer. | ||
| 87 | bool wait; // Whether the next draw should wait. | ||
| 88 | } DxgImm; | ||
| 89 | |||
| 90 | static inline size_t vertex_size_bytes() { | ||
| 91 | return 3 * sizeof(float); | ||
| 92 | } | ||
| 93 | |||
| 94 | static inline size_t verts_byte_count(size_t numVerts) { | ||
| 95 | return numVerts * vertex_size_bytes(); | ||
| 96 | } | ||
| 97 | |||
| 98 | static inline size_t dxg_imm_verts_left(const DxgImm* imm) { | ||
| 99 | assert(imm); | ||
| 100 | assert(imm->bufferSizeVerts >= imm->vertsWritten); | ||
| 101 | return imm->bufferSizeVerts - imm->vertsWritten; | ||
| 102 | } | ||
| 103 | |||
| 104 | static void dxg_imm_copy_verts(DxgImm* imm, const float* pVerts, size_t count) { | ||
| 105 | assert(imm); | ||
| 106 | assert(pVerts); | ||
| 107 | assert(count <= dxg_imm_verts_left(imm)); | ||
| 108 | memcpy(&imm->pCurrentBufferData[imm->vertsWritten], pVerts, verts_byte_count(count)); | ||
| 109 | imm->vertsWritten += count; | ||
| 110 | } | ||
| 111 | |||
| 112 | // Set up the current resource set for drawing. | ||
| 113 | static void dxg_imm_set_up_resource_set(DxgImm* imm) { | ||
| 114 | assert(imm); | ||
| 115 | ResourceSet* const pResources = &imm->resources[imm->cur]; | ||
| 116 | TrapIfFailed(pResources->pVertexBuffer->lpVtbl->Map( | ||
| 117 | pResources->pVertexBuffer, 0, NULL, &imm->pCurrentBufferData)); | ||
| 118 | dxg_cmdrec_reset(&pResources->cmdRec); | ||
| 119 | } | ||
| 120 | |||
| 121 | // Move on to the next resource set. | ||
| 122 | static ID3D12Resource* dxg_imm_next_resource_set(DxgImm* imm) { | ||
| 123 | assert(imm); | ||
| 124 | ResourceSet* const pResources = &imm->resources[imm->cur]; | ||
| 125 | // Unmap the current buffer. | ||
| 126 | // TODO: Do we actually need to do this or can we leave it mapped? If the | ||
| 127 | // latter, then we could just map both buffers and let them be. | ||
| 128 | pResources->pVertexBuffer->lpVtbl->Unmap(pResources->pVertexBuffer, 0, NULL); | ||
| 129 | // Move on to the next resource set. | ||
| 130 | imm->cur = (imm->cur + 1) & 1; | ||
| 131 | imm->vertsWritten = 0; | ||
| 132 | // Set up the new resource set. | ||
| 133 | dxg_imm_set_up_resource_set(imm); | ||
| 134 | } | ||
| 135 | |||
| 136 | // Wait for the current buffer to be available for writing. | ||
| 137 | static void dxg_imm_wait(DxgImm* imm) { | ||
| 138 | assert(imm); | ||
| 139 | assert(imm->wait); | ||
| 140 | // We only need to wait upon the first round around both buffers. | ||
| 141 | // First Signal is on fence value 1, 0 is not actually Signaled. | ||
| 142 | if (imm->fenceValue > 2) { // TODO: Do we need this check? | ||
| 143 | // The last buffer (not current) was Signaled with fenceValue - 1. | ||
| 144 | // The current buffer was therefore Signaled two fence values ago, or | ||
| 145 | // fenceValue - 2. | ||
| 146 | dxg_wait(imm->pFence, imm->fenceEvent, imm->fenceValue - 2); | ||
| 147 | } | ||
| 148 | imm->wait = false; | ||
| 149 | } | ||
| 150 | |||
| 151 | // Draw the current buffer. | ||
| 152 | static void dxg_imm_draw(DxgImm* imm) { | ||
| 153 | assert(imm); | ||
| 154 | ResourceSet* const pResourceSet = &imm->resources[imm->cur]; | ||
| 155 | ID3D12Resource* const pCurrentBuffer = pResourceSet->pVertexBuffer; | ||
| 156 | ID3D12GraphicsCommandList* const pCmdList = pResourceSet->cmdRec.pCmdList; | ||
| 157 | const D3D12_VIEWPORT* const pViewport = &imm->graphicsState.viewport; | ||
| 158 | const D3D12_RECT scissor = { | ||
| 159 | .bottom = pViewport->Height, | ||
| 160 | .left = 0, | ||
| 161 | .right = pViewport->Width, | ||
| 162 | .top = 0, | ||
| 163 | }; | ||
| 164 | const D3D12_VERTEX_BUFFER_VIEW vertexBufferView = { | ||
| 165 | .BufferLocation = pCurrentBuffer->lpVtbl->GetGPUVirtualAddress(pCurrentBuffer), | ||
| 166 | .SizeInBytes = verts_byte_count(imm->vertsWritten), | ||
| 167 | .StrideInBytes = vertex_size_bytes(), | ||
| 168 | }; | ||
| 169 | pCmdList->lpVtbl->RSSetViewports(pCmdList, 1, pViewport); | ||
| 170 | pCmdList->lpVtbl->RSSetScissorRects(pCmdList, 1, &scissor); | ||
| 171 | pCmdList->lpVtbl->OMSetRenderTargets( | ||
| 172 | pCmdList, 1, &imm->graphicsState.hBackBufferView, false, &imm->graphicsState.hDepthStencilView); | ||
| 173 | pCmdList->lpVtbl->SetPipelineState(pCmdList, imm->pPipelineState); | ||
| 174 | pCmdList->lpVtbl->SetGraphicsRootSignature(pCmdList, imm->pRootSignature); | ||
| 175 | pCmdList->lpVtbl->IASetPrimitiveTopology(pCmdList, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); | ||
| 176 | pCmdList->lpVtbl->IASetVertexBuffers(pCmdList, 0, 1, &vertexBufferView); | ||
| 177 | pCmdList->lpVtbl->DrawInstanced(pCmdList, imm->vertsWritten, 1, 0, 0); | ||
| 178 | pCmdList->lpVtbl->Close(pCmdList); | ||
| 179 | ID3D12CommandList* const cmdLists[] = {(ID3D12CommandList*)pCmdList}; | ||
| 180 | ID3D12CommandQueue* const pCmdQueue = imm->pCmdQueue; | ||
| 181 | pCmdQueue->lpVtbl->ExecuteCommandLists(pCmdQueue, 1, cmdLists); | ||
| 182 | } | ||
| 183 | |||
| 184 | DxgImm* dxg_imm_init(ID3D12Device* pDevice, ID3D12CommandQueue* pCmdQueue, DXGI_FORMAT swapChainRtvFormat, DXGI_SAMPLE_DESC swapChainSampleDesc, size_t bufferSizeVerts) { | ||
| 185 | assert(pDevice); | ||
| 186 | assert(pCmdQueue); | ||
| 187 | |||
| 188 | DxgImm* imm = calloc(1, sizeof(DxgImm)); | ||
| 189 | if (!imm) { | ||
| 190 | return 0; | ||
| 191 | } | ||
| 192 | |||
| 193 | imm->pDevice = pDevice; | ||
| 194 | imm->pCmdQueue = pCmdQueue; | ||
| 195 | imm->bufferSizeVerts = bufferSizeVerts; | ||
| 196 | imm->fenceValue = 0; | ||
| 197 | |||
| 198 | // TODO: Move this to the application side. | ||
| 199 | const D3D_SHADER_MODEL model = D3D_SHADER_MODEL_6_5; | ||
| 200 | D3D12_FEATURE_DATA_SHADER_MODEL shaderModel = { model }; | ||
| 201 | HRESULT result = pDevice->lpVtbl->CheckFeatureSupport( | ||
| 202 | pDevice, D3D12_FEATURE_SHADER_MODEL, &shaderModel, sizeof(shaderModel)); | ||
| 203 | if (FAILED(result) || (shaderModel.HighestShaderModel < model)) { | ||
| 204 | DEBUG_PRINT("ERROR: Shader Model 6.5 is not supported!\n"); | ||
| 205 | TrapIfFailed(result); | ||
| 206 | } | ||
| 207 | |||
| 208 | const D3D12_SHADER_BYTECODE vs_bytecode = { | ||
| 209 | .pShaderBytecode = imm_vs, | ||
| 210 | .BytecodeLength = sizeof(imm_vs) | ||
| 211 | }; | ||
| 212 | |||
| 213 | const D3D12_SHADER_BYTECODE ps_bytecode = { | ||
| 214 | .pShaderBytecode = imm_ps, | ||
| 215 | .BytecodeLength = sizeof(imm_ps) | ||
| 216 | }; | ||
| 217 | |||
| 218 | // TODO: Find out how many root parameters to use. | ||
| 219 | // Let's do bindless rendering to keep things flexible. | ||
| 220 | const D3D12_ROOT_SIGNATURE_DESC rootsig_desc = { | ||
| 221 | .NumParameters = 0, | ||
| 222 | .pParameters = NULL, | ||
| 223 | .NumStaticSamplers = 0, | ||
| 224 | .pStaticSamplers = NULL, | ||
| 225 | .Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT | ||
| 226 | }; | ||
| 227 | |||
| 228 | ID3DBlob* pRootSignature = NULL; | ||
| 229 | ID3DBlob* pErrors = NULL; | ||
| 230 | result = D3D12SerializeRootSignature( | ||
| 231 | &rootsig_desc, | ||
| 232 | D3D_ROOT_SIGNATURE_VERSION_1, | ||
| 233 | &pRootSignature, | ||
| 234 | &pErrors); | ||
| 235 | if (FAILED(result)) { | ||
| 236 | if (pErrors) { | ||
| 237 | DEBUG_PRINT(pErrors->lpVtbl->GetBufferPointer(pErrors)); | ||
| 238 | } | ||
| 239 | TrapIfFailed(result); | ||
| 240 | } | ||
| 241 | |||
| 242 | TrapIfFailed(imm->pDevice->lpVtbl->CreateRootSignature( | ||
| 243 | imm->pDevice, | ||
| 244 | 0, | ||
| 245 | pRootSignature->lpVtbl->GetBufferPointer(pRootSignature), | ||
| 246 | pRootSignature->lpVtbl->GetBufferSize(pRootSignature), | ||
| 247 | &IID_ID3D12RootSignature, | ||
| 248 | &imm->pRootSignature)); | ||
| 249 | |||
| 250 | const D3D12_INPUT_ELEMENT_DESC input_layout[] = { | ||
| 251 | { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 } | ||
| 252 | }; | ||
| 253 | const D3D12_INPUT_LAYOUT_DESC input_layout_desc = { | ||
| 254 | .pInputElementDescs = input_layout, | ||
| 255 | .NumElements = COUNTOF(input_layout) | ||
| 256 | }; | ||
| 257 | |||
| 258 | const D3D12_GRAPHICS_PIPELINE_STATE_DESC gpso = { | ||
| 259 | .pRootSignature = imm->pRootSignature, | ||
| 260 | .VS = vs_bytecode, | ||
| 261 | .PS = ps_bytecode, | ||
| 262 | .BlendState = CD3DX12_BLEND_DESC_DEFAULT(), | ||
| 263 | .SampleMask = PointSampling, | ||
| 264 | .RasterizerState = CD3DX12_RASTERIZER_DESC_DEFAULT(), | ||
| 265 | .InputLayout = input_layout_desc, | ||
| 266 | .PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, | ||
| 267 | .NumRenderTargets = 1, | ||
| 268 | .RTVFormats = {swapChainRtvFormat}, | ||
| 269 | .SampleDesc = swapChainSampleDesc | ||
| 270 | }; | ||
| 271 | TrapIfFailed(imm->pDevice->lpVtbl->CreateGraphicsPipelineState( | ||
| 272 | imm->pDevice, &gpso, &IID_ID3D12PipelineState, &imm->pPipelineState)); | ||
| 273 | |||
| 274 | const size_t bufferSize = verts_byte_count(bufferSizeVerts); | ||
| 275 | for (int i = 0; i < 2; ++i) { | ||
| 276 | imm->resources[i].pVertexBuffer = create_buffer(pDevice, bufferSize); | ||
| 277 | if (!imm->resources[i].pVertexBuffer) { | ||
| 278 | dxg_imm_destroy(&imm); | ||
| 279 | } | ||
| 280 | TrapIfFailed(dxg_cmdrec_init(&imm->resources[i].cmdRec, pDevice)); | ||
| 281 | } | ||
| 282 | imm->cur = 0; | ||
| 283 | dxg_imm_set_up_resource_set(imm); | ||
| 284 | |||
| 285 | TrapIfFailed(pDevice->lpVtbl->CreateFence( | ||
| 286 | pDevice, imm->fenceValue, D3D12_FENCE_FLAG_NONE, &IID_ID3D12Fence, &imm->pFence)); | ||
| 287 | |||
| 288 | if ((imm->fenceEvent = CreateEvent(NULL, FALSE, FALSE, NULL)) == NULL) { | ||
| 289 | TrapIfFailed(HRESULT_FROM_WIN32(GetLastError())); | ||
| 290 | } | ||
| 291 | |||
| 292 | return imm; | ||
| 293 | } | ||
| 294 | |||
| 295 | void dxg_imm_destroy(DxgImm** ppImm) { | ||
| 296 | assert(ppImm); | ||
| 297 | DxgImm* imm = *ppImm; | ||
| 298 | if (imm) { | ||
| 299 | for (int i = 0; i < 2; ++i) { | ||
| 300 | SafeRelease(imm->resources[i].pVertexBuffer); | ||
| 301 | dxg_cmdrec_destroy(&imm->resources[i].cmdRec); | ||
| 302 | } | ||
| 303 | SafeRelease(imm->pRootSignature); | ||
| 304 | SafeRelease(imm->pPipelineState); | ||
| 305 | SafeRelease(imm->pFence); | ||
| 306 | if (imm->fenceEvent != NULL) { | ||
| 307 | CloseHandle(imm->fenceEvent); | ||
| 308 | } | ||
| 309 | free(imm); | ||
| 310 | *ppImm = 0; | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | void dxg_imm_set_graphics_state( | ||
| 315 | DxgImm* imm, | ||
| 316 | const D3D12_VIEWPORT* pViewport, | ||
| 317 | D3D12_CPU_DESCRIPTOR_HANDLE hBackBufferView, | ||
| 318 | D3D12_CPU_DESCRIPTOR_HANDLE hDepthStencilView) { | ||
| 319 | assert(imm); | ||
| 320 | assert(pViewport); | ||
| 321 | assert(hBackBufferView.ptr); | ||
| 322 | assert(hDepthStencilView.ptr); | ||
| 323 | imm->graphicsState = (GraphicsState) { | ||
| 324 | .viewport = *pViewport, | ||
| 325 | .hBackBufferView = hBackBufferView, | ||
| 326 | .hDepthStencilView = hDepthStencilView, | ||
| 327 | }; | ||
| 328 | } | ||
| 329 | |||
| 330 | void dxg_imm_flush(DxgImm* imm) { | ||
| 331 | assert(imm); | ||
| 332 | if (imm->vertsWritten > 0) { | ||
| 333 | dxg_imm_draw(imm); | ||
| 334 | // Signal the fence so that the current buffer can be reused once the | ||
| 335 | // draw has finished. | ||
| 336 | ID3D12CommandQueue* pCmdQueue = imm->pCmdQueue; | ||
| 337 | imm->fenceValue++; | ||
| 338 | pCmdQueue->lpVtbl->Signal(pCmdQueue, imm->pFence, imm->fenceValue); | ||
| 339 | // Next draw should Wait for the next buffer. Wait lazily on the next | ||
| 340 | // draw to avoid a stall here. | ||
| 341 | imm->wait = true; | ||
| 342 | dxg_imm_next_resource_set(imm); | ||
| 343 | } | ||
| 344 | } | ||
| 345 | |||
| 346 | void dxg_imm_draw_triangles(DxgImm* imm, const float* pVerts, size_t numTris) { | ||
| 347 | assert(imm); | ||
| 348 | assert(pVerts); | ||
| 349 | // TODO: This could be a loop to handle the case where the max buffer | ||
| 350 | // capacity cannot hold numTris. Or maybe we should rely on the caller | ||
| 351 | // to specify a big enough capacity, but that makes the API less | ||
| 352 | // friendly. | ||
| 353 | size_t triCapacity = dxg_imm_verts_left(imm) / 3; | ||
| 354 | if (triCapacity == 0) { | ||
| 355 | dxg_imm_flush(imm); | ||
| 356 | } | ||
| 357 | // If we just flushed the previous buffer, then we have to wait on the next | ||
| 358 | // one. The wait is done here, and not inside the branch above, because the | ||
| 359 | // client code can also flush the buffer. | ||
| 360 | if (imm->wait) { | ||
| 361 | dxg_imm_wait(imm); | ||
| 362 | } | ||
| 363 | // Re-evaluate capacity. It must be >0 now. | ||
| 364 | triCapacity = dxg_imm_verts_left(imm) / 3; | ||
| 365 | assert(triCapacity > 0); | ||
| 366 | const size_t numVerts = MIN(triCapacity, numTris) * 3; | ||
| 367 | dxg_imm_copy_verts(imm, pVerts, numVerts); | ||
| 368 | } | ||
