1402 lines
47 KiB
C++
1402 lines
47 KiB
C++
#include "NativeRenderer.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <memory>
|
|
|
|
namespace XCEngine::UI::Editor::Host {
|
|
|
|
namespace {
|
|
|
|
constexpr float kBaseDpi = 96.0f;
|
|
constexpr float kDefaultFontSize = 16.0f;
|
|
|
|
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;
|
|
}
|
|
|
|
float ClampDpiScale(float dpiScale) {
|
|
return dpiScale > 0.0f ? dpiScale : 1.0f;
|
|
}
|
|
|
|
float ResolveFontSize(float fontSize) {
|
|
return fontSize > 0.0f ? fontSize : kDefaultFontSize;
|
|
}
|
|
|
|
float SnapToPixel(float value, float dpiScale) {
|
|
const float scale = ClampDpiScale(dpiScale);
|
|
return std::round(value * scale);
|
|
}
|
|
|
|
D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect, float dpiScale) {
|
|
const float left = SnapToPixel(rect.x, dpiScale);
|
|
const float top = SnapToPixel(rect.y, dpiScale);
|
|
const float right = SnapToPixel(rect.x + rect.width, dpiScale);
|
|
const float bottom = SnapToPixel(rect.y + rect.height, dpiScale);
|
|
return D2D1::RectF(left, top, right, bottom);
|
|
}
|
|
|
|
D2D1_POINT_2F ToD2DPoint(
|
|
const ::XCEngine::UI::UIPoint& point,
|
|
float dpiScale,
|
|
float pixelOffset = 0.0f) {
|
|
return D2D1::Point2F(
|
|
SnapToPixel(point.x, dpiScale) + pixelOffset,
|
|
SnapToPixel(point.y, dpiScale) + pixelOffset);
|
|
}
|
|
|
|
float ResolveStrokePixelOffset(float thickness) {
|
|
const float roundedThickness = std::round(thickness);
|
|
return std::fmod(roundedThickness, 2.0f) == 1.0f ? 0.5f : 0.0f;
|
|
}
|
|
|
|
D2D1_BITMAP_PROPERTIES1 BuildD2DBitmapProperties(
|
|
DXGI_FORMAT format,
|
|
D2D1_BITMAP_OPTIONS options,
|
|
D2D1_ALPHA_MODE alphaMode = D2D1_ALPHA_MODE_IGNORE) {
|
|
return D2D1::BitmapProperties1(
|
|
options,
|
|
D2D1::PixelFormat(format, alphaMode),
|
|
kBaseDpi,
|
|
kBaseDpi);
|
|
}
|
|
|
|
bool IsInteropTextureHandle(const ::XCEngine::UI::UITextureHandle& texture) {
|
|
return texture.kind == ::XCEngine::UI::UITextureHandleKind::ShaderResourceView &&
|
|
texture.resourceHandle != 0u;
|
|
}
|
|
|
|
bool CollectInteropTextureHandles(
|
|
const ::XCEngine::UI::UIDrawData& drawData,
|
|
std::vector<::XCEngine::UI::UITextureHandle>& outTextures) {
|
|
outTextures.clear();
|
|
std::unordered_set<std::uintptr_t> seenKeys = {};
|
|
for (const ::XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
|
|
for (const ::XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
|
|
if (!IsInteropTextureHandle(command.texture) ||
|
|
!seenKeys.insert(command.texture.resourceHandle).second) {
|
|
continue;
|
|
}
|
|
|
|
outTextures.push_back(command.texture);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool NativeRenderer::Initialize(HWND hwnd) {
|
|
Shutdown();
|
|
|
|
if (hwnd == nullptr) {
|
|
m_lastRenderError = "Initialize rejected a null hwnd.";
|
|
return false;
|
|
}
|
|
|
|
m_hwnd = hwnd;
|
|
D2D1_FACTORY_OPTIONS factoryOptions = {};
|
|
#ifdef _DEBUG
|
|
factoryOptions.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
|
|
#endif
|
|
HRESULT hr = D2D1CreateFactory(
|
|
D2D1_FACTORY_TYPE_SINGLE_THREADED,
|
|
__uuidof(ID2D1Factory1),
|
|
&factoryOptions,
|
|
reinterpret_cast<void**>(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 true;
|
|
}
|
|
|
|
void NativeRenderer::Shutdown() {
|
|
DetachWindowRenderer();
|
|
while (!m_liveTextures.empty()) {
|
|
auto it = m_liveTextures.begin();
|
|
delete *it;
|
|
m_liveTextures.erase(it);
|
|
}
|
|
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::SetDpiScale(float dpiScale) {
|
|
m_dpiScale = ClampDpiScale(dpiScale);
|
|
if (m_renderTarget) {
|
|
m_renderTarget->SetDpi(kBaseDpi, kBaseDpi);
|
|
}
|
|
}
|
|
|
|
float NativeRenderer::GetDpiScale() const {
|
|
return m_dpiScale;
|
|
}
|
|
|
|
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::AttachWindowRenderer(D3D12WindowRenderer& windowRenderer) {
|
|
if (m_windowRenderer != &windowRenderer) {
|
|
ReleaseWindowRendererInterop();
|
|
m_windowRenderer = &windowRenderer;
|
|
}
|
|
|
|
if (!EnsureWindowRendererInterop()) {
|
|
return false;
|
|
}
|
|
|
|
// Keep a single real window presentation path. The hwnd render target is
|
|
// fallback-only and should not stay alive while D3D11On12 interop is healthy.
|
|
DiscardRenderTarget();
|
|
|
|
if (!m_backBufferInteropTargets.empty()) {
|
|
return true;
|
|
}
|
|
|
|
return RebuildBackBufferInteropTargets();
|
|
}
|
|
|
|
void NativeRenderer::DetachWindowRenderer() {
|
|
ReleaseWindowRendererInterop();
|
|
m_windowRenderer = nullptr;
|
|
}
|
|
|
|
void NativeRenderer::ReleaseWindowRendererBackBufferTargets() {
|
|
ClearActiveInteropSourceTextures();
|
|
if (m_d2dDeviceContext != nullptr) {
|
|
m_d2dDeviceContext->SetTarget(nullptr);
|
|
D2D1_TAG firstTag = 0u;
|
|
D2D1_TAG secondTag = 0u;
|
|
m_d2dDeviceContext->Flush(&firstTag, &secondTag);
|
|
}
|
|
if (m_d3d11DeviceContext != nullptr) {
|
|
m_d3d11DeviceContext->ClearState();
|
|
m_d3d11DeviceContext->Flush();
|
|
}
|
|
m_backBufferInteropTargets.clear();
|
|
}
|
|
|
|
bool NativeRenderer::RebuildWindowRendererBackBufferTargets() {
|
|
if (!EnsureWindowRendererInterop()) {
|
|
return false;
|
|
}
|
|
|
|
DiscardRenderTarget();
|
|
ReleaseWindowRendererBackBufferTargets();
|
|
return RebuildBackBufferInteropTargets();
|
|
}
|
|
|
|
bool NativeRenderer::HasAttachedWindowRenderer() const {
|
|
return m_windowRenderer != nullptr &&
|
|
m_d3d11On12Device != nullptr &&
|
|
m_d2dDeviceContext != nullptr &&
|
|
!m_backBufferInteropTargets.empty();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool NativeRenderer::RenderToWindowRenderer(const ::XCEngine::UI::UIDrawData& drawData) {
|
|
if (!EnsureWindowRendererInterop()) {
|
|
if (m_lastRenderError.empty()) {
|
|
m_lastRenderError = "Window renderer interop is not available.";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (m_backBufferInteropTargets.empty() &&
|
|
!RebuildBackBufferInteropTargets()) {
|
|
if (m_lastRenderError.empty()) {
|
|
m_lastRenderError = "Window renderer back buffer interop targets are unavailable.";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const std::uint32_t backBufferIndex =
|
|
m_windowRenderer->GetSwapChain() != nullptr
|
|
? m_windowRenderer->GetSwapChain()->GetCurrentBackBufferIndex()
|
|
: 0u;
|
|
if (backBufferIndex >= m_backBufferInteropTargets.size()) {
|
|
m_lastRenderError = "Back buffer interop target index is out of range.";
|
|
return false;
|
|
}
|
|
|
|
if (!m_windowRenderer->PreparePresentSurface()) {
|
|
m_lastRenderError = "Failed to prepare the D3D12 present surface: " +
|
|
m_windowRenderer->GetLastError();
|
|
return false;
|
|
}
|
|
|
|
if (!m_windowRenderer->SubmitFrame(false)) {
|
|
m_lastRenderError = "Failed to submit the D3D12 frame before UI composition.";
|
|
return false;
|
|
}
|
|
|
|
if (!PrepareActiveInteropSourceTextures(drawData)) {
|
|
ID3D11Resource* backBufferResource =
|
|
m_backBufferInteropTargets[backBufferIndex].wrappedResource.Get();
|
|
if (backBufferResource != nullptr) {
|
|
m_d3d11On12Device->AcquireWrappedResources(&backBufferResource, 1u);
|
|
m_d3d11On12Device->ReleaseWrappedResources(&backBufferResource, 1u);
|
|
}
|
|
m_d3d11DeviceContext->Flush();
|
|
ClearActiveInteropSourceTextures();
|
|
const bool signaled = m_windowRenderer->SignalFrameCompletion();
|
|
ReleaseWindowRendererInterop();
|
|
if (!signaled) {
|
|
m_lastRenderError =
|
|
"Failed to signal D3D12 frame completion after interop preparation failed.";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::vector<ID3D11Resource*> acquiredResources = {};
|
|
acquiredResources.reserve(1u + m_activeInteropSourceTextures.size());
|
|
acquiredResources.push_back(m_backBufferInteropTargets[backBufferIndex].wrappedResource.Get());
|
|
for (const D3D12SourceTextureInteropResource& texture : m_activeInteropSourceTextures) {
|
|
acquiredResources.push_back(texture.wrappedResource.Get());
|
|
}
|
|
|
|
m_d3d11On12Device->AcquireWrappedResources(
|
|
acquiredResources.data(),
|
|
static_cast<UINT>(acquiredResources.size()));
|
|
|
|
m_d2dDeviceContext->SetTarget(m_backBufferInteropTargets[backBufferIndex].targetBitmap.Get());
|
|
const bool rendered = RenderToTarget(*m_d2dDeviceContext.Get(), *m_interopBrush.Get(), drawData);
|
|
const HRESULT hr = m_d2dDeviceContext->EndDraw();
|
|
|
|
m_d3d11On12Device->ReleaseWrappedResources(
|
|
acquiredResources.data(),
|
|
static_cast<UINT>(acquiredResources.size()));
|
|
m_d3d11DeviceContext->Flush();
|
|
m_d2dDeviceContext->SetTarget(nullptr);
|
|
ClearActiveInteropSourceTextures();
|
|
|
|
if (!rendered || FAILED(hr)) {
|
|
m_lastRenderError = FAILED(hr)
|
|
? HrToString("ID2D1DeviceContext::EndDraw", hr)
|
|
: "RenderToTarget failed during D3D11On12 composition.";
|
|
const bool signaled = m_windowRenderer->SignalFrameCompletion();
|
|
ReleaseWindowRendererInterop();
|
|
if (!signaled) {
|
|
m_lastRenderError =
|
|
"Failed to signal D3D12 frame completion after UI composition failed.";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!m_windowRenderer->SignalFrameCompletion()) {
|
|
m_lastRenderError = "Failed to signal D3D12 frame completion after UI composition.";
|
|
ReleaseWindowRendererInterop();
|
|
return false;
|
|
}
|
|
if (!m_windowRenderer->PresentFrame()) {
|
|
m_lastRenderError = "Failed to present the D3D12 swap chain.";
|
|
ReleaseWindowRendererInterop();
|
|
return false;
|
|
}
|
|
|
|
m_lastRenderError.clear();
|
|
return true;
|
|
}
|
|
|
|
const std::string& NativeRenderer::GetLastRenderError() const {
|
|
return m_lastRenderError;
|
|
}
|
|
|
|
bool NativeRenderer::LoadTextureFromFile(
|
|
const std::filesystem::path& path,
|
|
::XCEngine::UI::UITextureHandle& outTexture,
|
|
std::string& outError) {
|
|
outError.clear();
|
|
ReleaseTexture(outTexture);
|
|
|
|
auto texture = std::make_unique<NativeTextureResource>();
|
|
if (!DecodeTextureFile(path, *texture, outError)) {
|
|
outTexture = {};
|
|
return false;
|
|
}
|
|
|
|
outTexture.nativeHandle = reinterpret_cast<std::uintptr_t>(texture.get());
|
|
outTexture.width = texture->width;
|
|
outTexture.height = texture->height;
|
|
outTexture.kind = ::XCEngine::UI::UITextureHandleKind::DescriptorHandle;
|
|
m_liveTextures.insert(texture.get());
|
|
texture.release();
|
|
return true;
|
|
}
|
|
|
|
void NativeRenderer::ReleaseTexture(::XCEngine::UI::UITextureHandle& texture) {
|
|
if (!texture.IsValid()) {
|
|
texture = {};
|
|
return;
|
|
}
|
|
|
|
auto* resource = reinterpret_cast<NativeTextureResource*>(texture.nativeHandle);
|
|
if (resource != nullptr) {
|
|
const auto found = m_liveTextures.find(resource);
|
|
if (found != m_liveTextures.end()) {
|
|
m_liveTextures.erase(found);
|
|
delete resource;
|
|
}
|
|
}
|
|
|
|
texture = {};
|
|
}
|
|
|
|
float NativeRenderer::MeasureTextWidth(
|
|
const ::XCEngine::UI::Editor::UIEditorTextMeasureRequest& request) const {
|
|
if (!m_dwriteFactory || request.text.empty()) {
|
|
return 0.0f;
|
|
}
|
|
|
|
const std::wstring text = Utf8ToWide(request.text);
|
|
if (text.empty()) {
|
|
return 0.0f;
|
|
}
|
|
|
|
const float dpiScale = ClampDpiScale(m_dpiScale);
|
|
const float scaledFontSize = ResolveFontSize(request.fontSize) * dpiScale;
|
|
IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize);
|
|
if (textFormat == nullptr) {
|
|
return 0.0f;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IDWriteTextLayout> textLayout;
|
|
HRESULT hr = m_dwriteFactory->CreateTextLayout(
|
|
text.c_str(),
|
|
static_cast<UINT32>(text.size()),
|
|
textFormat,
|
|
4096.0f,
|
|
scaledFontSize * 2.0f,
|
|
textLayout.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || !textLayout) {
|
|
return 0.0f;
|
|
}
|
|
|
|
DWRITE_TEXT_METRICS textMetrics = {};
|
|
hr = textLayout->GetMetrics(&textMetrics);
|
|
if (FAILED(hr)) {
|
|
return 0.0f;
|
|
}
|
|
|
|
DWRITE_OVERHANG_METRICS overhangMetrics = {};
|
|
float width = textMetrics.widthIncludingTrailingWhitespace;
|
|
if (SUCCEEDED(textLayout->GetOverhangMetrics(&overhangMetrics))) {
|
|
width += (std::max)(overhangMetrics.left, 0.0f);
|
|
width += (std::max)(overhangMetrics.right, 0.0f);
|
|
}
|
|
|
|
return std::ceil(width) / dpiScale;
|
|
}
|
|
|
|
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),
|
|
kBaseDpi,
|
|
kBaseDpi);
|
|
|
|
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::EnsureWindowRendererInterop() {
|
|
if (m_windowRenderer == nullptr) {
|
|
m_lastRenderError = "EnsureWindowRendererInterop requires an attached D3D12 window renderer.";
|
|
return false;
|
|
}
|
|
if (m_d2dFactory == nullptr || m_dwriteFactory == nullptr) {
|
|
m_lastRenderError = "EnsureWindowRendererInterop requires initialized D2D and DWrite factories.";
|
|
return false;
|
|
}
|
|
if (m_d3d11On12Device != nullptr &&
|
|
m_d2dDeviceContext != nullptr &&
|
|
m_interopBrush != nullptr) {
|
|
return true;
|
|
}
|
|
|
|
ReleaseWindowRendererInterop();
|
|
|
|
ID3D12Device* d3d12Device = m_windowRenderer->GetDevice();
|
|
ID3D12CommandQueue* d3d12CommandQueue = m_windowRenderer->GetCommandQueue();
|
|
if (d3d12Device == nullptr || d3d12CommandQueue == nullptr) {
|
|
m_lastRenderError = "The attached D3D12 window renderer does not expose a native device/queue.";
|
|
return false;
|
|
}
|
|
|
|
const std::array<D3D_FEATURE_LEVEL, 4> featureLevels = {
|
|
D3D_FEATURE_LEVEL_12_1,
|
|
D3D_FEATURE_LEVEL_12_0,
|
|
D3D_FEATURE_LEVEL_11_1,
|
|
D3D_FEATURE_LEVEL_11_0
|
|
};
|
|
const std::array<IUnknown*, 1> commandQueues = {
|
|
reinterpret_cast<IUnknown*>(d3d12CommandQueue)
|
|
};
|
|
|
|
UINT createFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
|
|
#ifdef _DEBUG
|
|
createFlags |= D3D11_CREATE_DEVICE_DEBUG;
|
|
#endif
|
|
|
|
D3D_FEATURE_LEVEL actualFeatureLevel = D3D_FEATURE_LEVEL_11_0;
|
|
HRESULT hr = D3D11On12CreateDevice(
|
|
d3d12Device,
|
|
createFlags,
|
|
featureLevels.data(),
|
|
static_cast<UINT>(featureLevels.size()),
|
|
commandQueues.data(),
|
|
static_cast<UINT>(commandQueues.size()),
|
|
0u,
|
|
m_d3d11Device.ReleaseAndGetAddressOf(),
|
|
m_d3d11DeviceContext.ReleaseAndGetAddressOf(),
|
|
&actualFeatureLevel);
|
|
#ifdef _DEBUG
|
|
if (FAILED(hr)) {
|
|
createFlags &= ~D3D11_CREATE_DEVICE_DEBUG;
|
|
hr = D3D11On12CreateDevice(
|
|
d3d12Device,
|
|
createFlags,
|
|
featureLevels.data(),
|
|
static_cast<UINT>(featureLevels.size()),
|
|
commandQueues.data(),
|
|
static_cast<UINT>(commandQueues.size()),
|
|
0u,
|
|
m_d3d11Device.ReleaseAndGetAddressOf(),
|
|
m_d3d11DeviceContext.ReleaseAndGetAddressOf(),
|
|
&actualFeatureLevel);
|
|
}
|
|
#endif
|
|
if (FAILED(hr) || m_d3d11Device == nullptr || m_d3d11DeviceContext == nullptr) {
|
|
m_lastRenderError = HrToString("D3D11On12CreateDevice", hr);
|
|
ReleaseWindowRendererInterop();
|
|
return false;
|
|
}
|
|
|
|
hr = m_d3d11Device.As(&m_d3d11On12Device);
|
|
if (FAILED(hr) || m_d3d11On12Device == nullptr) {
|
|
m_lastRenderError = HrToString("ID3D11Device::QueryInterface(ID3D11On12Device)", hr);
|
|
ReleaseWindowRendererInterop();
|
|
return false;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IDXGIDevice> dxgiDevice;
|
|
hr = m_d3d11Device.As(&dxgiDevice);
|
|
if (FAILED(hr) || dxgiDevice == nullptr) {
|
|
m_lastRenderError = HrToString("ID3D11Device::QueryInterface(IDXGIDevice)", hr);
|
|
ReleaseWindowRendererInterop();
|
|
return false;
|
|
}
|
|
|
|
hr = m_d2dFactory->CreateDevice(dxgiDevice.Get(), m_d2dDevice.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || m_d2dDevice == nullptr) {
|
|
m_lastRenderError = HrToString("ID2D1Factory1::CreateDevice", hr);
|
|
ReleaseWindowRendererInterop();
|
|
return false;
|
|
}
|
|
|
|
hr = m_d2dDevice->CreateDeviceContext(
|
|
D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
|
|
m_d2dDeviceContext.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || m_d2dDeviceContext == nullptr) {
|
|
m_lastRenderError = HrToString("ID2D1Device::CreateDeviceContext", hr);
|
|
ReleaseWindowRendererInterop();
|
|
return false;
|
|
}
|
|
|
|
hr = m_d2dDeviceContext->CreateSolidColorBrush(
|
|
D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f),
|
|
m_interopBrush.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || m_interopBrush == nullptr) {
|
|
m_lastRenderError = HrToString("ID2D1DeviceContext::CreateSolidColorBrush", hr);
|
|
ReleaseWindowRendererInterop();
|
|
return false;
|
|
}
|
|
|
|
m_d2dDeviceContext->SetDpi(kBaseDpi, kBaseDpi);
|
|
m_d2dDeviceContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
|
return RebuildBackBufferInteropTargets();
|
|
}
|
|
|
|
void NativeRenderer::ReleaseWindowRendererInterop() {
|
|
ReleaseWindowRendererBackBufferTargets();
|
|
m_interopBrush.Reset();
|
|
m_d2dDeviceContext.Reset();
|
|
m_d2dDevice.Reset();
|
|
m_d3d11On12Device.Reset();
|
|
m_d3d11DeviceContext.Reset();
|
|
m_d3d11Device.Reset();
|
|
}
|
|
|
|
bool NativeRenderer::RebuildBackBufferInteropTargets() {
|
|
m_backBufferInteropTargets.clear();
|
|
if (m_windowRenderer == nullptr || m_d3d11On12Device == nullptr || m_d2dDeviceContext == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
const std::uint32_t backBufferCount = m_windowRenderer->GetBackBufferCount();
|
|
m_backBufferInteropTargets.resize(backBufferCount);
|
|
for (std::uint32_t index = 0; index < backBufferCount; ++index) {
|
|
const ::XCEngine::RHI::D3D12Texture* backBufferTexture =
|
|
m_windowRenderer->GetBackBufferTexture(index);
|
|
if (backBufferTexture == nullptr || backBufferTexture->GetResource() == nullptr) {
|
|
m_lastRenderError = "Failed to resolve a D3D12 swap chain back buffer.";
|
|
m_backBufferInteropTargets.clear();
|
|
return false;
|
|
}
|
|
|
|
D3D11_RESOURCE_FLAGS resourceFlags = {};
|
|
resourceFlags.BindFlags = D3D11_BIND_RENDER_TARGET;
|
|
HRESULT hr = m_d3d11On12Device->CreateWrappedResource(
|
|
backBufferTexture->GetResource(),
|
|
&resourceFlags,
|
|
D3D12_RESOURCE_STATE_RENDER_TARGET,
|
|
D3D12_RESOURCE_STATE_PRESENT,
|
|
IID_PPV_ARGS(m_backBufferInteropTargets[index].wrappedResource.ReleaseAndGetAddressOf()));
|
|
if (FAILED(hr) || m_backBufferInteropTargets[index].wrappedResource == nullptr) {
|
|
m_lastRenderError = HrToString("ID3D11On12Device::CreateWrappedResource(backbuffer)", hr);
|
|
m_backBufferInteropTargets.clear();
|
|
return false;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IDXGISurface> dxgiSurface;
|
|
hr = m_backBufferInteropTargets[index].wrappedResource.As(&dxgiSurface);
|
|
if (FAILED(hr) || dxgiSurface == nullptr) {
|
|
m_lastRenderError = HrToString("ID3D11Resource::QueryInterface(IDXGISurface)", hr);
|
|
m_backBufferInteropTargets.clear();
|
|
return false;
|
|
}
|
|
|
|
const D2D1_BITMAP_PROPERTIES1 bitmapProperties =
|
|
BuildD2DBitmapProperties(
|
|
backBufferTexture->GetDesc().Format,
|
|
D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW);
|
|
hr = m_d2dDeviceContext->CreateBitmapFromDxgiSurface(
|
|
dxgiSurface.Get(),
|
|
&bitmapProperties,
|
|
m_backBufferInteropTargets[index].targetBitmap.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || m_backBufferInteropTargets[index].targetBitmap == nullptr) {
|
|
m_lastRenderError = HrToString("ID2D1DeviceContext::CreateBitmapFromDxgiSurface(backbuffer)", hr);
|
|
m_backBufferInteropTargets.clear();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NativeRenderer::ClearActiveInteropSourceTextures() {
|
|
m_activeInteropBitmaps.clear();
|
|
m_activeInteropSourceTextures.clear();
|
|
}
|
|
|
|
bool NativeRenderer::PrepareActiveInteropSourceTextures(
|
|
const ::XCEngine::UI::UIDrawData& drawData) {
|
|
ClearActiveInteropSourceTextures();
|
|
if (m_windowRenderer == nullptr || m_d3d11On12Device == nullptr || m_d2dDeviceContext == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
std::vector<::XCEngine::UI::UITextureHandle> textureHandles = {};
|
|
CollectInteropTextureHandles(drawData, textureHandles);
|
|
m_activeInteropSourceTextures.reserve(textureHandles.size());
|
|
|
|
for (const ::XCEngine::UI::UITextureHandle& textureHandle : textureHandles) {
|
|
auto* texture =
|
|
reinterpret_cast<::XCEngine::RHI::RHITexture*>(textureHandle.resourceHandle);
|
|
auto* nativeTexture = dynamic_cast<::XCEngine::RHI::D3D12Texture*>(texture);
|
|
if (nativeTexture == nullptr || nativeTexture->GetResource() == nullptr) {
|
|
m_lastRenderError = "Failed to resolve a D3D12 source texture for UI composition.";
|
|
ClearActiveInteropSourceTextures();
|
|
return false;
|
|
}
|
|
|
|
D3D11_RESOURCE_FLAGS resourceFlags = {};
|
|
resourceFlags.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
|
|
|
D3D12SourceTextureInteropResource resource = {};
|
|
resource.key = textureHandle.resourceHandle;
|
|
HRESULT hr = m_d3d11On12Device->CreateWrappedResource(
|
|
nativeTexture->GetResource(),
|
|
&resourceFlags,
|
|
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
|
|
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
|
|
IID_PPV_ARGS(resource.wrappedResource.ReleaseAndGetAddressOf()));
|
|
if (FAILED(hr) || resource.wrappedResource == nullptr) {
|
|
m_lastRenderError = HrToString("ID3D11On12Device::CreateWrappedResource(source)", hr);
|
|
ClearActiveInteropSourceTextures();
|
|
return false;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IDXGISurface> dxgiSurface;
|
|
hr = resource.wrappedResource.As(&dxgiSurface);
|
|
if (FAILED(hr) || dxgiSurface == nullptr) {
|
|
m_lastRenderError = HrToString("ID3D11Resource::QueryInterface(IDXGISurface)", hr);
|
|
ClearActiveInteropSourceTextures();
|
|
return false;
|
|
}
|
|
|
|
const D2D1_BITMAP_PROPERTIES1 bitmapProperties =
|
|
BuildD2DBitmapProperties(
|
|
nativeTexture->GetDesc().Format,
|
|
D2D1_BITMAP_OPTIONS_NONE);
|
|
hr = m_d2dDeviceContext->CreateBitmapFromDxgiSurface(
|
|
dxgiSurface.Get(),
|
|
&bitmapProperties,
|
|
resource.bitmap.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || resource.bitmap == nullptr) {
|
|
m_lastRenderError = HrToString("ID2D1DeviceContext::CreateBitmapFromDxgiSurface(source)", hr);
|
|
ClearActiveInteropSourceTextures();
|
|
return false;
|
|
}
|
|
|
|
m_activeInteropBitmaps.emplace(resource.key, resource.bitmap);
|
|
m_activeInteropSourceTextures.push_back(std::move(resource));
|
|
}
|
|
|
|
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() {
|
|
InvalidateCachedTextureBitmaps(m_renderTarget.Get());
|
|
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(
|
|
D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
|
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
|
|
kBaseDpi,
|
|
kBaseDpi);
|
|
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->SetDpi(kBaseDpi, kBaseDpi);
|
|
m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
|
m_lastRenderError.clear();
|
|
return true;
|
|
}
|
|
|
|
void NativeRenderer::InvalidateCachedTextureBitmaps(const ID2D1RenderTarget* renderTarget) {
|
|
for (NativeTextureResource* texture : m_liveTextures) {
|
|
if (texture == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
if (renderTarget == nullptr || texture->cachedTarget == renderTarget) {
|
|
texture->cachedBitmap.Reset();
|
|
texture->cachedTarget = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool NativeRenderer::DecodeTextureFile(
|
|
const std::filesystem::path& path,
|
|
NativeTextureResource& outTexture,
|
|
std::string& outError) {
|
|
outError.clear();
|
|
if (!EnsureWicFactory(outError)) {
|
|
return false;
|
|
}
|
|
|
|
const std::wstring widePath = path.wstring();
|
|
Microsoft::WRL::ComPtr<IWICBitmapDecoder> decoder;
|
|
HRESULT hr = m_wicFactory->CreateDecoderFromFilename(
|
|
widePath.c_str(),
|
|
nullptr,
|
|
GENERIC_READ,
|
|
WICDecodeMetadataCacheOnLoad,
|
|
decoder.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || !decoder) {
|
|
outError = HrToString("IWICImagingFactory::CreateDecoderFromFilename", hr);
|
|
return false;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IWICBitmapFrameDecode> frame;
|
|
hr = decoder->GetFrame(0u, frame.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || !frame) {
|
|
outError = HrToString("IWICBitmapDecoder::GetFrame", hr);
|
|
return false;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<IWICFormatConverter> converter;
|
|
hr = m_wicFactory->CreateFormatConverter(converter.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || !converter) {
|
|
outError = HrToString("IWICImagingFactory::CreateFormatConverter", hr);
|
|
return false;
|
|
}
|
|
|
|
hr = converter->Initialize(
|
|
frame.Get(),
|
|
GUID_WICPixelFormat32bppPBGRA,
|
|
WICBitmapDitherTypeNone,
|
|
nullptr,
|
|
0.0f,
|
|
WICBitmapPaletteTypeCustom);
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("IWICFormatConverter::Initialize", hr);
|
|
return false;
|
|
}
|
|
|
|
UINT width = 0u;
|
|
UINT height = 0u;
|
|
hr = converter->GetSize(&width, &height);
|
|
if (FAILED(hr) || width == 0u || height == 0u) {
|
|
outError = HrToString("IWICBitmapSource::GetSize", hr);
|
|
return false;
|
|
}
|
|
|
|
std::vector<std::uint8_t> pixels(
|
|
static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4u);
|
|
hr = converter->CopyPixels(
|
|
nullptr,
|
|
width * 4u,
|
|
static_cast<UINT>(pixels.size()),
|
|
pixels.data());
|
|
if (FAILED(hr)) {
|
|
outError = HrToString("IWICBitmapSource::CopyPixels", hr);
|
|
return false;
|
|
}
|
|
|
|
outTexture.pixels = std::move(pixels);
|
|
outTexture.width = width;
|
|
outTexture.height = height;
|
|
outTexture.cachedBitmap.Reset();
|
|
outTexture.cachedTarget = nullptr;
|
|
return true;
|
|
}
|
|
|
|
bool NativeRenderer::ResolveTextureBitmap(
|
|
ID2D1RenderTarget& renderTarget,
|
|
NativeTextureResource& texture,
|
|
Microsoft::WRL::ComPtr<ID2D1Bitmap>& outBitmap) {
|
|
outBitmap.Reset();
|
|
if (texture.width == 0u || texture.height == 0u || texture.pixels.empty()) {
|
|
return false;
|
|
}
|
|
|
|
if (texture.cachedBitmap && texture.cachedTarget == &renderTarget) {
|
|
outBitmap = texture.cachedBitmap;
|
|
return true;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<ID2D1Bitmap> bitmap;
|
|
const D2D1_BITMAP_PROPERTIES properties = D2D1::BitmapProperties(
|
|
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
|
|
kBaseDpi,
|
|
kBaseDpi);
|
|
const HRESULT hr = renderTarget.CreateBitmap(
|
|
D2D1::SizeU(texture.width, texture.height),
|
|
texture.pixels.data(),
|
|
texture.width * 4u,
|
|
&properties,
|
|
bitmap.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || !bitmap) {
|
|
return false;
|
|
}
|
|
|
|
if (&renderTarget == m_renderTarget.Get()) {
|
|
texture.cachedBitmap = bitmap;
|
|
texture.cachedTarget = &renderTarget;
|
|
}
|
|
|
|
outBitmap = std::move(bitmap);
|
|
return true;
|
|
}
|
|
|
|
bool NativeRenderer::ResolveInteropBitmap(
|
|
const ::XCEngine::UI::UITextureHandle& texture,
|
|
Microsoft::WRL::ComPtr<ID2D1Bitmap>& outBitmap) const {
|
|
outBitmap.Reset();
|
|
if (!IsInteropTextureHandle(texture)) {
|
|
return false;
|
|
}
|
|
|
|
const auto found = m_activeInteropBitmaps.find(texture.resourceHandle);
|
|
if (found == m_activeInteropBitmaps.end() || found->second == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
outBitmap = found->second;
|
|
return true;
|
|
}
|
|
|
|
bool NativeRenderer::RenderToTarget(
|
|
ID2D1RenderTarget& renderTarget,
|
|
ID2D1SolidColorBrush& solidBrush,
|
|
const ::XCEngine::UI::UIDrawData& drawData) {
|
|
renderTarget.SetDpi(kBaseDpi, kBaseDpi);
|
|
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));
|
|
const float dpiScale = ClampDpiScale(m_dpiScale);
|
|
|
|
switch (command.type) {
|
|
case ::XCEngine::UI::UIDrawCommandType::FilledRect: {
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
|
const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f;
|
|
if (command.rounding > 0.0f) {
|
|
renderTarget.FillRoundedRectangle(
|
|
D2D1::RoundedRect(rect, rounding, rounding),
|
|
&solidBrush);
|
|
} else {
|
|
renderTarget.FillRectangle(rect, &solidBrush);
|
|
}
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::FilledRectLinearGradient: {
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
|
const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f;
|
|
|
|
const D2D1_GRADIENT_STOP stops[2] = {
|
|
D2D1::GradientStop(0.0f, ToD2DColor(command.color)),
|
|
D2D1::GradientStop(1.0f, ToD2DColor(command.secondaryColor))
|
|
};
|
|
|
|
Microsoft::WRL::ComPtr<ID2D1GradientStopCollection> stopCollection;
|
|
HRESULT hr = renderTarget.CreateGradientStopCollection(
|
|
stops,
|
|
2u,
|
|
stopCollection.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || !stopCollection) {
|
|
break;
|
|
}
|
|
|
|
const D2D1_POINT_2F startPoint =
|
|
command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical
|
|
? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.top)
|
|
: D2D1::Point2F(rect.left, (rect.top + rect.bottom) * 0.5f);
|
|
const D2D1_POINT_2F endPoint =
|
|
command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical
|
|
? D2D1::Point2F((rect.left + rect.right) * 0.5f, rect.bottom)
|
|
: D2D1::Point2F(rect.right, (rect.top + rect.bottom) * 0.5f);
|
|
|
|
Microsoft::WRL::ComPtr<ID2D1LinearGradientBrush> gradientBrush;
|
|
hr = renderTarget.CreateLinearGradientBrush(
|
|
D2D1::LinearGradientBrushProperties(startPoint, endPoint),
|
|
stopCollection.Get(),
|
|
gradientBrush.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || !gradientBrush) {
|
|
break;
|
|
}
|
|
|
|
if (command.rounding > 0.0f) {
|
|
renderTarget.FillRoundedRectangle(
|
|
D2D1::RoundedRect(rect, rounding, rounding),
|
|
gradientBrush.Get());
|
|
} else {
|
|
renderTarget.FillRectangle(rect, gradientBrush.Get());
|
|
}
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::RectOutline: {
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
|
const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale;
|
|
const float rounding = command.rounding > 0.0f ? command.rounding * dpiScale : 0.0f;
|
|
if (command.rounding > 0.0f) {
|
|
renderTarget.DrawRoundedRectangle(
|
|
D2D1::RoundedRect(rect, rounding, rounding),
|
|
&solidBrush,
|
|
thickness);
|
|
} else {
|
|
renderTarget.DrawRectangle(rect, &solidBrush, thickness);
|
|
}
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::Line: {
|
|
const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale;
|
|
const float pixelOffset = ResolveStrokePixelOffset(thickness);
|
|
const D2D1_POINT_2F start = ToD2DPoint(command.position, dpiScale, pixelOffset);
|
|
const D2D1_POINT_2F end = ToD2DPoint(command.uvMin, dpiScale, pixelOffset);
|
|
renderTarget.DrawLine(start, end, &solidBrush, thickness);
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::FilledTriangle: {
|
|
Microsoft::WRL::ComPtr<ID2D1PathGeometry> geometry;
|
|
HRESULT hr = m_d2dFactory->CreatePathGeometry(geometry.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || !geometry) {
|
|
break;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<ID2D1GeometrySink> sink;
|
|
hr = geometry->Open(sink.ReleaseAndGetAddressOf());
|
|
if (FAILED(hr) || !sink) {
|
|
break;
|
|
}
|
|
|
|
const D2D1_POINT_2F a = ToD2DPoint(command.position, dpiScale);
|
|
const D2D1_POINT_2F b = ToD2DPoint(command.uvMin, dpiScale);
|
|
const D2D1_POINT_2F c = ToD2DPoint(command.uvMax, dpiScale);
|
|
const D2D1_POINT_2F points[2] = { b, c };
|
|
sink->BeginFigure(a, D2D1_FIGURE_BEGIN_FILLED);
|
|
sink->AddLines(points, 2u);
|
|
sink->EndFigure(D2D1_FIGURE_END_CLOSED);
|
|
hr = sink->Close();
|
|
if (FAILED(hr)) {
|
|
break;
|
|
}
|
|
|
|
renderTarget.FillGeometry(geometry.Get(), &solidBrush);
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::FilledCircle: {
|
|
const float radius = command.radius * dpiScale;
|
|
renderTarget.FillEllipse(
|
|
D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius),
|
|
&solidBrush);
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::CircleOutline: {
|
|
const float radius = command.radius * dpiScale;
|
|
const float thickness = (command.thickness > 0.0f ? command.thickness : 1.0f) * dpiScale;
|
|
renderTarget.DrawEllipse(
|
|
D2D1::Ellipse(ToD2DPoint(command.position, dpiScale), radius, radius),
|
|
&solidBrush,
|
|
thickness);
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::Text: {
|
|
if (command.text.empty()) {
|
|
break;
|
|
}
|
|
|
|
const float fontSize = ResolveFontSize(command.fontSize);
|
|
const float scaledFontSize = fontSize * dpiScale;
|
|
IDWriteTextFormat* textFormat = GetTextFormat(scaledFontSize);
|
|
if (textFormat == nullptr) {
|
|
break;
|
|
}
|
|
|
|
const std::wstring text = Utf8ToWide(command.text);
|
|
if (text.empty()) {
|
|
break;
|
|
}
|
|
|
|
const D2D1_SIZE_F targetSize = renderTarget.GetSize();
|
|
const float originX = SnapToPixel(command.position.x, dpiScale);
|
|
const float originY = SnapToPixel(command.position.y, dpiScale);
|
|
const float lineHeight = std::ceil(scaledFontSize * 1.6f);
|
|
const D2D1_RECT_F layoutRect = D2D1::RectF(
|
|
originX,
|
|
originY,
|
|
targetSize.width,
|
|
originY + lineHeight);
|
|
renderTarget.DrawTextW(
|
|
text.c_str(),
|
|
static_cast<UINT32>(text.size()),
|
|
textFormat,
|
|
layoutRect,
|
|
&solidBrush,
|
|
D2D1_DRAW_TEXT_OPTIONS_CLIP,
|
|
DWRITE_MEASURING_MODE_GDI_NATURAL);
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::Image: {
|
|
if (!command.texture.IsValid()) {
|
|
break;
|
|
}
|
|
|
|
Microsoft::WRL::ComPtr<ID2D1Bitmap> bitmap;
|
|
if (command.texture.kind == ::XCEngine::UI::UITextureHandleKind::ShaderResourceView) {
|
|
if (!ResolveInteropBitmap(command.texture, bitmap) || !bitmap) {
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
|
renderTarget.DrawRectangle(rect, &solidBrush, 1.0f);
|
|
break;
|
|
}
|
|
} else {
|
|
auto* texture = reinterpret_cast<NativeTextureResource*>(command.texture.nativeHandle);
|
|
if (texture == nullptr || m_liveTextures.find(texture) == m_liveTextures.end()) {
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
|
renderTarget.DrawRectangle(rect, &solidBrush, 1.0f);
|
|
break;
|
|
}
|
|
|
|
if (!ResolveTextureBitmap(renderTarget, *texture, bitmap) || !bitmap) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
|
const float sourceWidth = static_cast<float>(command.texture.width);
|
|
const float sourceHeight = static_cast<float>(command.texture.height);
|
|
const float sourceLeft = sourceWidth * std::clamp(command.uvMin.x, 0.0f, 1.0f);
|
|
const float sourceTop = sourceHeight * std::clamp(command.uvMin.y, 0.0f, 1.0f);
|
|
const float sourceRight = sourceWidth * std::clamp(command.uvMax.x, 0.0f, 1.0f);
|
|
const float sourceBottom = sourceHeight * std::clamp(command.uvMax.y, 0.0f, 1.0f);
|
|
renderTarget.DrawBitmap(
|
|
bitmap.Get(),
|
|
rect,
|
|
std::clamp(command.color.a, 0.0f, 1.0f),
|
|
D2D1_BITMAP_INTERPOLATION_MODE_LINEAR,
|
|
D2D1::RectF(sourceLeft, sourceTop, sourceRight, sourceBottom));
|
|
break;
|
|
}
|
|
case ::XCEngine::UI::UIDrawCommandType::PushClipRect: {
|
|
const D2D1_RECT_F rect = ToD2DRect(command.rect, dpiScale);
|
|
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) const {
|
|
if (!m_dwriteFactory) {
|
|
return nullptr;
|
|
}
|
|
|
|
const float resolvedFontSize = ResolveFontSize(fontSize);
|
|
const int key = static_cast<int>(std::lround(resolvedFontSize * 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,
|
|
resolvedFontSize,
|
|
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
|