Files
XCEngine/editor/app/Rendering/D3D12/D3D12WindowCapture.cpp
2026-04-25 16:46:01 +08:00

442 lines
14 KiB
C++

#include "D3D12WindowCapture.h"
#include "D3D12WindowRenderer.h"
#include <XCEngine/RHI/D3D12/D3D12Texture.h>
#include <d3d12.h>
#include <cstring>
#include <sstream>
#include <vector>
namespace XCEngine::UI::Editor::Host {
using ::XCEngine::RHI::D3D12Texture;
using Microsoft::WRL::ComPtr;
namespace {
std::string HrToString(const char* operation, HRESULT hr) {
std::ostringstream stream = {};
stream << operation << " failed with HRESULT 0x"
<< std::hex << std::uppercase
<< static_cast<unsigned long>(hr);
return stream.str();
}
bool ReadbackTexturePixels(
ID3D12Device& device,
ID3D12CommandQueue& commandQueue,
ID3D12Resource& sourceResource,
UINT width,
UINT height,
std::vector<std::uint8_t>& outPixels,
std::string& outError) {
outPixels.clear();
const D3D12_RESOURCE_DESC sourceDesc = sourceResource.GetDesc();
D3D12_PLACED_SUBRESOURCE_FOOTPRINT readbackLayout = {};
UINT rowCount = 0u;
UINT64 rowSizeInBytes = 0u;
UINT64 totalSize = 0u;
device.GetCopyableFootprints(
&sourceDesc,
0u,
1u,
0u,
&readbackLayout,
&rowCount,
&rowSizeInBytes,
&totalSize);
if (totalSize == 0u || rowCount == 0u || rowSizeInBytes == 0u) {
outError = "GetCopyableFootprints returned an empty readback layout.";
return false;
}
D3D12_HEAP_PROPERTIES heapProperties = {};
heapProperties.Type = D3D12_HEAP_TYPE_READBACK;
D3D12_RESOURCE_DESC readbackDesc = {};
readbackDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
readbackDesc.Alignment = 0u;
readbackDesc.Width = totalSize;
readbackDesc.Height = 1u;
readbackDesc.DepthOrArraySize = 1u;
readbackDesc.MipLevels = 1u;
readbackDesc.Format = DXGI_FORMAT_UNKNOWN;
readbackDesc.SampleDesc.Count = 1u;
readbackDesc.SampleDesc.Quality = 0u;
readbackDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
readbackDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
ComPtr<ID3D12Resource> readbackBuffer = {};
HRESULT hr = device.CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&readbackDesc,
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(readbackBuffer.ReleaseAndGetAddressOf()));
if (FAILED(hr) || readbackBuffer == nullptr) {
outError = HrToString("ID3D12Device::CreateCommittedResource(readback)", hr);
return false;
}
ComPtr<ID3D12CommandAllocator> commandAllocator = {};
hr = device.CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(commandAllocator.ReleaseAndGetAddressOf()));
if (FAILED(hr) || commandAllocator == nullptr) {
outError = HrToString("ID3D12Device::CreateCommandAllocator(capture)", hr);
return false;
}
ComPtr<ID3D12GraphicsCommandList> commandList = {};
hr = device.CreateCommandList(
0u,
D3D12_COMMAND_LIST_TYPE_DIRECT,
commandAllocator.Get(),
nullptr,
IID_PPV_ARGS(commandList.ReleaseAndGetAddressOf()));
if (FAILED(hr) || commandList == nullptr) {
outError = HrToString("ID3D12Device::CreateCommandList(capture)", hr);
return false;
}
D3D12_RESOURCE_BARRIER toCopyBarrier = {};
toCopyBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
toCopyBarrier.Transition.pResource = &sourceResource;
toCopyBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
toCopyBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE;
toCopyBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
commandList->ResourceBarrier(1u, &toCopyBarrier);
D3D12_TEXTURE_COPY_LOCATION sourceLocation = {};
sourceLocation.pResource = &sourceResource;
sourceLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
sourceLocation.SubresourceIndex = 0u;
D3D12_TEXTURE_COPY_LOCATION destinationLocation = {};
destinationLocation.pResource = readbackBuffer.Get();
destinationLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
destinationLocation.PlacedFootprint = readbackLayout;
D3D12_BOX sourceRegion = {};
sourceRegion.left = 0u;
sourceRegion.top = 0u;
sourceRegion.front = 0u;
sourceRegion.right = width;
sourceRegion.bottom = height;
sourceRegion.back = 1u;
commandList->CopyTextureRegion(
&destinationLocation,
0u,
0u,
0u,
&sourceLocation,
&sourceRegion);
D3D12_RESOURCE_BARRIER restoreBarrier = {};
restoreBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
restoreBarrier.Transition.pResource = &sourceResource;
restoreBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE;
restoreBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
restoreBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
commandList->ResourceBarrier(1u, &restoreBarrier);
hr = commandList->Close();
if (FAILED(hr)) {
outError = HrToString("ID3D12GraphicsCommandList::Close(capture)", hr);
return false;
}
ID3D12CommandList* commandLists[] = { commandList.Get() };
commandQueue.ExecuteCommandLists(1u, commandLists);
ComPtr<ID3D12Fence> fence = {};
hr = device.CreateFence(0u, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(fence.ReleaseAndGetAddressOf()));
if (FAILED(hr) || fence == nullptr) {
outError = HrToString("ID3D12Device::CreateFence(capture)", hr);
return false;
}
const HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (fenceEvent == nullptr) {
outError = "CreateEvent failed for D3D12 capture synchronization.";
return false;
}
constexpr UINT64 kFenceValue = 1u;
hr = commandQueue.Signal(fence.Get(), kFenceValue);
if (FAILED(hr)) {
CloseHandle(fenceEvent);
outError = HrToString("ID3D12CommandQueue::Signal(capture)", hr);
return false;
}
if (fence->GetCompletedValue() < kFenceValue) {
hr = fence->SetEventOnCompletion(kFenceValue, fenceEvent);
if (FAILED(hr)) {
CloseHandle(fenceEvent);
outError = HrToString("ID3D12Fence::SetEventOnCompletion(capture)", hr);
return false;
}
WaitForSingleObject(fenceEvent, INFINITE);
}
CloseHandle(fenceEvent);
const UINT packedRowPitch = width * 4u;
outPixels.resize(static_cast<std::size_t>(packedRowPitch) * static_cast<std::size_t>(height));
D3D12_RANGE readRange = { 0u, static_cast<SIZE_T>(totalSize) };
void* mappedData = nullptr;
hr = readbackBuffer->Map(0u, &readRange, &mappedData);
if (FAILED(hr) || mappedData == nullptr) {
outError = HrToString("ID3D12Resource::Map(readback)", hr);
outPixels.clear();
return false;
}
const auto* sourceBytes = static_cast<const std::uint8_t*>(mappedData);
for (UINT rowIndex = 0u; rowIndex < height; ++rowIndex) {
std::memcpy(
outPixels.data() + static_cast<std::size_t>(rowIndex) * packedRowPitch,
sourceBytes + static_cast<std::size_t>(rowIndex) * readbackLayout.Footprint.RowPitch,
packedRowPitch);
}
D3D12_RANGE writeRange = { 0u, 0u };
readbackBuffer->Unmap(0u, &writeRange);
return true;
}
void ConvertRgbaPixelsToBgra(std::vector<std::uint8_t>& pixels) {
const std::size_t pixelCount = pixels.size() / 4u;
for (std::size_t pixelIndex = 0u; pixelIndex < pixelCount; ++pixelIndex) {
const std::size_t baseIndex = pixelIndex * 4u;
std::swap(pixels[baseIndex + 0u], pixels[baseIndex + 2u]);
}
}
} // namespace
bool D3D12WindowCapture::CaptureCurrentBackBufferToPng(
const D3D12WindowRenderer& windowRenderer,
const std::filesystem::path& outputPath,
std::string& outError) {
outError.clear();
if (outputPath.empty()) {
outError = "CaptureCurrentBackBufferToPng rejected an empty output path.";
return false;
}
ID3D12Device* device = windowRenderer.GetDevice();
ID3D12CommandQueue* commandQueue = windowRenderer.GetCommandQueue();
const D3D12Texture* backBufferTexture = windowRenderer.GetCurrentBackBufferTexture();
if (device == nullptr || commandQueue == nullptr || backBufferTexture == nullptr ||
backBufferTexture->GetResource() == nullptr) {
outError = "CaptureCurrentBackBufferToPng requires an active D3D12 backbuffer.";
return false;
}
const DXGI_FORMAT backBufferFormat = backBufferTexture->GetDesc().Format;
const bool requiresRgbaToBgraConversion =
backBufferFormat == DXGI_FORMAT_R8G8B8A8_UNORM;
if (!requiresRgbaToBgraConversion &&
backBufferFormat != DXGI_FORMAT_B8G8R8A8_UNORM) {
std::ostringstream stream = {};
stream << "Unsupported backbuffer format for PNG capture: "
<< static_cast<unsigned int>(backBufferFormat);
outError = stream.str();
return false;
}
if (!EnsureWicFactory(outError)) {
return false;
}
std::vector<std::uint8_t> pixels = {};
if (!ReadbackTexturePixels(
*device,
*commandQueue,
*backBufferTexture->GetResource(),
backBufferTexture->GetWidth(),
backBufferTexture->GetHeight(),
pixels,
outError)) {
return false;
}
if (requiresRgbaToBgraConversion) {
ConvertRgbaPixelsToBgra(pixels);
}
if (!EncodePng(
outputPath,
pixels.data(),
backBufferTexture->GetWidth(),
backBufferTexture->GetHeight(),
backBufferTexture->GetWidth() * 4u,
GUID_WICPixelFormat32bppBGRA,
outError)) {
return false;
}
std::error_code fileError = {};
const std::uintmax_t encodedFileSize = std::filesystem::file_size(outputPath, fileError);
if (fileError || encodedFileSize == 0u) {
outError = "Native D3D12 capture completed without producing a valid PNG file.";
return false;
}
return true;
}
void D3D12WindowCapture::Shutdown() {
m_wicFactory.Reset();
if (m_wicComInitialized) {
CoUninitialize();
m_wicComInitialized = false;
}
}
bool D3D12WindowCapture::EnsureWicFactory(std::string& outError) {
outError.clear();
if (m_wicFactory != nullptr) {
return true;
}
const HRESULT initHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (FAILED(initHr) && initHr != RPC_E_CHANGED_MODE) {
outError = HrToString("CoInitializeEx", initHr);
return false;
}
if (SUCCEEDED(initHr)) {
m_wicComInitialized = true;
}
const HRESULT factoryHr = CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(m_wicFactory.ReleaseAndGetAddressOf()));
if (FAILED(factoryHr) || m_wicFactory == nullptr) {
outError = HrToString("CoCreateInstance(CLSID_WICImagingFactory)", factoryHr);
m_wicFactory.Reset();
return false;
}
return true;
}
bool D3D12WindowCapture::EncodePng(
const std::filesystem::path& outputPath,
const std::uint8_t* pixels,
UINT width,
UINT height,
UINT rowPitch,
REFWICPixelFormatGUID pixelFormat,
std::string& outError) const {
outError.clear();
if (m_wicFactory == nullptr || pixels == nullptr || width == 0u || height == 0u || rowPitch == 0u) {
outError = "EncodePng requires initialized WIC state and valid image data.";
return false;
}
ComPtr<IWICStream> stream = {};
HRESULT hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf());
if (FAILED(hr) || stream == nullptr) {
outError = HrToString("IWICImagingFactory::CreateStream", hr);
return false;
}
const std::wstring wideOutputPath = outputPath.wstring();
hr = stream->InitializeFromFilename(wideOutputPath.c_str(), GENERIC_WRITE);
if (FAILED(hr)) {
outError = HrToString("IWICStream::InitializeFromFilename", hr);
return false;
}
ComPtr<IWICBitmapEncoder> encoder = {};
hr = m_wicFactory->CreateEncoder(
GUID_ContainerFormatPng,
nullptr,
encoder.ReleaseAndGetAddressOf());
if (FAILED(hr) || encoder == nullptr) {
outError = HrToString("IWICImagingFactory::CreateEncoder", hr);
return false;
}
hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache);
if (FAILED(hr)) {
outError = HrToString("IWICBitmapEncoder::Initialize", hr);
return false;
}
ComPtr<IWICBitmapFrameEncode> frame = {};
ComPtr<IPropertyBag2> propertyBag = {};
hr = encoder->CreateNewFrame(
frame.ReleaseAndGetAddressOf(),
propertyBag.ReleaseAndGetAddressOf());
if (FAILED(hr) || frame == nullptr) {
outError = HrToString("IWICBitmapEncoder::CreateNewFrame", hr);
return false;
}
hr = frame->Initialize(propertyBag.Get());
if (FAILED(hr)) {
outError = HrToString("IWICBitmapFrameEncode::Initialize", hr);
return false;
}
hr = frame->SetSize(width, height);
if (FAILED(hr)) {
outError = HrToString("IWICBitmapFrameEncode::SetSize", hr);
return false;
}
WICPixelFormatGUID resolvedPixelFormat = pixelFormat;
hr = frame->SetPixelFormat(&resolvedPixelFormat);
if (FAILED(hr)) {
outError = HrToString("IWICBitmapFrameEncode::SetPixelFormat", hr);
return false;
}
if (!IsEqualGUID(resolvedPixelFormat, pixelFormat)) {
outError = "IWICBitmapFrameEncode::SetPixelFormat resolved an unexpected pixel format.";
return false;
}
hr = frame->WritePixels(
height,
rowPitch,
rowPitch * height,
const_cast<BYTE*>(reinterpret_cast<const BYTE*>(pixels)));
if (FAILED(hr)) {
outError = HrToString("IWICBitmapFrameEncode::WritePixels", hr);
return false;
}
hr = frame->Commit();
if (FAILED(hr)) {
outError = HrToString("IWICBitmapFrameEncode::Commit", hr);
return false;
}
hr = encoder->Commit();
if (FAILED(hr)) {
outError = HrToString("IWICBitmapEncoder::Commit", hr);
return false;
}
hr = stream->Commit(STGC_DEFAULT);
if (FAILED(hr)) {
outError = HrToString("IWICStream::Commit", hr);
return false;
}
return true;
}
} // namespace XCEngine::UI::Editor::Host