#include #include #include #include namespace XCEngine::XCUI::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(hr)); return buffer; } } // namespace bool NativeRenderer::Initialize(HWND hwnd) { Shutdown(); if (hwnd == nullptr) { return false; } m_hwnd = hwnd; if (FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, m_d2dFactory.ReleaseAndGetAddressOf()))) { Shutdown(); return false; } if (FAILED(DWriteCreateFactory( DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(m_dwriteFactory.ReleaseAndGetAddressOf())))) { Shutdown(); return false; } 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()) { return false; } const bool rendered = RenderToTarget(*m_renderTarget.Get(), *m_solidBrush.Get(), drawData); const HRESULT hr = m_renderTarget->EndDraw(); if (hr == D2DERR_RECREATE_TARGET) { DiscardRenderTarget(); return false; } return rendered && SUCCEEDED(hr); } 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 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 offscreenRenderTarget; hr = m_d2dFactory->CreateWicBitmapRenderTarget( bitmap.Get(), renderTargetProperties, offscreenRenderTarget.ReleaseAndGetAddressOf()); if (FAILED(hr)) { outError = HrToString("ID2D1Factory::CreateWicBitmapRenderTarget", hr); return false; } Microsoft::WRL::ComPtr 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 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 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 frame; Microsoft::WRL::ComPtr 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) { 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((std::max)(clientRect.right - clientRect.left, 1L)); const UINT height = static_cast((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)); if (FAILED(m_d2dFactory->CreateHwndRenderTarget( renderTargetProps, hwndProps, m_renderTarget.ReleaseAndGetAddressOf()))) { return false; } if (FAILED(m_renderTarget->CreateSolidColorBrush( D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f), m_solidBrush.ReleaseAndGetAddressOf()))) { DiscardRenderTarget(); return false; } m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); 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 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& 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(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(std::lround(fontSize * 10.0f)); const auto found = m_textFormats.find(key); if (found != m_textFormats.end()) { return found->second.Get(); } Microsoft::WRL::ComPtr 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(text.size()), nullptr, 0); if (sizeNeeded <= 0) { return {}; } std::wstring wideText(static_cast(sizeNeeded), L'\0'); MultiByteToWideChar( CP_UTF8, 0, text.data(), static_cast(text.size()), wideText.data(), sizeNeeded); return wideText; } } // namespace XCEngine::XCUI::Host