D3D12 creation logic (with bonus rant on error handling)
// 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