512 lines
15 KiB
C++
512 lines
15 KiB
C++
#include "NativeRenderer.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
|
|
namespace XCEngine::UI::Editor::Host {
|
|
|
|
namespace {
|
|
|
|
D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect) {
|
|
return D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
|
|
}
|
|
|
|
std::string HrToString(const char* operation, HRESULT hr) {
|
|
char buffer[128] = {};
|
|
sprintf_s(buffer, "%s failed with hr=0x%08X.", operation, static_cast<unsigned int>(hr));
|
|
return buffer;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool NativeRenderer::Initialize(HWND hwnd) {
|
|
Shutdown();
|
|
|
|
if (hwnd == nullptr) {
|
|
m_lastRenderError = "Initialize rejected a null hwnd.";
|
|
return false;
|
|
}
|
|
|
|
m_hwnd = hwnd;
|
|
HRESULT hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, m_d2dFactory.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr)) {
|
|
m_lastRenderError = HrToString("D2D1CreateFactory", hr);
|
|
Shutdown();
|
|
return false;
|
|
}
|
|
|
|
hr = DWriteCreateFactory(
|
|
DWRITE_FACTORY_TYPE_SHARED,
|
|
__uuidof(IDWriteFactory),
|
|
reinterpret_cast<IUnknown**>(m_dwriteFactory.ReleaseAndGetAddressOf()));
|
|
if (FAILED(hr)) {
|
|
m_lastRenderError = HrToString("DWriteCreateFactory", hr);
|
|
Shutdown();
|
|
return false;
|
|
}
|
|
|
|
m_lastRenderError.clear();
|
|
return EnsureRenderTarget();
|
|
}
|
|
|
|
void NativeRenderer::Shutdown() {
|
|
m_textFormats.clear();
|
|
m_solidBrush.Reset();
|
|
m_renderTarget.Reset();
|
|
m_wicFactory.Reset();
|
|
m_dwriteFactory.Reset();
|
|
m_d2dFactory.Reset();
|
|
if (m_wicComInitialized) {
|
|
CoUninitialize();
|
|
m_wicComInitialized = false;
|
|
}
|
|
m_hwnd = nullptr;
|
|
}
|
|
|
|
void NativeRenderer::Resize(UINT width, UINT height) {
|
|
if (!m_renderTarget || width == 0 || height == 0) {
|
|
return;
|
|
}
|
|
|
|
const HRESULT hr = m_renderTarget->Resize(D2D1::SizeU(width, height));
|
|
if (hr == D2DERR_RECREATE_TARGET) {
|
|
DiscardRenderTarget();
|
|
}
|
|
}
|
|
|
|
bool NativeRenderer::Render(const ::XCEngine::UI::UIDrawData& drawData) {
|
|
if (!EnsureRenderTarget()) {
|
|
if (m_lastRenderError.empty()) {
|
|
m_lastRenderError = "EnsureRenderTarget failed.";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const bool rendered = RenderToTarget(*m_renderTarget.Get(), *m_solidBrush.Get(), drawData);
|
|
const HRESULT hr = m_renderTarget->EndDraw();
|
|
if (hr == D2DERR_RECREATE_TARGET) {
|
|
m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr);
|
|
DiscardRenderTarget();
|
|
return false;
|
|
}
|
|
|
|
if (!rendered || FAILED(hr)) {
|
|
m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr);
|
|
return false;
|
|
}
|
|
|
|
m_lastRenderError.clear();
|
|
return true;
|
|
}
|
|
|
|
const std::string& NativeRenderer::GetLastRenderError() const {
|
|
return m_lastRenderError;
|
|
}
|
|
|
|
bool NativeRenderer::CaptureToPng(
|
|
const ::XCEngine::UI::UIDrawData& drawData,
|
|
UINT width,
|
|
UINT height,
|
|
const std::filesystem::path& outputPath,
|
|
std::string& outError) {
|
|
outError.clear();
|
|
if (width == 0 || height == 0) {
|
|
outError = "CaptureToPng rejected an empty render size.";
|
|
return false;
|
|
}
|
|
|
|
if (!m_d2dFactory || !m_dwriteFactory) {
|
|
outError = "CaptureToPng requires an initialized NativeRenderer.";
|
|
return false;
|
|
}
|
|
|
|
if (!EnsureWicFactory(outError)) {
|
|
return false;
|
|
}
|
|
|
|
std::error_code errorCode = {};
|
|
std::filesystem::create_directories(outputPath.parent_path(), errorCode);
|
|
if (errorCode) {
|
|
outError = "Failed to create screenshot directory: " + outputPath.parent_path().string();
|
|
return false;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IWICBitmap> bitmap;
|
|
HRESULT hr = m_wicFactory->CreateBitmap(
|
|
width,
|
|
height,
|
|
GUID_WICPixelFormat32bppPBGRA,
|
|
WICBitmapCacheOnLoad,
|
|
bitmap.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("IWICImagingFactory::CreateBitmap", hr);
|
|
return false;
|
|
}
|
|
|
|
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties(
|
|
D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
|
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED));
|
|
|
|
Microsoft::WRL::ComPtr<ID2D1RenderTarget> offscreenRenderTarget;
|
|
hr = m_d2dFactory->CreateWicBitmapRenderTarget(
|
|
bitmap.Get(),
|
|
renderTargetProperties,
|
|
offscreenRenderTarget.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("ID2D1Factory::CreateWicBitmapRenderTarget", hr);
|
|
return false;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> offscreenBrush;
|
|
hr = offscreenRenderTarget->CreateSolidColorBrush(
|
|
D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f),
|
|
offscreenBrush.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("ID2D1RenderTarget::CreateSolidColorBrush", hr);
|
|
return false;
|
|
}
|
|
|
|
const bool rendered = RenderToTarget(*offscreenRenderTarget.Get(), *offscreenBrush.Get(), drawData);
|
|
hr = offscreenRenderTarget->EndDraw();
|
|
if (!rendered || FAILED(hr)) {
|
|
outError = HrToString("ID2D1RenderTarget::EndDraw", hr);
|
|
return false;
|
|
}
|
|
|
|
const std::wstring wideOutputPath = outputPath.wstring();
|
|
Microsoft::WRL::ComPtr<IWICStream> stream;
|
|
hr = m_wicFactory->CreateStream(stream.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("IWICImagingFactory::CreateStream", hr);
|
|
return false;
|
|
}
|
|
|
|
hr = stream->InitializeFromFilename(wideOutputPath.c_str(), GENERIC_WRITE);
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("IWICStream::InitializeFromFilename", hr);
|
|
return false;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IWICBitmapEncoder> encoder;
|
|
hr = m_wicFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("IWICImagingFactory::CreateEncoder", hr);
|
|
return false;
|
|
}
|
|
|
|
hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache);
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("IWICBitmapEncoder::Initialize", hr);
|
|
return false;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IWICBitmapFrameEncode> frame;
|
|
Microsoft::WRL::ComPtr<IPropertyBag2> propertyBag;
|
|
hr = encoder->CreateNewFrame(frame.ReleaseAndGetAddressOf(), propertyBag.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr)) {
|
|
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 pixelFormat = GUID_WICPixelFormat32bppPBGRA;
|
|
hr = frame->SetPixelFormat(&pixelFormat);
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("IWICBitmapFrameEncode::SetPixelFormat", hr);
|
|
return false;
|
|
}
|
|
|
|
hr = frame->WriteSource(bitmap.Get(), nullptr);
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("IWICBitmapFrameEncode::WriteSource", 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;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NativeRenderer::EnsureRenderTarget() {
|
|
if (!m_hwnd || !m_d2dFactory || !m_dwriteFactory) {
|
|
m_lastRenderError = "EnsureRenderTarget requires hwnd, D2D factory, and DWrite factory.";
|
|
return false;
|
|
}
|
|
|
|
return CreateDeviceResources();
|
|
}
|
|
|
|
bool NativeRenderer::EnsureWicFactory(std::string& outError) {
|
|
outError.clear();
|
|
if (m_wicFactory) {
|
|
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)) {
|
|
outError = HrToString("CoCreateInstance(CLSID_WICImagingFactory)", factoryHr);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NativeRenderer::DiscardRenderTarget() {
|
|
m_solidBrush.Reset();
|
|
m_renderTarget.Reset();
|
|
}
|
|
|
|
bool NativeRenderer::CreateDeviceResources() {
|
|
if (m_renderTarget) {
|
|
return true;
|
|
}
|
|
|
|
RECT clientRect = {};
|
|
GetClientRect(m_hwnd, &clientRect);
|
|
const UINT width = static_cast<UINT>((std::max)(clientRect.right - clientRect.left, 1L));
|
|
const UINT height = static_cast<UINT>((std::max)(clientRect.bottom - clientRect.top, 1L));
|
|
|
|
const D2D1_RENDER_TARGET_PROPERTIES renderTargetProps = D2D1::RenderTargetProperties();
|
|
const D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = D2D1::HwndRenderTargetProperties(
|
|
m_hwnd,
|
|
D2D1::SizeU(width, height));
|
|
|
|
const HRESULT renderTargetHr = m_d2dFactory->CreateHwndRenderTarget(
|
|
renderTargetProps,
|
|
hwndProps,
|
|
m_renderTarget.ReleaseAndGetAddressOf());
|
|
if (FAILED(renderTargetHr)) {
|
|
m_lastRenderError = HrToString("ID2D1Factory::CreateHwndRenderTarget", renderTargetHr);
|
|
return false;
|
|
}
|
|
|
|
const HRESULT brushHr = m_renderTarget->CreateSolidColorBrush(
|
|
D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f),
|
|
m_solidBrush.ReleaseAndGetAddressOf());
|
|
if (FAILED(brushHr)) {
|
|
m_lastRenderError = HrToString("ID2D1HwndRenderTarget::CreateSolidColorBrush", brushHr);
|
|
DiscardRenderTarget();
|
|
return false;
|
|
}
|
|
|
|
m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
|
m_lastRenderError.clear();
|
|
return true;
|
|
}
|
|
|
|
bool NativeRenderer::RenderToTarget(
|
|
ID2D1RenderTarget& renderTarget,
|
|
ID2D1SolidColorBrush& solidBrush,
|
|
const ::XCEngine::UI::UIDrawData& drawData) {
|
|
renderTarget.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
|
renderTarget.BeginDraw();
|
|
renderTarget.Clear(D2D1::ColorF(0.04f, 0.05f, 0.06f, 1.0f));
|
|
|
|
std::vector<D2D1_RECT_F> clipStack = {};
|
|
for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
|
|
for (const ::XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
|
|
RenderCommand(renderTarget, solidBrush, command, clipStack);
|
|
}
|
|
}
|
|
|
|
while (!clipStack.empty()) {
|
|
renderTarget.PopAxisAlignedClip();
|
|
clipStack.pop_back();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NativeRenderer::RenderCommand(
|
|
ID2D1RenderTarget& renderTarget,
|
|
ID2D1SolidColorBrush& solidBrush,
|
|
const ::XCEngine::UI::UIDrawCommand& command,
|
|
std::vector<D2D1_RECT_F>& clipStack) {
|
|
solidBrush.SetColor(ToD2DColor(command.color));
|
|
|
|
switch (command.type) {
|
|
case ::XCEngine::UI::UIDrawCommandType::FilledRect: {
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
|
if (command.rounding > 0.0f) {
|
|
renderTarget.FillRoundedRectangle(
|
|
D2D1::RoundedRect(rect, command.rounding, command.rounding),
|
|
&solidBrush);
|
|
} else {
|
|
renderTarget.FillRectangle(rect, &solidBrush);
|
|
}
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::RectOutline: {
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
|
const float thickness = command.thickness > 0.0f ? command.thickness : 1.0f;
|
|
if (command.rounding > 0.0f) {
|
|
renderTarget.DrawRoundedRectangle(
|
|
D2D1::RoundedRect(rect, command.rounding, command.rounding),
|
|
&solidBrush,
|
|
thickness);
|
|
} else {
|
|
renderTarget.DrawRectangle(rect, &solidBrush, thickness);
|
|
}
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::Text: {
|
|
if (command.text.empty()) {
|
|
break;
|
|
}
|
|
|
|
const float fontSize = command.fontSize > 0.0f ? command.fontSize : 16.0f;
|
|
IDWriteTextFormat* textFormat = GetTextFormat(fontSize);
|
|
if (textFormat == nullptr) {
|
|
break;
|
|
}
|
|
|
|
const std::wstring text = Utf8ToWide(command.text);
|
|
if (text.empty()) {
|
|
break;
|
|
}
|
|
|
|
const D2D1_SIZE_F targetSize = renderTarget.GetSize();
|
|
const D2D1_RECT_F layoutRect = D2D1::RectF(
|
|
command.position.x,
|
|
command.position.y,
|
|
targetSize.width,
|
|
command.position.y + fontSize * 1.8f);
|
|
renderTarget.DrawTextW(
|
|
text.c_str(),
|
|
static_cast<UINT32>(text.size()),
|
|
textFormat,
|
|
layoutRect,
|
|
&solidBrush,
|
|
D2D1_DRAW_TEXT_OPTIONS_CLIP,
|
|
DWRITE_MEASURING_MODE_NATURAL);
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::Image: {
|
|
if (!command.texture.IsValid()) {
|
|
break;
|
|
}
|
|
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
|
renderTarget.DrawRectangle(rect, &solidBrush, 1.0f);
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::PushClipRect: {
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect);
|
|
renderTarget.PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
|
clipStack.push_back(rect);
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::PopClipRect: {
|
|
if (!clipStack.empty()) {
|
|
renderTarget.PopAxisAlignedClip();
|
|
clipStack.pop_back();
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
|
|
if (!m_dwriteFactory) {
|
|
return nullptr;
|
|
}
|
|
|
|
const int key = static_cast<int>(std::lround(fontSize * 10.0f));
|
|
const auto found = m_textFormats.find(key);
|
|
if (found != m_textFormats.end()) {
|
|
return found->second.Get();
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IDWriteTextFormat> textFormat;
|
|
const HRESULT hr = m_dwriteFactory->CreateTextFormat(
|
|
L"Segoe UI",
|
|
nullptr,
|
|
DWRITE_FONT_WEIGHT_REGULAR,
|
|
DWRITE_FONT_STYLE_NORMAL,
|
|
DWRITE_FONT_STRETCH_NORMAL,
|
|
fontSize,
|
|
L"",
|
|
textFormat.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr)) {
|
|
return nullptr;
|
|
}
|
|
|
|
textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
|
|
textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
|
|
textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP);
|
|
|
|
IDWriteTextFormat* result = textFormat.Get();
|
|
m_textFormats.emplace(key, std::move(textFormat));
|
|
return result;
|
|
}
|
|
|
|
D2D1_COLOR_F NativeRenderer::ToD2DColor(const ::XCEngine::UI::UIColor& color) {
|
|
return D2D1::ColorF(color.r, color.g, color.b, color.a);
|
|
}
|
|
|
|
std::wstring NativeRenderer::Utf8ToWide(std::string_view text) {
|
|
if (text.empty()) {
|
|
return {};
|
|
}
|
|
|
|
const int sizeNeeded = MultiByteToWideChar(
|
|
CP_UTF8,
|
|
0,
|
|
text.data(),
|
|
static_cast<int>(text.size()),
|
|
nullptr,
|
|
0);
|
|
if (sizeNeeded <= 0) {
|
|
return {};
|
|
}
|
|
|
|
std::wstring wideText(static_cast<size_t>(sizeNeeded), L'\0');
|
|
MultiByteToWideChar(
|
|
CP_UTF8,
|
|
0,
|
|
text.data(),
|
|
static_cast<int>(text.size()),
|
|
wideText.data(),
|
|
sizeNeeded);
|
|
return wideText;
|
|
}
|
|
|
|
} // namespace XCEngine::UI::Editor::Host
|