/* * Copyright 2011-2026 Branimir Karadzic. All rights reserved. * License: https://github.com/bkaradzic/bgfx/blob/master/LICENSE */ #include "bgfx_p.h" #if BGFX_CONFIG_RENDERER_DIRECT3D12 # include "renderer_d3d12.h" # include "video_d3d12.h" # if BGFX_CONFIG_VIDEO_DIRECT3D12 # include "video.h" # include namespace bgfx { namespace d3d12 { struct RendererContextD3D12; void initVideoDecoder(const VideoBindingD3D12& _binding); ID3D12PipelineState* videoGetPipelineState(RendererContextD3D12* _renderer, ProgramHandle _handle); ScratchBufferD3D12& videoGetScratchBuffer(RendererContextD3D12* _renderer); D3D12_GPU_DESCRIPTOR_HANDLE videoGetSamplerHandle(RendererContextD3D12* _renderer, const uint32_t* _samplerFlags); ID3D12GraphicsCommandList* videoGetCommandList(RendererContextD3D12* _renderer); void videoReleaseResource(RendererContextD3D12* _renderer, ID3D12Resource* _resource); static VideoBindingD3D12 s_videoD3D12; extern const GUID IID_ID3D12CommandAllocator; extern const GUID IID_ID3D12CommandQueue; extern const GUID IID_ID3D12Fence; extern const GUID IID_ID3D12GraphicsCommandList; extern const GUID IID_ID3D12Resource; static const GUID IID_ID3D12VideoDecodeCommandList = { 0x3b60536e, 0xad29, 0x4e64, { 0xa2, 0x69, 0xf8, 0x53, 0x83, 0x7e, 0x5e, 0x53 } }; static const GUID IID_ID3D12VideoDecoder = { 0xc59b6bdc, 0x7720, 0x4074, { 0xa1, 0x36, 0x17, 0xa1, 0x56, 0x03, 0x74, 0x70 } }; static const GUID IID_ID3D12VideoDecoderHeap = { 0x0946b7c9, 0xebf6, 0x4047, { 0xbb, 0x73, 0x86, 0x83, 0xe2, 0x7d, 0xbb, 0x1f } }; static const GUID IID_ID3D12VideoDevice = { 0x1f052807, 0x0b46, 0x4acc, { 0x8a, 0x89, 0x36, 0x4f, 0x79, 0x37, 0x18, 0xa4 } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_AV1_12BIT_PROFILE2 = { 0x17127009, 0xa00f, 0x4ce1, { 0x99, 0x4e, 0xbf, 0x40, 0x81, 0xf6, 0xf3, 0xf0 } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_AV1_12BIT_PROFILE2_420 = { 0x2d80bed6, 0x9cac, 0x4835, { 0x9e, 0x91, 0x32, 0x7b, 0xbc, 0x4f, 0x9e, 0xe8 } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE0 = { 0xb8be4ccb, 0xcf53, 0x46ba, { 0x8d, 0x59, 0xd6, 0xb8, 0xa6, 0xda, 0x5d, 0x2a } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE1 = { 0x6936ff0f, 0x45b1, 0x4163, { 0x9c, 0xc1, 0x64, 0x6e, 0xf6, 0x94, 0x61, 0x08 } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE2 = { 0x0c5f2aa1, 0xe541, 0x4089, { 0xbb, 0x7b, 0x98, 0x11, 0x0a, 0x19, 0xd7, 0xc8 } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_H264 = { 0x1b81be68, 0xa0c7, 0x11d3, { 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5 } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN = { 0x5b11d51b, 0x2f4c, 0x4452, { 0xbc, 0xc3, 0x09, 0xf2, 0xa1, 0x16, 0x0c, 0xc0 } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN10 = { 0x107af0e0, 0xef1a, 0x4d19, { 0xab, 0xa8, 0x67, 0xa1, 0x63, 0x07, 0x3d, 0x13 } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN10_422 = { 0x0bac4fe5, 0x1532, 0x4429, { 0xa8, 0x54, 0xf8, 0x4d, 0xe0, 0x49, 0x53, 0xdb } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN10_444 = { 0x0dabeffa, 0x4458, 0x4602, { 0xbc, 0x03, 0x07, 0x95, 0x65, 0x9d, 0x61, 0x7c } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN12 = { 0x1a72925f, 0x0c2c, 0x4f15, { 0x96, 0xfb, 0xb1, 0x7d, 0x14, 0x73, 0x60, 0x3f } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN12_422 = { 0x55bcac81, 0xf311, 0x4093, { 0xa7, 0xd0, 0x1c, 0xbc, 0x0b, 0x84, 0x9b, 0xee } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN12_444 = { 0x9798634d, 0xfe9d, 0x48e5, { 0xb4, 0xda, 0xdb, 0xec, 0x45, 0xb3, 0xdf, 0x01 } }; static const GUID D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN_444 = { 0x4008018f, 0xf537, 0x4b36, { 0x98, 0xcf, 0x61, 0xaf, 0x8a, 0x2c, 0x1a, 0x33 } }; struct VideoDecoderD3D12 { static constexpr uint32_t kNumDpbSlots = 17; static constexpr uint32_t kBitstreamRingFrames = 8; static constexpr uint32_t kCopyRingSize = 16; struct ReorderedPicture { ID3D12Resource* texture; int32_t displayOrder; int64_t ptsUs; }; struct DpbState { D3D12_RESOURCE_STATES state; int32_t poc; int32_t frameNum; }; VideoDecoderD3D12() : m_renderer(NULL) , m_videoDevice(NULL) , m_decoder(NULL) , m_decoderHeap(NULL) , m_videoQueue(NULL) , m_videoFence(NULL) , m_videoFenceValue(0) , m_videoControl(BX_COUNTOF(m_videoCmdAllocator) ) , m_copyControl(BX_COUNTOF(m_copyCmdAllocator) ) , m_bitstreamBuffer(NULL) , m_bitstreamMapped(NULL) , m_bitstreamCapacity(0) , m_bitstreamCursor(0) , m_dummyCbv(NULL) , m_dpb(NULL) , m_dpbOutput(NULL) , m_referenceOnly(false) , m_codedWidth(0) , m_codedHeight(0) , m_dstWidth(0) , m_dstHeight(0) , m_numDpbSlots(kNumDpbSlots) , m_nextSlot(0) , m_currentSlot(0) , m_numActiveRefs(0) , m_displayOrderNext(0) , m_prevPocLsb(0) , m_prevPocMsb(0) , m_displayedSlot(-1) , m_prevDisplayedSlot(-1) , m_statusReportFeedback(0) , m_initFlags(0) , m_cachedAuBytes(0) { bx::memSet(m_dpbState, 0, sizeof(m_dpbState) ); bx::memSet(m_referenceUsage, 0, sizeof(m_referenceUsage) ); bx::memSet(&m_spsActive, 0, sizeof(m_spsActive) ); for (uint32_t ii = 0; ii < kBitstreamRingFrames; ++ii) { m_videoCmdAllocator[ii] = NULL; m_videoCmdAllocatorFence[ii] = 0; } m_videoCmd = NULL; for (uint32_t ii = 0; ii < kCopyRingSize; ++ii) { m_copyCmdAllocator[ii] = NULL; m_copyFenceRing[ii] = 0; } m_copyCmdList = NULL; m_copyFence = NULL; m_copyFenceValue = 0; bx::memSet(m_slotLastCopyFence, 0, sizeof(m_slotLastCopyFence) ); m_lastDpbOutputCopyFence = 0; for (uint32_t ii = 0; ii < kVideoMaxReorderInFlight; ++ii) { m_reorderPool[ii].texture = NULL; m_reorderPool[ii].displayOrder = -1; } } bool create(const VideoDecoderInit& _init, RendererContextD3D12* _renderer, uint16_t _width, uint16_t _height) { m_renderer = _renderer; m_initFlags = _init.flags; m_cachedAuBytes = (0 != _init.cachedAuBytes) ? _init.cachedAuBytes : (4u << 20); m_auQueue.configure(m_cachedAuBytes, 0 != (m_initFlags & BGFX_VIDEO_DECODER_INIT_RETAIN) ); if (VideoCodec::H264 != _init.codec) { BX_TRACE("VideoDecoderD3D12: codec %d not yet supported on D3D12.", _init.codec); return false; } if (NULL == _init.parameterSets || 0 == _init.parameterSetsSize) { BX_TRACE("VideoDecoderD3D12: empty parameter sets."); return false; } h264::PPS ppsActive = {}; if (!bgfx::parseParameterSets(m_spsArray, BX_COUNTOF(m_spsArray), m_ppsArray, BX_COUNTOF(m_ppsArray), m_spsActive, ppsActive, _init.parameterSets, _init.parameterSetsSize) ) { BX_TRACE("VideoDecoderD3D12: failed to parse SPS/PPS from parameter sets."); return false; } if (0 != m_spsActive.bit_depth_luma_minus8 || 0 != m_spsActive.bit_depth_chroma_minus8 || 1 != m_spsActive.chroma_format_idc) { BX_TRACE("VideoDecoderD3D12: only 8-bit 4:2:0 (NV12) is supported."); return false; } HRESULT hr = s_videoD3D12.device->QueryInterface(IID_ID3D12VideoDevice, (void**)&m_videoDevice); if (FAILED(hr) || NULL == m_videoDevice) { BX_TRACE("VideoDecoderD3D12: ID3D12VideoDevice not available."); return false; } m_dstWidth = _width; m_dstHeight = _height; m_codedWidth = (m_spsActive.pic_width_in_mbs_minus1 + 1) * 16; m_codedHeight = (m_spsActive.pic_height_in_map_units_minus1 + 1) * 16 * (m_spsActive.frame_mbs_only_flag ? 1 : 2); m_numDpbSlots = bx::clamp(uint32_t(m_spsActive.num_ref_frames) + 1, 2, kNumDpbSlots); const DXGI_FORMAT decodeFormat = DXGI_FORMAT_NV12; D3D12_VIDEO_DECODER_DESC decoderDesc { .NodeMask = 0, .Configuration = { .DecodeProfile = D3D12_VIDEO_DECODE_PROFILE_H264, .BitstreamEncryption = {}, .InterlaceType = D3D12_VIDEO_FRAME_CODED_INTERLACE_TYPE_NONE, }, }; D3D12_FEATURE_DATA_VIDEO_DECODE_SUPPORT decodeSupport { .NodeIndex = 0, .Configuration = decoderDesc.Configuration, .Width = m_codedWidth, .Height = m_codedHeight, .DecodeFormat = decodeFormat, .FrameRate = { 0, 1 }, .BitRate = 0, .SupportFlags = D3D12_VIDEO_DECODE_SUPPORT_FLAG_NONE, .ConfigurationFlags = D3D12_VIDEO_DECODE_CONFIGURATION_FLAG_NONE, .DecodeTier = D3D12_VIDEO_DECODE_TIER_NOT_SUPPORTED, }; hr = m_videoDevice->CheckFeatureSupport(D3D12_FEATURE_VIDEO_DECODE_SUPPORT, &decodeSupport, sizeof(decodeSupport) ); if (FAILED(hr) || 0 == (decodeSupport.SupportFlags & D3D12_VIDEO_DECODE_SUPPORT_FLAG_SUPPORTED) || decodeSupport.DecodeTier < D3D12_VIDEO_DECODE_TIER_1) { BX_TRACE("VideoDecoderD3D12: H.264 NV12 not supported by hardware (tier=%d, hr=0x%08x).", decodeSupport.DecodeTier, hr); DX_RELEASE_I(m_videoDevice); return false; } m_referenceOnly = 0 != (decodeSupport.ConfigurationFlags & D3D12_VIDEO_DECODE_CONFIGURATION_FLAG_REFERENCE_ONLY_ALLOCATIONS_REQUIRED); const uint16_t vendorId = s_videoD3D12.vendorId; if (BGFX_PCI_ID_INTEL == vendorId) { if (!m_referenceOnly) { BX_TRACE("VideoDecoderD3D12: Intel detected (vendorId=0x%04x): forcing reference-only DPB.", vendorId); } m_referenceOnly = true; } uint32_t heapWidth = m_codedWidth; uint32_t heapHeight = m_codedHeight; if (0 != (decodeSupport.ConfigurationFlags & D3D12_VIDEO_DECODE_CONFIGURATION_FLAG_HEIGHT_ALIGNMENT_MULTIPLE_32_REQUIRED) ) { heapWidth = bx::strideAlign(heapWidth, 32); heapHeight = bx::strideAlign(heapHeight, 32); } D3D12_VIDEO_DECODER_HEAP_DESC heapDesc { .NodeMask = 0, .Configuration = decoderDesc.Configuration, .DecodeWidth = heapWidth, .DecodeHeight = heapHeight, .Format = decodeFormat, .FrameRate = { 0, 1 }, .BitRate = 0, .MaxDecodePictureBufferCount = m_numDpbSlots, }; hr = m_videoDevice->CreateVideoDecoderHeap(&heapDesc, IID_ID3D12VideoDecoderHeap, (void**)&m_decoderHeap); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: CreateVideoDecoderHeap failed (0x%08x).", hr); destroy(); return false; } hr = m_videoDevice->CreateVideoDecoder(&decoderDesc, IID_ID3D12VideoDecoder, (void**)&m_decoder); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: CreateVideoDecoder failed (0x%08x).", hr); destroy(); return false; } D3D12_COMMAND_QUEUE_DESC queueDesc { .Type = D3D12_COMMAND_LIST_TYPE_VIDEO_DECODE, .Priority = 0, .Flags = D3D12_COMMAND_QUEUE_FLAG_NONE, .NodeMask = 0, }; hr = s_videoD3D12.device->CreateCommandQueue(&queueDesc, IID_ID3D12CommandQueue, (void**)&m_videoQueue); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: video CreateCommandQueue failed (0x%08x).", hr); destroy(); return false; } for (uint32_t ii = 0; ii < kBitstreamRingFrames; ++ii) { hr = s_videoD3D12.device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_VIDEO_DECODE, IID_ID3D12CommandAllocator, (void**)&m_videoCmdAllocator[ii]); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: video CreateCommandAllocator failed (0x%08x).", hr); destroy(); return false; } } hr = s_videoD3D12.device->CreateCommandList(0 , D3D12_COMMAND_LIST_TYPE_VIDEO_DECODE , m_videoCmdAllocator[0] , NULL , IID_ID3D12VideoDecodeCommandList , (void**)&m_videoCmd ); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: CreateCommandList(VIDEO_DECODE) failed (0x%08x).", hr); destroy(); return false; } m_videoCmd->Close(); hr = s_videoD3D12.device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_ID3D12Fence, (void**)&m_videoFence); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: CreateFence failed (0x%08x).", hr); destroy(); return false; } for (uint32_t ii = 0; ii < kCopyRingSize; ++ii) { hr = s_videoD3D12.device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_ID3D12CommandAllocator, (void**)&m_copyCmdAllocator[ii]); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: copy CreateCommandAllocator failed (0x%08x).", hr); destroy(); return false; } } hr = s_videoD3D12.device->CreateCommandList(0 , D3D12_COMMAND_LIST_TYPE_DIRECT , m_copyCmdAllocator[0] , NULL , IID_ID3D12GraphicsCommandList , (void**)&m_copyCmdList ); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: CreateCommandList(DIRECT) failed (0x%08x).", hr); destroy(); return false; } m_copyCmdList->Close(); hr = s_videoD3D12.device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_ID3D12Fence, (void**)&m_copyFence); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: copy CreateFence failed (0x%08x).", hr); destroy(); return false; } for (uint32_t ii = 0; ii < kCopyRingSize; ++ii) { m_copyFenceRing[ii] = 0; } m_copyFenceValue = 0; m_videoControl.reset(); m_copyControl.reset(); bx::memSet(m_slotLastCopyFence, 0, sizeof(m_slotLastCopyFence) ); const uint32_t bitstreamBudget = bx::max(2u*1024u*1024u, m_codedWidth * m_codedHeight); m_bitstreamCapacity = bx::alignUp(uint64_t(bitstreamBudget) * kBitstreamRingFrames, D3D12_VIDEO_DECODE_MIN_BITSTREAM_OFFSET_ALIGNMENT); { D3D12_RESOURCE_DESC desc { .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, .Alignment = 0, .Width = m_bitstreamCapacity, .Height = 1, .DepthOrArraySize = 1, .MipLevels = 1, .Format = DXGI_FORMAT_UNKNOWN, .SampleDesc = { .Count = 1, .Quality = 0 }, .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, .Flags = D3D12_RESOURCE_FLAG_NONE, }; D3D12_HEAP_PROPERTIES heap { .Type = D3D12_HEAP_TYPE_UPLOAD, .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, .CreationNodeMask = 0, .VisibleNodeMask = 0, }; hr = s_videoD3D12.device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_GENERIC_READ, NULL, IID_ID3D12Resource, (void**)&m_bitstreamBuffer); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: bitstream buffer create failed (0x%08x).", hr); destroy(); return false; } D3D12_RANGE noRead = { 0, 0 }; hr = m_bitstreamBuffer->Map(0, &noRead, (void**)&m_bitstreamMapped); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: bitstream Map failed (0x%08x).", hr); destroy(); return false; } } { D3D12_RESOURCE_DESC desc { .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, .Alignment = 0, .Width = 256, .Height = 1, .DepthOrArraySize = 1, .MipLevels = 1, .Format = DXGI_FORMAT_UNKNOWN, .SampleDesc = { .Count = 1, .Quality = 0 }, .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, .Flags = D3D12_RESOURCE_FLAG_NONE, }; D3D12_HEAP_PROPERTIES heap { .Type = D3D12_HEAP_TYPE_UPLOAD, .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, .CreationNodeMask = 0, .VisibleNodeMask = 0, }; hr = s_videoD3D12.device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_GENERIC_READ, NULL, IID_ID3D12Resource, (void**)&m_dummyCbv); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: dummy CBV create failed (0x%08x).", hr); destroy(); return false; } } { D3D12_RESOURCE_DESC desc { .Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D, .Alignment = 0, .Width = m_codedWidth, .Height = m_codedHeight, .DepthOrArraySize = uint16_t(m_numDpbSlots), .MipLevels = 1, .Format = decodeFormat, .SampleDesc = { .Count = 1, .Quality = 0 }, .Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN, .Flags = m_referenceOnly ? (D3D12_RESOURCE_FLAG_VIDEO_DECODE_REFERENCE_ONLY | D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE) : D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS , }; D3D12_HEAP_PROPERTIES heap { .Type = D3D12_HEAP_TYPE_DEFAULT, .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, .CreationNodeMask = 0, .VisibleNodeMask = 0, }; hr = s_videoD3D12.device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, NULL, IID_ID3D12Resource, (void**)&m_dpb); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: DPB texture create failed (0x%08x).", hr); destroy(); return false; } if (m_referenceOnly) { // AMD path: separate output texture (sampleable NV12). desc.DepthOrArraySize = 1; desc.Flags = D3D12_RESOURCE_FLAG_NONE; hr = s_videoD3D12.device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, NULL, IID_ID3D12Resource, (void**)&m_dpbOutput); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: DPB output texture create failed (0x%08x).", hr); destroy(); return false; } } } for (uint32_t ii = 0; ii < m_numDpbSlots; ++ii) { m_dpbState[ii].state = D3D12_RESOURCE_STATE_COMMON; } BX_TRACE("VideoDecoderD3D12: H.264 decoder ready, coded %dx%d, dst %dx%d, %s DPB." , m_codedWidth, m_codedHeight , m_dstWidth, m_dstHeight , m_referenceOnly ? "reference-only" : "shared" ); return true; } void destroy() { if (NULL != m_videoFence && NULL != m_videoQueue) { const uint64_t fence = ++m_videoFenceValue; m_videoQueue->Signal(m_videoFence, fence); if (m_videoFence->GetCompletedValue() < fence) { HANDLE evt = CreateEventA(NULL, FALSE, FALSE, NULL); m_videoFence->SetEventOnCompletion(fence, evt); WaitForSingleObject(evt, INFINITE); CloseHandle(evt); } } if (NULL != m_copyFence && NULL != s_videoD3D12.commandQueue) { const uint64_t fence = ++m_copyFenceValue; s_videoD3D12.commandQueue->Signal(m_copyFence, fence); if (m_copyFence->GetCompletedValue() < fence) { HANDLE evt = CreateEventA(NULL, FALSE, FALSE, NULL); m_copyFence->SetEventOnCompletion(fence, evt); WaitForSingleObject(evt, INFINITE); CloseHandle(evt); } } DX_RELEASE(m_videoCmd, 0); DX_RELEASE(m_copyCmdList, 0); for (uint32_t ii = 0; ii < kBitstreamRingFrames; ++ii) { if (NULL != m_videoCmdAllocator[ii]) { m_videoCmdAllocator[ii]->Reset(); } } for (uint32_t ii = 0; ii < kCopyRingSize; ++ii) { if (NULL != m_copyCmdAllocator[ii]) { m_copyCmdAllocator[ii]->Reset(); } } for (uint32_t ii = 0; ii < kVideoMaxReorderInFlight; ++ii) { if (NULL != m_reorderPool[ii].texture) { videoReleaseResource(m_renderer, m_reorderPool[ii].texture); m_reorderPool[ii].texture = NULL; } m_reorderPool[ii].displayOrder = -1; } if (NULL != m_bitstreamBuffer && NULL != m_bitstreamMapped) { D3D12_RANGE noWrite = { 0, 0 }; m_bitstreamBuffer->Unmap(0, &noWrite); m_bitstreamMapped = NULL; } DX_RELEASE(m_bitstreamBuffer, 0); DX_RELEASE(m_dummyCbv, 0); DX_RELEASE(m_dpbOutput, 0); DX_RELEASE(m_dpb, 0); for (uint32_t ii = 0; ii < kBitstreamRingFrames; ++ii) { DX_RELEASE(m_videoCmdAllocator[ii], 0); } for (uint32_t ii = 0; ii < kCopyRingSize; ++ii) { DX_RELEASE(m_copyCmdAllocator[ii], 0); } DX_RELEASE(m_copyFence, 0); DX_RELEASE(m_videoFence, 0); DX_RELEASE(m_videoQueue, 0); DX_RELEASE(m_decoder, 0); DX_RELEASE(m_decoderHeap, 0); DX_RELEASE_I(m_videoDevice); } bool decode(const VideoDecoderFrame& _frame, TextureD3D12& _dst) { if (NULL == m_decoder) { return false; } if (0 != (_frame.flags & BGFX_VIDEO_DECODE_FRAME_SET) ) { resetForSet(); } if (NULL != _frame.bitstream && 0 != _frame.numAus) { const uint8_t* bs = _frame.bitstream; for (uint32_t ii = 0; ii < _frame.numAus; ++ii) { const uint32_t auSize = _frame.aus[ii].size; const bool isIdr = bgfx::peekAuIsIdr(bs, auSize); m_auQueue.enqueue(bs, auSize, _frame.aus[ii].ptsUs, isIdr); bs += auSize; } } while (!m_auQueue.empty() && bgfx::hasReorderSpace(m_reorderPool, kVideoMaxReorderInFlight) ) { const bgfx::PendingAU& au = m_auQueue.front(); decodeOneAU(au.data.data(), uint32_t(au.data.size() ), au.ptsUs); m_auQueue.pop(); } m_auQueue.compact(); if (0 == _frame.numAus && 0 == (_frame.flags & BGFX_VIDEO_DECODE_FRAME_NO_BLIT) ) { int64_t adjustedClock = _frame.presentationTimeUs; if (0 != (_frame.flags & BGFX_VIDEO_DECODE_FRAME_LOOP) && m_auQueue.isRetaining() && m_auQueue.minPts() < m_auQueue.maxPts() ) { adjustedClock = bgfx::applyLoopWrap(adjustedClock, m_auQueue.minPts(), m_auQueue.maxPts() ); const bool poolCovers = bgfx::reorderPoolCovers(m_reorderPool, kVideoMaxReorderInFlight, adjustedClock, 500000); const bool forwardCanCatchUp = true && !m_auQueue.empty() && m_auQueue.front().ptsUs <= adjustedClock + 500000 ; if (!poolCovers && !forwardCanCatchUp) { bgfx::faultInFromCache( m_auQueue , adjustedClock , [this]() { resetForSet(); } , [this](const uint8_t* _bs, uint32_t _bsSize, int64_t _pts) { decodeOneAU(_bs, _bsSize, _pts); } , [this]() { return bgfx::hasReorderSpace(m_reorderPool, kVideoMaxReorderInFlight); } ); } } pickDisplaySlotForTime(adjustedClock, _dst); } return true; } void resetForSet() { if (m_auQueue.isRetaining() ) { m_auQueue.rewindTo(m_auQueue.size() ); } else { m_auQueue.clear(); } for (uint32_t ii = 0; ii < kVideoMaxReorderInFlight; ++ii) { m_reorderPool[ii].displayOrder = -1; m_reorderPool[ii].ptsUs = 0; } m_displayedSlot = -1; m_prevDisplayedSlot = -1; m_displayOrderNext = 0; m_numActiveRefs = 0; m_nextSlot = 0; m_currentSlot = 0; bx::memSet(m_referenceUsage, 0, sizeof(m_referenceUsage) ); for (uint32_t ii = 0; ii < kNumDpbSlots; ++ii) { m_dpbState[ii].poc = 0; m_dpbState[ii].frameNum = 0; } m_prevPocLsb = 0; m_prevPocMsb = 0; } bool decodeOneAU(const uint8_t* _bitstream, uint32_t _bitstreamSize, int64_t _ptsUs) { h264::NALHeader nal = {}; h264::SliceHeader sh = {}; bool isIdr = false; if (!bgfx::parseSliceFromAccessUnit(m_spsArray, m_ppsArray, _bitstream, _bitstreamSize, nal, sh, isIdr) ) { BX_TRACE("VideoDecoderD3D12: no slice NAL found in access unit."); return false; } const h264::PPS& pps = m_ppsArray[sh.pic_parameter_set_id]; const h264::SPS& sps = m_spsArray[pps.seq_parameter_set_id]; if (isIdr) { m_numActiveRefs = 0; m_nextSlot = 0; } const int32_t poc = bgfx::computePoc(m_prevPocMsb, m_prevPocLsb, sps, sh, nal, isIdr); m_currentSlot = bgfx::pickFreeDpbSlot( m_nextSlot , m_numDpbSlots , m_referenceUsage , m_numActiveRefs , [](uint32_t){ return false; } ); BX_ASSERT(m_currentSlot < m_numDpbSlots , "DPB exhausted: m_numDpbSlots=%u m_numActiveRefs=%u. Cap maxRefs to m_numDpbSlots-1." , m_numDpbSlots , m_numActiveRefs ); m_nextSlot = (m_currentSlot + 1) % m_numDpbSlots; const uint32_t reorderSlot = bgfx::pickReorderSlotOrEvictOldest(m_reorderPool, kVideoMaxReorderInFlight); const int32_t displayOrder = m_displayOrderNext++; m_reorderPool[reorderSlot].displayOrder = displayOrder; m_reorderPool[reorderSlot].ptsUs = _ptsUs; m_dpbState[m_currentSlot].poc = poc; m_dpbState[m_currentSlot].frameNum = sh.frame_num; while (0 == m_videoControl.reserve(1) ) { const uint32_t readIdx = m_videoControl.m_read; const uint64_t waitFence = m_videoCmdAllocatorFence[readIdx]; if (waitFence > m_videoFence->GetCompletedValue() ) { HANDLE evt = CreateEventA(NULL, FALSE, FALSE, NULL); m_videoFence->SetEventOnCompletion(waitFence, evt); WaitForSingleObject(evt, INFINITE); CloseHandle(evt); } m_videoControl.consume(1); } const uint32_t allocIdx = m_videoControl.m_current; const uint32_t sliceOffsetInAu = bgfx::findFirstSliceNalOffset(_bitstream, _bitstreamSize); const uint8_t* sliceBitstream = _bitstream + sliceOffsetInAu; const uint32_t sliceBitstreamSize = _bitstreamSize - sliceOffsetInAu; const uint32_t alignedSize = bx::alignUp(sliceBitstreamSize, D3D12_VIDEO_DECODE_MIN_BITSTREAM_OFFSET_ALIGNMENT); if (m_bitstreamCursor + alignedSize > m_bitstreamCapacity) { m_bitstreamCursor = 0; } const uint64_t bitstreamOffset = m_bitstreamCursor; bx::memCopy(m_bitstreamMapped + bitstreamOffset, sliceBitstream, sliceBitstreamSize); m_bitstreamCursor += alignedSize; HRESULT hr = m_videoCmdAllocator[allocIdx]->Reset(); BX_WARN(SUCCEEDED(hr), "VideoDecoderD3D12: video allocator Reset failed (0x%08x).", hr); BX_UNUSED(hr); hr = m_videoCmd->Reset(m_videoCmdAllocator[allocIdx]); BX_WARN(SUCCEEDED(hr), "VideoDecoderD3D12: video cmdlist Reset failed (0x%08x).", hr); if (m_dpbState[m_currentSlot].state != D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE) { D3D12_RESOURCE_BARRIER barriers[2] { { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = m_dpb, .Subresource = m_currentSlot, .StateBefore = m_dpbState[m_currentSlot].state, .StateAfter = D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE, }, }, { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = m_dpb, .Subresource = m_numDpbSlots + m_currentSlot, .StateBefore = m_dpbState[m_currentSlot].state, .StateAfter = D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE, }, }, }; m_videoCmd->ResourceBarrier(BX_COUNTOF(barriers), barriers); m_dpbState[m_currentSlot].state = D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE; } if (m_referenceOnly) { D3D12_RESOURCE_BARRIER barrier { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = m_dpbOutput, .Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, .StateBefore = D3D12_RESOURCE_STATE_COMMON, .StateAfter = D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE, }, }; m_videoCmd->ResourceBarrier(1, &barrier); } DXVA_PicParams_H264 picParams = {}; DXVA_Qmatrix_H264 qmatrix = {}; DXVA_Slice_H264_Short sliceCtl = {}; picParams.wFrameWidthInMbsMinus1 = uint16_t(sps.pic_width_in_mbs_minus1); picParams.wFrameHeightInMbsMinus1 = uint16_t(sps.pic_height_in_map_units_minus1); picParams.IntraPicFlag = isIdr ? 1 : 0; picParams.MbaffFrameFlag = 0; picParams.field_pic_flag = 0; picParams.chroma_format_idc = 1; picParams.bit_depth_chroma_minus8 = uint8_t(sps.bit_depth_chroma_minus8); picParams.bit_depth_luma_minus8 = uint8_t(sps.bit_depth_luma_minus8); picParams.residual_colour_transform_flag = uint8_t(sps.separate_colour_plane_flag); picParams.CurrPic.AssociatedFlag = 0; picParams.CurrPic.Index7Bits = uint8_t(m_currentSlot); picParams.CurrFieldOrderCnt[0] = poc; picParams.CurrFieldOrderCnt[1] = poc; for (uint32_t ii = 0; ii < 16; ++ii) { picParams.RefFrameList[ii].bPicEntry = 0xFF; picParams.FieldOrderCntList[ii][0] = 0; picParams.FieldOrderCntList[ii][1] = 0; picParams.FrameNumList[ii] = 0; } picParams.UsedForReferenceFlags = 0; for (uint32_t ii = 0; ii < m_numActiveRefs && ii < 16; ++ii) { const uint32_t refSlot = m_referenceUsage[ii]; picParams.RefFrameList[ii].AssociatedFlag = 0; picParams.RefFrameList[ii].Index7Bits = uint8_t(refSlot); picParams.FieldOrderCntList[ii][0] = m_dpbState[refSlot].poc; picParams.FieldOrderCntList[ii][1] = m_dpbState[refSlot].poc; picParams.FrameNumList[ii] = uint16_t(m_dpbState[refSlot].frameNum); picParams.UsedForReferenceFlags |= 1u << (ii * 2 + 0); picParams.UsedForReferenceFlags |= 1u << (ii * 2 + 1); } picParams.weighted_pred_flag = uint8_t(pps.weighted_pred_flag); picParams.weighted_bipred_idc = uint8_t(pps.weighted_bipred_idc); picParams.sp_for_switch_flag = 0; picParams.transform_8x8_mode_flag = uint8_t(pps.transform_8x8_mode_flag); picParams.constrained_intra_pred_flag = uint8_t(pps.constrained_intra_pred_flag); picParams.num_ref_frames = uint8_t(sps.num_ref_frames); picParams.MbsConsecutiveFlag = 1; picParams.frame_mbs_only_flag = uint8_t(sps.frame_mbs_only_flag); picParams.MinLumaBipredSize8x8Flag = sps.level_idc >= 31 ? 1 : 0; picParams.RefPicFlag = (0 != nal.idc) ? 1 : 0; picParams.frame_num = uint16_t(sh.frame_num); picParams.pic_init_qp_minus26 = int8_t(pps.pic_init_qp_minus26); picParams.pic_init_qs_minus26 = int8_t(pps.pic_init_qs_minus26); picParams.chroma_qp_index_offset = int8_t(pps.chroma_qp_index_offset); picParams.second_chroma_qp_index_offset = int8_t(pps.second_chroma_qp_index_offset); picParams.log2_max_frame_num_minus4 = uint8_t(sps.log2_max_frame_num_minus4); picParams.pic_order_cnt_type = uint8_t(sps.pic_order_cnt_type); picParams.log2_max_pic_order_cnt_lsb_minus4 = uint8_t(sps.log2_max_pic_order_cnt_lsb_minus4); picParams.delta_pic_order_always_zero_flag = uint8_t(sps.delta_pic_order_always_zero_flag); picParams.direct_8x8_inference_flag = uint8_t(sps.direct_8x8_inference_flag); picParams.entropy_coding_mode_flag = uint8_t(pps.entropy_coding_mode_flag); picParams.pic_order_present_flag = uint8_t(pps.pic_order_present_flag); picParams.num_slice_groups_minus1 = uint8_t(pps.num_slice_groups_minus1); picParams.slice_group_map_type = uint8_t(pps.slice_group_map_type); picParams.deblocking_filter_control_present_flag = uint8_t(pps.deblocking_filter_control_present_flag); picParams.redundant_pic_cnt_present_flag = uint8_t(pps.redundant_pic_cnt_present_flag); picParams.slice_group_change_rate_minus1 = uint16_t(pps.slice_group_change_rate_minus1); picParams.Reserved16Bits = 3; picParams.StatusReportFeedbackNumber = ++m_statusReportFeedback; picParams.ContinuationFlag = 1; picParams.num_ref_idx_l0_active_minus1 = uint8_t(pps.num_ref_idx_l0_active_minus1); picParams.num_ref_idx_l1_active_minus1 = uint8_t(pps.num_ref_idx_l1_active_minus1); if (sps.seq_scaling_matrix_present_flag || pps.pic_scaling_matrix_present_flag) { uint8_t lists4x4[6][16]; uint8_t lists8x8[2][64]; buildEffectiveScalingLists(sps, pps, lists4x4, lists8x8); for (int i = 0; i < 6; ++i) { for (int j = 0; j < 16; ++j) { qmatrix.bScalingLists4x4[i][j] = lists4x4[i][j]; } } for (int i = 0; i < 64; ++i) { qmatrix.bScalingLists8x8[0][i] = lists8x8[0][i]; qmatrix.bScalingLists8x8[1][i] = lists8x8[1][i]; } } else { bx::memSet(&qmatrix, 16, sizeof(qmatrix) ); } sliceCtl.BSNALunitDataLocation = 0; sliceCtl.SliceBytesInBuffer = sliceBitstreamSize; sliceCtl.wBadSliceChopping = 0; ID3D12Resource* refFrames[kNumDpbSlots] = {}; UINT refSubresources[kNumDpbSlots] = {}; for (uint32_t ii = 0; ii < m_numDpbSlots; ++ii) { refFrames[ii] = m_dpb; refSubresources[ii] = UINT(ii); } D3D12_VIDEO_DECODE_INPUT_STREAM_ARGUMENTS input { .NumFrameArguments = 3, .FrameArguments = { { .Type = D3D12_VIDEO_DECODE_ARGUMENT_TYPE_PICTURE_PARAMETERS, .Size = sizeof(picParams), .pData = &picParams }, { .Type = D3D12_VIDEO_DECODE_ARGUMENT_TYPE_INVERSE_QUANTIZATION_MATRIX,.Size = sizeof(qmatrix), .pData = &qmatrix }, { .Type = D3D12_VIDEO_DECODE_ARGUMENT_TYPE_SLICE_CONTROL, .Size = sizeof(sliceCtl), .pData = &sliceCtl }, }, .ReferenceFrames = { .NumTexture2Ds = kNumDpbSlots, .ppTexture2Ds = refFrames, .pSubresources = refSubresources, .ppHeaps = NULL, }, .CompressedBitstream = { .pBuffer = m_bitstreamBuffer, .Offset = bitstreamOffset, .Size = sliceBitstreamSize, }, .pHeap = m_decoderHeap, }; D3D12_VIDEO_DECODE_OUTPUT_STREAM_ARGUMENTS output = m_referenceOnly ? D3D12_VIDEO_DECODE_OUTPUT_STREAM_ARGUMENTS { .pOutputTexture2D = m_dpbOutput, .OutputSubresource = 0, .ConversionArguments = { .Enable = TRUE, .pReferenceTexture2D = m_dpb, .ReferenceSubresource = m_currentSlot, .OutputColorSpace = DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, .DecodeColorSpace = DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, }, } : D3D12_VIDEO_DECODE_OUTPUT_STREAM_ARGUMENTS { .pOutputTexture2D = m_dpb, .OutputSubresource = m_currentSlot, .ConversionArguments = {}, } ; m_videoCmd->DecodeFrame(m_decoder, &output, &input); if (m_referenceOnly) { D3D12_RESOURCE_BARRIER barrier { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = m_dpbOutput, .Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, .StateBefore = D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }; m_videoCmd->ResourceBarrier(1, &barrier); D3D12_RESOURCE_BARRIER dpbBarriers[2] { { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = m_dpb, .Subresource = m_currentSlot, .StateBefore = D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = m_dpb, .Subresource = m_numDpbSlots + m_currentSlot, .StateBefore = D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, }; m_videoCmd->ResourceBarrier(BX_COUNTOF(dpbBarriers), dpbBarriers); m_dpbState[m_currentSlot].state = D3D12_RESOURCE_STATE_COMMON; } else { D3D12_RESOURCE_BARRIER barriers[2] { { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = m_dpb, .Subresource = m_currentSlot, .StateBefore = D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = m_dpb, .Subresource = m_numDpbSlots + m_currentSlot, .StateBefore = D3D12_RESOURCE_STATE_VIDEO_DECODE_WRITE, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, }; m_videoCmd->ResourceBarrier(BX_COUNTOF(barriers), barriers); m_dpbState[m_currentSlot].state = D3D12_RESOURCE_STATE_COMMON; } hr = m_videoCmd->Close(); BX_WARN(SUCCEEDED(hr), "VideoDecoderD3D12: video Close failed (0x%08x).", hr); ID3D12CommandList* lists[] = { m_videoCmd }; if (m_slotLastCopyFence[m_currentSlot] > 0) { m_videoQueue->Wait(m_copyFence, m_slotLastCopyFence[m_currentSlot]); } if (m_referenceOnly && m_lastDpbOutputCopyFence > 0) { m_videoQueue->Wait(m_copyFence, m_lastDpbOutputCopyFence); } m_videoQueue->ExecuteCommandLists(BX_COUNTOF(lists), lists); const uint64_t signalValue = ++m_videoFenceValue; m_videoQueue->Signal(m_videoFence, signalValue); m_videoCmdAllocatorFence[allocIdx] = signalValue; m_videoControl.commit(1); if (0 != nal.idc) { if (0 != sh.drpm.adaptive_ref_pic_marking_mode_flag && m_numActiveRefs > 0) { uint16_t refFrameNums[16]; const uint32_t n = bx::min(m_numActiveRefs, 16); for (uint32_t ii = 0; ii < n; ++ii) { refFrameNums[ii] = uint16_t(m_dpbState[m_referenceUsage[ii]].frameNum); } const int32_t maxFrameNum = 1 << (sps.log2_max_frame_num_minus4 + 4); m_numActiveRefs = applyMmcoUnmarkShortTerm( m_referenceUsage , refFrameNums , n , sh.frame_num , maxFrameNum , sh ); } const uint32_t maxRefs = bx::min(uint32_t(sps.num_ref_frames), m_numDpbSlots - 1); m_numActiveRefs = bgfx::appendShortTermRef( m_referenceUsage , m_numActiveRefs , m_currentSlot , maxRefs ); } while (0 == m_copyControl.reserve(1) ) { const uint32_t readIdx = m_copyControl.m_read; const uint64_t waitFence = m_copyFenceRing[readIdx]; if (m_copyFence->GetCompletedValue() < waitFence) { HANDLE evt = CreateEventA(NULL, FALSE, FALSE, NULL); m_copyFence->SetEventOnCompletion(waitFence, evt); WaitForSingleObject(evt, INFINITE); CloseHandle(evt); } m_copyControl.consume(1); } const uint32_t copyRingIdx = m_copyControl.m_current; hr = m_copyCmdAllocator[copyRingIdx]->Reset(); BX_WARN(SUCCEEDED(hr), "VideoDecoderD3D12: copy allocator Reset failed (0x%08x).", hr); hr = m_copyCmdList->Reset(m_copyCmdAllocator[copyRingIdx], NULL); BX_WARN(SUCCEEDED(hr), "VideoDecoderD3D12: copy cmdlist Reset failed (0x%08x).", hr); ID3D12GraphicsCommandList* commandList = m_copyCmdList; ID3D12Resource* nv12Source = m_referenceOnly ? m_dpbOutput : m_dpb; const uint32_t nv12LumaSubres = m_referenceOnly ? 0u : m_currentSlot; const uint32_t nv12ChromaSubres = m_referenceOnly ? 1u : (m_numDpbSlots + m_currentSlot); ID3D12Resource* reorderTex = acquireReorderTexture(uint32_t(reorderSlot) ); if (NULL == reorderTex) { m_copyCmdList->Close(); return false; } { D3D12_RESOURCE_BARRIER barriers[2] { { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = reorderTex, .Subresource = 0, .StateBefore = D3D12_RESOURCE_STATE_COMMON, .StateAfter = D3D12_RESOURCE_STATE_COPY_DEST, }, }, { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = reorderTex, .Subresource = 1, .StateBefore = D3D12_RESOURCE_STATE_COMMON, .StateAfter = D3D12_RESOURCE_STATE_COPY_DEST, }, }, }; commandList->ResourceBarrier(BX_COUNTOF(barriers), barriers); } { D3D12_RESOURCE_BARRIER barriers[2] { { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = nv12Source, .Subresource = nv12LumaSubres, .StateBefore = D3D12_RESOURCE_STATE_COMMON, .StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE, }, }, { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = nv12Source, .Subresource = nv12ChromaSubres, .StateBefore = D3D12_RESOURCE_STATE_COMMON, .StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE, }, }, }; commandList->ResourceBarrier(BX_COUNTOF(barriers), barriers); } { const uint32_t reorderW = (m_dstWidth + 1) & ~1u; const uint32_t reorderH = (m_dstHeight + 1) & ~1u; const D3D12_BOX lumaBox = { 0, 0, 0, reorderW, reorderH, 1 }; const D3D12_BOX chromaBox = { 0, 0, 0, reorderW / 2, reorderH / 2, 1 }; { D3D12_TEXTURE_COPY_LOCATION dst { .pResource = reorderTex, .Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, .SubresourceIndex = 0, }; D3D12_TEXTURE_COPY_LOCATION src { .pResource = nv12Source, .Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, .SubresourceIndex = nv12LumaSubres, }; commandList->CopyTextureRegion(&dst, 0, 0, 0, &src, &lumaBox); } { D3D12_TEXTURE_COPY_LOCATION dst { .pResource = reorderTex, .Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, .SubresourceIndex = 1, }; D3D12_TEXTURE_COPY_LOCATION src { .pResource = nv12Source, .Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, .SubresourceIndex = nv12ChromaSubres, }; commandList->CopyTextureRegion(&dst, 0, 0, 0, &src, &chromaBox); } } { D3D12_RESOURCE_BARRIER barriers[2] { { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = nv12Source, .Subresource = nv12LumaSubres, .StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = nv12Source, .Subresource = nv12ChromaSubres, .StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, }; commandList->ResourceBarrier(BX_COUNTOF(barriers), barriers); } { D3D12_RESOURCE_BARRIER barriers[2] { { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = reorderTex, .Subresource = 0, .StateBefore = D3D12_RESOURCE_STATE_COPY_DEST, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = reorderTex, .Subresource = 1, .StateBefore = D3D12_RESOURCE_STATE_COPY_DEST, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, }; commandList->ResourceBarrier(BX_COUNTOF(barriers), barriers); } hr = m_copyCmdList->Close(); BX_WARN(SUCCEEDED(hr), "VideoDecoderD3D12: copy Close failed (0x%08x).", hr); ID3D12CommandQueue* gfxQueue = s_videoD3D12.commandQueue; gfxQueue->Wait(m_videoFence, signalValue); ID3D12CommandList* copyLists[] = { m_copyCmdList }; gfxQueue->ExecuteCommandLists(BX_COUNTOF(copyLists), copyLists); const uint64_t copySignal = ++m_copyFenceValue; gfxQueue->Signal(m_copyFence, copySignal); m_copyFenceRing[copyRingIdx] = copySignal; m_slotLastCopyFence[m_currentSlot] = copySignal; m_lastDpbOutputCopyFence = copySignal; m_copyControl.commit(1); return true; } void pickDisplaySlotForTime(int64_t _presentationTimeUs, TextureD3D12& _dst) { bgfx::pickDisplaySlotForTime( _presentationTimeUs , m_reorderPool , kVideoMaxReorderInFlight , m_displayedSlot , m_prevDisplayedSlot , m_auQueue.empty() , [this, &_dst](uint32_t _slot) { ID3D12GraphicsCommandList* gfx = videoGetCommandList(m_renderer); dispatchYuvToRgb(gfx, m_reorderPool[_slot].texture, _dst); } ); } RendererContextD3D12* m_renderer; ID3D12Resource* acquireReorderTexture(uint32_t _slot) { BX_ASSERT(_slot < kVideoMaxReorderInFlight, "VideoDecoderD3D12: reorder slot %u out of range.", _slot); if (NULL != m_reorderPool[_slot].texture) { return m_reorderPool[_slot].texture; } ID3D12Device* device = s_videoD3D12.device; D3D12_RESOURCE_DESC desc { .Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D, .Alignment = 0, .Width = (m_dstWidth + 1) & ~1u, .Height = (m_dstHeight + 1) & ~1u, .DepthOrArraySize = 1, .MipLevels = 1, .Format = DXGI_FORMAT_NV12, .SampleDesc = { .Count = 1, .Quality = 0 }, .Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN, .Flags = D3D12_RESOURCE_FLAG_NONE, }; D3D12_HEAP_PROPERTIES heap { .Type = D3D12_HEAP_TYPE_DEFAULT, .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, .CreationNodeMask = 0, .VisibleNodeMask = 0, }; HRESULT hr = device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, NULL, IID_ID3D12Resource, (void**)&m_reorderPool[_slot].texture); if (FAILED(hr) ) { BX_TRACE("VideoDecoderD3D12: reorder texture create failed (0x%08x).", hr); return NULL; } return m_reorderPool[_slot].texture; } void dispatchYuvToRgb(ID3D12GraphicsCommandList* _commandList, ID3D12Resource* _nv12, TextureD3D12& _dst) { BX_ASSERT(NULL != g_videoDecode && isValid(g_videoDecode->m_program), "Video decode program not initialized."); ID3D12PipelineState* pso = videoGetPipelineState(m_renderer, g_videoDecode->m_program); if (NULL == pso) { return; } { D3D12_RESOURCE_BARRIER barriers[2] { { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = _nv12, .Subresource = 0, .StateBefore = D3D12_RESOURCE_STATE_COMMON, .StateAfter = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, }, }, { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = _nv12, .Subresource = 1, .StateBefore = D3D12_RESOURCE_STATE_COMMON, .StateAfter = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, }, }, }; _commandList->ResourceBarrier(BX_COUNTOF(barriers), barriers); } _dst.setState(_commandList, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); ScratchBufferD3D12& scratch = videoGetScratchBuffer(m_renderer); _commandList->SetComputeRootSignature(s_videoD3D12.computeRootSignature); ID3D12DescriptorHeap* heaps[] = { s_videoD3D12.samplerHeap, scratch.getHeap(), }; _commandList->SetDescriptorHeaps(BX_COUNTOF(heaps), heaps); _commandList->SetPipelineState(pso); uint32_t samplerFlags[BGFX_MAX_COMPUTE_BINDINGS] = {}; samplerFlags[1] = BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP | BGFX_SAMPLER_W_CLAMP; samplerFlags[2] = BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP | BGFX_SAMPLER_W_CLAMP; _commandList->SetComputeRootDescriptorTable(ComputeRp::Sampler, videoGetSamplerHandle(m_renderer, samplerFlags) ); D3D12_GPU_VIRTUAL_ADDRESS cbvAddress = m_dummyCbv->GetGPUVirtualAddress(); _commandList->SetComputeRootConstantBufferView(ComputeRp::CBV, cbvAddress); D3D12_GPU_DESCRIPTOR_HANDLE srvHandle[BGFX_MAX_COMPUTE_BINDINGS] = {}; { scratch.allocEmpty(srvHandle[0]); D3D12_SHADER_RESOURCE_VIEW_DESC srvDescY { .Format = DXGI_FORMAT_R8_UNORM, .ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D, .Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING, .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1, .PlaneSlice = 0, .ResourceMinLODClamp = 0.0f, }, }; scratch.allocSrv(srvHandle[1], _nv12, srvDescY); D3D12_SHADER_RESOURCE_VIEW_DESC srvDescCbCr { .Format = DXGI_FORMAT_R8G8_UNORM, .ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D, .Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING, .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1, .PlaneSlice = 1, .ResourceMinLODClamp = 0.0f, }, }; scratch.allocSrv(srvHandle[2], _nv12, srvDescCbCr); for (uint32_t ii = 3; ii < BGFX_MAX_COMPUTE_BINDINGS; ++ii) { scratch.allocEmpty(srvHandle[ii]); } } _commandList->SetComputeRootDescriptorTable(ComputeRp::SRV, srvHandle[0]); D3D12_GPU_DESCRIPTOR_HANDLE uavHandle[BGFX_MAX_COMPUTE_BINDINGS] = {}; { scratch.allocUav(uavHandle[0], _dst, 0); for (uint32_t ii = 1; ii < BGFX_MAX_COMPUTE_BINDINGS; ++ii) { scratch.allocEmpty(uavHandle[ii]); } } _commandList->SetComputeRootDescriptorTable(ComputeRp::UAV, uavHandle[0]); _commandList->Dispatch( bx::max( (m_dstWidth + 7) / 8, 1) , bx::max( (m_dstHeight + 7) / 8, 1) , 1 ); { D3D12_RESOURCE_BARRIER barriers[2] { { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = _nv12, .Subresource = 0, .StateBefore = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, { .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, .Transition = { .pResource = _nv12, .Subresource = 1, .StateBefore = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, .StateAfter = D3D12_RESOURCE_STATE_COMMON, }, }, }; _commandList->ResourceBarrier(BX_COUNTOF(barriers), barriers); } } ID3D12VideoDevice* m_videoDevice; ID3D12VideoDecoder* m_decoder; ID3D12VideoDecoderHeap* m_decoderHeap; ID3D12CommandQueue* m_videoQueue; ID3D12CommandAllocator* m_videoCmdAllocator[kBitstreamRingFrames]; uint64_t m_videoCmdAllocatorFence[kBitstreamRingFrames]; ID3D12VideoDecodeCommandList* m_videoCmd; ID3D12Fence* m_videoFence; uint64_t m_videoFenceValue; bx::RingBufferControl m_videoControl; ID3D12CommandAllocator* m_copyCmdAllocator[kCopyRingSize]; ID3D12GraphicsCommandList* m_copyCmdList; ID3D12Fence* m_copyFence; uint64_t m_copyFenceValue; uint64_t m_copyFenceRing[kCopyRingSize]; uint64_t m_slotLastCopyFence[kNumDpbSlots]; uint64_t m_lastDpbOutputCopyFence; bx::RingBufferControl m_copyControl; ID3D12Resource* m_bitstreamBuffer; uint8_t* m_bitstreamMapped; uint64_t m_bitstreamCapacity; uint64_t m_bitstreamCursor; ID3D12Resource* m_dummyCbv; ID3D12Resource* m_dpb; ID3D12Resource* m_dpbOutput; bool m_referenceOnly; DpbState m_dpbState[kNumDpbSlots]; uint8_t m_referenceUsage[kNumDpbSlots]; uint32_t m_codedWidth; uint32_t m_codedHeight; uint32_t m_dstWidth; uint32_t m_dstHeight; uint32_t m_numDpbSlots; uint32_t m_nextSlot; uint32_t m_currentSlot; uint32_t m_numActiveRefs; int32_t m_displayOrderNext; int32_t m_prevPocLsb; int32_t m_prevPocMsb; ReorderedPicture m_reorderPool[kVideoMaxReorderInFlight]; int32_t m_displayedSlot; int32_t m_prevDisplayedSlot; uint32_t m_statusReportFeedback; bgfx::AuQueue m_auQueue; uint32_t m_initFlags; uint32_t m_cachedAuBytes; h264::SPS m_spsActive; h264::SPS m_spsArray[kVideoMaxH264SpsCount]; h264::PPS m_ppsArray[kVideoMaxH264PpsCount]; }; VideoDecoderD3D12* videoDecoderCreate(const VideoDecoderInit& _init, RendererContextD3D12* _renderer, uint16_t _width, uint16_t _height) { VideoDecoderD3D12* decoder = BX_NEW(g_allocator, VideoDecoderD3D12); if (!decoder->create(_init, _renderer, _width, _height) ) { decoder->destroy(); bx::deleteObject(g_allocator, decoder); return NULL; } return decoder; } void videoDecoderDestroy(VideoDecoderD3D12* _decoder) { if (NULL != _decoder) { _decoder->destroy(); bx::deleteObject(g_allocator, _decoder); } } bool videoDecoderDecode(VideoDecoderD3D12* _decoder, const VideoDecoderFrame& _frame, TextureD3D12& _dst) { return _decoder->decode(_frame, _dst); } void initVideoDecoder(const VideoBindingD3D12& _binding) { s_videoD3D12 = _binding; ID3D12VideoDevice* videoDevice = NULL; HRESULT hr = _binding.device->QueryInterface(IID_ID3D12VideoDevice, (void**)&videoDevice); if (FAILED(hr) || NULL == videoDevice) { return; } D3D12_FEATURE_DATA_VIDEO_DECODE_PROFILE_COUNT profileCount = {}; hr = videoDevice->CheckFeatureSupport( D3D12_FEATURE_VIDEO_DECODE_PROFILE_COUNT , &profileCount , sizeof(profileCount) ); if (FAILED(hr) || 0 == profileCount.ProfileCount) { DX_RELEASE(videoDevice, 0); return; } GUID* profiles = (GUID*)BX_STACK_ALLOC(profileCount.ProfileCount * sizeof(GUID) ); D3D12_FEATURE_DATA_VIDEO_DECODE_PROFILES profileList { .NodeIndex = 0, .ProfileCount = profileCount.ProfileCount, .pProfiles = profiles, }; hr = videoDevice->CheckFeatureSupport( D3D12_FEATURE_VIDEO_DECODE_PROFILES , &profileList , sizeof(profileList) ); if (FAILED(hr) ) { DX_RELEASE(videoDevice, 0); return; } struct ProfileEntry { const GUID* guid; VideoCodec::Enum codec; uint32_t caps; }; static const ProfileEntry s_profiles[] = { { &D3D12_VIDEO_DECODE_PROFILE_H264, VideoCodec::H264, BGFX_CAPS_VIDEO_CODEC_BIT_8 | BGFX_CAPS_VIDEO_CODEC_CHROMA_420 }, { &D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN, VideoCodec::H265, BGFX_CAPS_VIDEO_CODEC_BIT_8 | BGFX_CAPS_VIDEO_CODEC_CHROMA_420 }, { &D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN10, VideoCodec::H265, BGFX_CAPS_VIDEO_CODEC_BIT_10 | BGFX_CAPS_VIDEO_CODEC_CHROMA_420 }, { &D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN12, VideoCodec::H265, BGFX_CAPS_VIDEO_CODEC_BIT_12 | BGFX_CAPS_VIDEO_CODEC_CHROMA_420 }, { &D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN10_422, VideoCodec::H265, BGFX_CAPS_VIDEO_CODEC_BIT_10 | BGFX_CAPS_VIDEO_CODEC_CHROMA_422 }, { &D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN12_422, VideoCodec::H265, BGFX_CAPS_VIDEO_CODEC_BIT_12 | BGFX_CAPS_VIDEO_CODEC_CHROMA_422 }, { &D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN_444, VideoCodec::H265, BGFX_CAPS_VIDEO_CODEC_BIT_8 | BGFX_CAPS_VIDEO_CODEC_CHROMA_444 }, { &D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN10_444, VideoCodec::H265, BGFX_CAPS_VIDEO_CODEC_BIT_10 | BGFX_CAPS_VIDEO_CODEC_CHROMA_444 }, { &D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN12_444, VideoCodec::H265, BGFX_CAPS_VIDEO_CODEC_BIT_12 | BGFX_CAPS_VIDEO_CODEC_CHROMA_444 }, { &D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE0, VideoCodec::AV1, BGFX_CAPS_VIDEO_CODEC_BIT_8 | BGFX_CAPS_VIDEO_CODEC_BIT_10 | BGFX_CAPS_VIDEO_CODEC_CHROMA_420 }, { &D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE1, VideoCodec::AV1, BGFX_CAPS_VIDEO_CODEC_BIT_8 | BGFX_CAPS_VIDEO_CODEC_BIT_10 | BGFX_CAPS_VIDEO_CODEC_CHROMA_444 }, { &D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE2, VideoCodec::AV1, BGFX_CAPS_VIDEO_CODEC_BIT_10 | BGFX_CAPS_VIDEO_CODEC_CHROMA_420 | BGFX_CAPS_VIDEO_CODEC_CHROMA_422 }, { &D3D12_VIDEO_DECODE_PROFILE_AV1_12BIT_PROFILE2, VideoCodec::AV1, BGFX_CAPS_VIDEO_CODEC_BIT_12 | BGFX_CAPS_VIDEO_CODEC_CHROMA_420 | BGFX_CAPS_VIDEO_CODEC_CHROMA_422 }, { &D3D12_VIDEO_DECODE_PROFILE_AV1_12BIT_PROFILE2_420, VideoCodec::AV1, BGFX_CAPS_VIDEO_CODEC_BIT_12 | BGFX_CAPS_VIDEO_CODEC_CHROMA_420 }, }; bool anySupported = false; for (UINT ii = 0; ii < profileList.ProfileCount; ++ii) { for (uint32_t jj = 0; jj < BX_COUNTOF(s_profiles); ++jj) { if (0 != bx::memCmp(&profiles[ii], s_profiles[jj].guid, sizeof(GUID) ) ) { continue; } D3D12_FEATURE_DATA_VIDEO_DECODE_SUPPORT support { .NodeIndex = 0, .Configuration = { .DecodeProfile = profiles[ii], .BitstreamEncryption = D3D12_BITSTREAM_ENCRYPTION_TYPE_NONE, .InterlaceType = D3D12_VIDEO_FRAME_CODED_INTERLACE_TYPE_NONE, }, .Width = 1920, .Height = 1080, .DecodeFormat = (s_profiles[jj].caps & BGFX_CAPS_VIDEO_CODEC_BIT_10) ? DXGI_FORMAT_P010 : DXGI_FORMAT_NV12, .FrameRate = { .Numerator = 30, .Denominator = 1 }, .BitRate = 0, .SupportFlags = D3D12_VIDEO_DECODE_SUPPORT_FLAG_NONE, .ConfigurationFlags = D3D12_VIDEO_DECODE_CONFIGURATION_FLAG_NONE, .DecodeTier = D3D12_VIDEO_DECODE_TIER_NOT_SUPPORTED, }; hr = videoDevice->CheckFeatureSupport( D3D12_FEATURE_VIDEO_DECODE_SUPPORT , &support , sizeof(support) ); if (SUCCEEDED(hr) && 0 != (support.SupportFlags & D3D12_VIDEO_DECODE_SUPPORT_FLAG_SUPPORTED) ) { g_caps.codecs[s_profiles[jj].codec] |= s_profiles[jj].caps; anySupported = true; } break; } } if (anySupported) { g_caps.supported |= BGFX_CAPS_VIDEO_DECODE; const TextureFormat::Enum dstFormats[] = { TextureFormat::BGRA8, TextureFormat::RGBA8, TextureFormat::RGB10A2, TextureFormat::RGBA16F, }; for (uint32_t ii = 0; ii < BX_COUNTOF(dstFormats); ++ii) { const TextureFormat::Enum fmt = dstFormats[ii]; if (0 != (g_caps.formats[fmt] & BGFX_CAPS_FORMAT_TEXTURE_2D) ) { g_caps.formats[fmt] |= BGFX_CAPS_FORMAT_TEXTURE_VIDEO_DECODE_DST; } } } DX_RELEASE_I(videoDevice); } } } // namespace bgfx::d3d12 # else namespace bgfx { namespace d3d12 { void initVideoDecoder(const VideoBindingD3D12& _binding) { BX_UNUSED(_binding); } VideoDecoderD3D12* videoDecoderCreate(const VideoDecoderInit& _init, RendererContextD3D12* _renderer, uint16_t _width, uint16_t _height) { BX_UNUSED(_init, _renderer, _width, _height); return NULL; } void videoDecoderDestroy(VideoDecoderD3D12* _decoder) { BX_UNUSED(_decoder); } bool videoDecoderDecode(VideoDecoderD3D12* _decoder, const VideoDecoderFrame& _frame, TextureD3D12& _dst) { BX_UNUSED(_decoder, _frame, _dst); return false; } } } // namespace bgfx::d3d12 # endif // BGFX_CONFIG_VIDEO_DIRECT3D12 #endif // BGFX_CONFIG_RENDERER_DIRECT3D12