D3D12 creation logic (with bonus rant on error handling)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This function exists solely to make debugging D3D errors easier. | |
// (single point of failure for breakpoints!) | |
static HRESULT d3d_check( HRESULT hr ) | |
{ | |
if ( FAILED( hr ) ) | |
{ | |
hr = hr; // put a breakpoint here | |
} | |
return hr; | |
} | |
// Now we have a bunch of functions like this. | |
// The pertinent detail being that all of these take a HRESULT * | |
// output parameter and just are no-ops returning a NULL pointer | |
// when the HRESULT is already in a failed state. | |
static ID3D12Resource * create_buffer( ID3D12Device *dev, D3D12_HEAP_TYPE heap, UINT64 size, D3D12_RESOURCE_FLAGS flags, D3D12_RESOURCE_STATES states, HRESULT *hr ) | |
{ | |
D3D12_HEAP_PROPERTIES heap_props = { heap }; | |
D3D12_RESOURCE_DESC desc; | |
ID3D12Resource * resource = NULL; | |
init_buffer_desc( &desc, size ); | |
desc.Flags = flags; | |
if ( SUCCEEDED( *hr ) ) | |
*hr = d3d_check( dev->CreateCommittedResource( &heap_props, D3D12_HEAP_FLAG_NONE, &desc, states, NULL, IID_PPV_ARGS( &resource ) ) ); | |
return resource; | |
} | |
static ID3D12Resource * create_tex2d( ID3D12Device *dev, D3D12_HEAP_TYPE heap, U32 width, U32 height, U16 nmips, DXGI_FORMAT format, D3D12_RESOURCE_FLAGS flags, D3D12_RESOURCE_STATES states, HRESULT *hr ) | |
{ | |
D3D12_HEAP_PROPERTIES heap_props = { heap }; | |
D3D12_RESOURCE_DESC desc; | |
ID3D12Resource * resource = NULL; | |
init_tex2d_desc( &desc, width, height, nmips, format ); | |
desc.Flags = flags; | |
if ( SUCCEEDED( *hr ) ) | |
*hr = d3d_check( dev->CreateCommittedResource( &heap_props, D3D12_HEAP_FLAG_NONE, &desc, states, NULL, IID_PPV_ARGS( &resource ) ) ); | |
return resource; | |
} | |
// Convenience function to create buffers in our default setup for UAV usage | |
static ID3D12Resource * create_buffer_for_uav( ID3D12Device * dev, UINT64 size, HRESULT * hr ) | |
{ | |
return create_buffer( dev, D3D12_HEAP_TYPE_DEFAULT, size, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS, | |
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, hr ); | |
} | |
// ... and with helper functions like that, you can structure resource creation | |
// code like this: | |
BINKSHADERSD3D12GPU * Create_Bink_shaders( BINKCREATESHADERSD3D12 * create ) | |
{ | |
UINT i; | |
HRESULT hr = S_OK; | |
if ( create == 0 || create->device == 0 || create->fence == 0 || create->gpu_mode_command_queue == 0 ) | |
return 0; | |
BINKSHADERSD3D12GPU * shaders; | |
shaders = (BINKSHADERSD3D12GPU *)BinkUtilMalloc( sizeof(BINKSHADERSD3D12GPU) ); | |
if ( shaders == 0 ) | |
return 0; | |
// zero the fields in "shaders" | |
// ---- then start creating things: | |
shaders->fence_event = CreateEvent( NULL, 0,0, NULL ); // autoreset event starting non-signaled | |
if ( !shaders->fence_event ) | |
hr = E_FAIL; | |
// Decode fence | |
if ( SUCCEEDED( hr ) ) | |
hr = d3d_check( device->CreateFence( 0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS( &shaders->decode_fence ) ) ); | |
// [...] and create even more things | |
// DC predict root signature | |
{ | |
// PER_PLANE_GRP_CONSTANT | |
static D3D12_DESCRIPTOR_RANGE const range_dc_predict_tab_ppgc[] = { | |
{ D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0, 0, PPGC_DC_OUT_UAV_PRED }, // u0 (dc_out_uav_pred) | |
}; | |
// PER_PLANE_AND_UPLOAD_SLOT | |
static D3D12_DESCRIPTOR_RANGE const range_dc_predict_tab_ppaus[] = { | |
{ D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0, 0, PPAUS_FRAME_CONSTS_CBV }, // b0 (frame consts CBV) | |
}; | |
D3D12_ROOT_PARAMETER root_params[2]; | |
root_params[0] = root_param_table( range_dc_predict_tab_ppgc ); // slot 0 | |
root_params[1] = root_param_table( range_dc_predict_tab_ppaus ); // slot 1 | |
// 2 slot total | |
shaders->root_sig[ ROOT_SIG_DC_PREDICT ] = create_root_sig( device, 2, root_params, 1, static_smps, D3D12_ROOT_SIGNATURE_FLAG_NONE, &hr ); | |
} | |
// [..] so many things to create... | |
// Create draw pipelines | |
{ | |
// Clean out all the stuff from the graphics pipeline state that we don't use | |
D3D12_GRAPHICS_PIPELINE_STATE_DESC gpdesc = create->prototype; | |
// <fill out the fields in gpdesc> | |
// Create the various shader permutations | |
for ( i = 0 ; i < DRAW_STATE_COUNT; ++i ) | |
{ | |
static const struct DrawStateDesc | |
{ | |
CompiledShader const * pshader; | |
BOOL blend; | |
D3D12_BLEND src_blend; | |
D3D12_BLEND dst_blend; | |
} states[ DRAW_STATE_COUNT ] = { | |
#define T(name, pshader, blend, srcBlend, dstBlend) { pshader, blend, srcBlend, dstBlend }, | |
DRAW_STATE_LIST | |
#undef T | |
}; | |
DrawStateDesc const * state = &states[i]; | |
// Pixel shader | |
gpdesc.PS = get_bytecode( state->pshader ); | |
// Blending state | |
// RenderTargetWriteMask uses user-provided setting! | |
gpdesc.BlendState.RenderTarget[0].BlendEnable = state->blend; | |
gpdesc.BlendState.RenderTarget[0].LogicOpEnable = FALSE; | |
gpdesc.BlendState.RenderTarget[0].SrcBlend = state->src_blend; | |
gpdesc.BlendState.RenderTarget[0].DestBlend = state->dst_blend; | |
gpdesc.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD; | |
gpdesc.BlendState.RenderTarget[0].SrcBlendAlpha = state->src_blend; | |
gpdesc.BlendState.RenderTarget[0].DestBlendAlpha = state->dst_blend; | |
gpdesc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD; | |
if ( SUCCEEDED( hr ) ) | |
hr = d3d_check( device->CreateGraphicsPipelineState( &gpdesc, IID_PPV_ARGS( &shaders->draw_state[i] ) ) ); | |
} | |
} | |
// [..] create yet more things (I'm serious, the original code is about 400 lines worth of "Create" calls) | |
// If *any* of this failed, bail. | |
if ( FAILED( hr ) ) | |
{ | |
Free_shaders( shaders ); // this releases all non-null ptrs in "shaders" then frees "shaders" itself. | |
return NULL; | |
} | |
return shaders; | |
} | |
// Bonus rant on error handling: | |
// | |
// In this case there is no fine-grained error reporting, just cleanup, because there is no useful | |
// intermediate stage (as far as the app is concerned) between "most of this succeeded" and | |
// "we failed immediately". For the resulting thing to be useful, _everything_ needs to be | |
// initialized. | |
// | |
// That's an interesting concern API design: if you have something complex like that, it's a good | |
// idea to have some *logging* code for devs that allows them to quickly pinpoint what's going | |
// wrong, but you want to keep *error codes* and their ilk really simple. Having a giant taxonomy | |
// of possible errors just means that nobody is going to handle most of them. | |
// | |
// What you want to do is separate error messages/log messages (which are unstructured text and can | |
// have as much detail as you want) from error codes, of which there should be a small, limited | |
// number (less than 5, ideally). | |
// | |
// Error messages should be written to aid diagnosing/debugging the problem. | |
// Error *codes* should tell the app "what state am I in now?" or "what do I do next?". | |
// | |
// E.g. there's a million ways to fail to connect to some network service, and it's a good idea | |
// to put some detail in error/log messages, but for app-level code, generally all it cares about | |
// is "am I connected to the server or not?". It is reasonable to expect app code handling "I don't | |
// have a connection". But a lot of APIs end up with error code taxonomies like this: | |
// | |
// enum NetworkError { | |
// Success, | |
// AllOK, | |
// NoRouteToHost, | |
// ConnectionRefused, | |
// DNSLookupFailed, | |
// OutOfMemory, | |
// UnsupportedProtocolVersion, | |
// IDontKnowWhyThisHappensButMaybeBethDoes, | |
// HardwareFault, | |
// ServerBusy, | |
// UnknownError | |
// }; | |
// | |
// and then any user of that API is left wondering what the subtle distinction between "success" and | |
// "all OK" means, what it should do on "OutOfMemory" (is the network subsystem even in a state where | |
// you can do a clean teardown when that error occurs, or do you basically have to exit the process | |
// at that point?), whether "HardwareFault" warrants extreme action like an OutOfMemory that leaves | |
// the subsystem in an unspecified state or can be fixed by just shutting down and re-initializing | |
// the connection, or what the hell it is expected to do on UnknownErrors. | |
// | |
// And, of course, with a list like that, it's likely that some new versions of the library will add | |
// 1 or 2 extra error codes, maybe slightly more specific than the previous categorization, and now | |
// app code that used to go through one path will go through a different path that's unhandled | |
// (because the app code for the error handling didn't get updated when the new errors were added). | |
// | |
// So at this point I believe pretty strongly in allocating error codes purely based on "what | |
// should the app do next?" (e.g. in this example: try again later, or close connection/reconnect, | |
// or "this isn't gonna fix itself, bail"), and having separate error message/logging facilities | |
// with more details (for when you write logs or need a message to present to the user). |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment