#include #include "XCUIBackend/IWindowUICompositor.h" #include "XCUIBackend/NativeWindowUICompositor.h" #include "XCUIBackend/UITextureRegistration.h" #include #include #include #include #include #include #include #include #include namespace { using XCEngine::Editor::Platform::D3D12WindowRenderer; using XCEngine::Editor::XCUIBackend::CreateNativeWindowUICompositor; using XCEngine::Editor::XCUIBackend::IXCUITextAtlasProvider; using XCEngine::Editor::XCUIBackend::IWindowUICompositor; using XCEngine::Editor::XCUIBackend::NativeWindowUICompositor; using XCEngine::Editor::XCUIBackend::UITextureRegistration; using XCEngine::Editor::XCUIBackend::XCUINativeWindowPresentStats; using XCEngine::Editor::XCUIBackend::XCUINativeWindowRenderPacket; using XCEngine::RHI::Format; using XCEngine::RHI::ResourceStates; using XCEngine::RHI::ResourceViewDesc; using XCEngine::RHI::ResourceViewDimension; using XCEngine::RHI::ResourceViewType; using XCEngine::RHI::RHIDevice; using XCEngine::RHI::RHIResourceView; using XCEngine::RHI::RHITexture; using XCEngine::RHI::TextureType; using XCEngine::UI::UITextureHandleKind; HWND MakeFakeHwnd() { return reinterpret_cast(static_cast(0x2345u)); } class StubTextAtlasProvider final : public IXCUITextAtlasProvider { public: bool GetAtlasTextureView(PixelFormat preferredFormat, AtlasTextureView& outView) const override { (void)preferredFormat; outView = {}; return false; } std::size_t GetFontCount() const override { return 0u; } FontHandle GetFont(std::size_t index) const override { (void)index; return {}; } FontHandle GetDefaultFont() const override { return {}; } bool GetFontInfo(FontHandle font, FontInfo& outInfo) const override { (void)font; outInfo = {}; return false; } bool GetBakedFontInfo(FontHandle font, float fontSize, BakedFontInfo& outInfo) const override { (void)font; (void)fontSize; outInfo = {}; return false; } bool FindGlyph(FontHandle font, float fontSize, std::uint32_t codepoint, GlyphInfo& outInfo) const override { (void)font; (void)fontSize; (void)codepoint; outInfo = {}; return false; } }; XCEngine::UI::UIDrawData MakeDrawData() { XCEngine::UI::UIDrawData drawData = {}; drawData.EmplaceDrawList("NativeOverlay").AddFilledRect( XCEngine::UI::UIRect(10.0f, 12.0f, 48.0f, 24.0f), XCEngine::UI::UIColor(0.2f, 0.4f, 0.8f, 1.0f)); return drawData; } struct TrackingShaderViewState { int shutdownCount = 0; int destructorCount = 0; }; class TrackingShaderResourceView final : public XCEngine::RHI::RHIShaderResourceView { public: TrackingShaderResourceView( TrackingShaderViewState& state, bool valid, ResourceViewDimension dimension = ResourceViewDimension::Texture2D, Format format = Format::R8G8B8A8_UNorm) : m_state(state) , m_valid(valid) , m_dimension(dimension) , m_format(format) { } ~TrackingShaderResourceView() override { ++m_state.destructorCount; } void Shutdown() override { ++m_state.shutdownCount; m_valid = false; } void* GetNativeHandle() override { return this; } bool IsValid() const override { return m_valid; } ResourceViewType GetViewType() const override { return ResourceViewType::ShaderResource; } ResourceViewDimension GetDimension() const override { return m_dimension; } Format GetFormat() const override { return m_format; } private: TrackingShaderViewState& m_state; bool m_valid = true; ResourceViewDimension m_dimension = ResourceViewDimension::Texture2D; Format m_format = Format::R8G8B8A8_UNorm; }; class FakeTexture final : public RHITexture { public: FakeTexture( std::uint32_t width, std::uint32_t height, Format format = Format::R8G8B8A8_UNorm, TextureType textureType = TextureType::Texture2D) : m_width(width) , m_height(height) , m_format(format) , m_textureType(textureType) { } std::uint32_t GetWidth() const override { return m_width; } std::uint32_t GetHeight() const override { return m_height; } std::uint32_t GetDepth() const override { return 1u; } std::uint32_t GetMipLevels() const override { return 1u; } Format GetFormat() const override { return m_format; } TextureType GetTextureType() const override { return m_textureType; } ResourceStates GetState() const override { return m_state; } void SetState(ResourceStates state) override { m_state = state; } void* GetNativeHandle() override { return this; } const std::string& GetName() const override { return m_name; } void SetName(const std::string& name) override { m_name = name; } void Shutdown() override { m_shutdownCalled = true; } bool shutdownCalled() const { return m_shutdownCalled; } private: std::uint32_t m_width = 0u; std::uint32_t m_height = 0u; Format m_format = Format::Unknown; TextureType m_textureType = TextureType::Texture2D; ResourceStates m_state = ResourceStates::Common; std::string m_name = {}; bool m_shutdownCalled = false; }; class RecordingDevice final : public RHIDevice { public: TrackingShaderViewState* nextShaderViewState = nullptr; bool createShaderViewValid = true; int createShaderViewCount = 0; RHITexture* lastShaderViewTexture = nullptr; ResourceViewDesc lastShaderViewDesc = {}; bool Initialize(const XCEngine::RHI::RHIDeviceDesc&) override { return true; } void Shutdown() override {} XCEngine::RHI::RHIBuffer* CreateBuffer(const XCEngine::RHI::BufferDesc&) override { return nullptr; } XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&) override { return nullptr; } XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&, const void*, size_t, std::uint32_t) override { return nullptr; } XCEngine::RHI::RHISwapChain* CreateSwapChain(const XCEngine::RHI::SwapChainDesc&, XCEngine::RHI::RHICommandQueue*) override { return nullptr; } XCEngine::RHI::RHICommandList* CreateCommandList(const XCEngine::RHI::CommandListDesc&) override { return nullptr; } XCEngine::RHI::RHICommandQueue* CreateCommandQueue(const XCEngine::RHI::CommandQueueDesc&) override { return nullptr; } XCEngine::RHI::RHIShader* CreateShader(const XCEngine::RHI::ShaderCompileDesc&) override { return nullptr; } XCEngine::RHI::RHIPipelineState* CreatePipelineState(const XCEngine::RHI::GraphicsPipelineDesc&) override { return nullptr; } XCEngine::RHI::RHIPipelineLayout* CreatePipelineLayout(const XCEngine::RHI::RHIPipelineLayoutDesc&) override { return nullptr; } XCEngine::RHI::RHIFence* CreateFence(const XCEngine::RHI::FenceDesc&) override { return nullptr; } XCEngine::RHI::RHISampler* CreateSampler(const XCEngine::RHI::SamplerDesc&) override { return nullptr; } XCEngine::RHI::RHIRenderPass* CreateRenderPass( std::uint32_t, const XCEngine::RHI::AttachmentDesc*, const XCEngine::RHI::AttachmentDesc*) override { return nullptr; } XCEngine::RHI::RHIFramebuffer* CreateFramebuffer( XCEngine::RHI::RHIRenderPass*, std::uint32_t, std::uint32_t, std::uint32_t, RHIResourceView**, RHIResourceView*) override { return nullptr; } XCEngine::RHI::RHIDescriptorPool* CreateDescriptorPool(const XCEngine::RHI::DescriptorPoolDesc&) override { return nullptr; } XCEngine::RHI::RHIDescriptorSet* CreateDescriptorSet( XCEngine::RHI::RHIDescriptorPool*, const XCEngine::RHI::DescriptorSetLayoutDesc&) override { return nullptr; } RHIResourceView* CreateVertexBufferView(XCEngine::RHI::RHIBuffer*, const ResourceViewDesc&) override { return nullptr; } RHIResourceView* CreateIndexBufferView(XCEngine::RHI::RHIBuffer*, const ResourceViewDesc&) override { return nullptr; } RHIResourceView* CreateRenderTargetView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } RHIResourceView* CreateDepthStencilView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } RHIResourceView* CreateShaderResourceView(XCEngine::RHI::RHITexture* texture, const ResourceViewDesc& desc) override { ++createShaderViewCount; lastShaderViewTexture = texture; lastShaderViewDesc = desc; if (nextShaderViewState == nullptr) { return nullptr; } return new TrackingShaderResourceView( *nextShaderViewState, createShaderViewValid, desc.dimension != ResourceViewDimension::Unknown ? desc.dimension : ResourceViewDimension::Texture2D, desc.format != 0u ? static_cast(desc.format) : Format::R8G8B8A8_UNorm); } RHIResourceView* CreateUnorderedAccessView(XCEngine::RHI::RHITexture*, const ResourceViewDesc&) override { return nullptr; } const XCEngine::RHI::RHICapabilities& GetCapabilities() const override { return m_capabilities; } const XCEngine::RHI::RHIDeviceInfo& GetDeviceInfo() const override { return m_deviceInfo; } void* GetNativeDevice() override { return this; } private: XCEngine::RHI::RHICapabilities m_capabilities = {}; XCEngine::RHI::RHIDeviceInfo m_deviceInfo = {}; }; TEST(NativeWindowUICompositorTest, RenderPacketReportsDrawDataPresenceAndClearResetsPayload) { XCUINativeWindowRenderPacket packet = {}; EXPECT_FALSE(packet.HasDrawData()); EXPECT_EQ(packet.textAtlasProvider, nullptr); StubTextAtlasProvider atlasProvider = {}; packet.drawData = MakeDrawData(); packet.textAtlasProvider = &atlasProvider; EXPECT_TRUE(packet.HasDrawData()); EXPECT_EQ(packet.drawData.GetDrawListCount(), 1u); EXPECT_EQ(packet.drawData.GetTotalCommandCount(), 1u); EXPECT_EQ(packet.textAtlasProvider, &atlasProvider); packet.Clear(); EXPECT_FALSE(packet.HasDrawData()); EXPECT_EQ(packet.drawData.GetDrawListCount(), 0u); EXPECT_EQ(packet.drawData.GetTotalCommandCount(), 0u); EXPECT_EQ(packet.textAtlasProvider, nullptr); } TEST(NativeWindowUICompositorTest, SubmitAndClearPendingPacketTracksCopiedDrawDataAndAtlasProvider) { NativeWindowUICompositor compositor = {}; StubTextAtlasProvider atlasProvider = {}; const XCEngine::UI::UIDrawData drawData = MakeDrawData(); compositor.SubmitRenderPacket(drawData, &atlasProvider); ASSERT_TRUE(compositor.HasPendingRenderPacket()); const XCUINativeWindowRenderPacket& packet = compositor.GetPendingRenderPacket(); EXPECT_TRUE(packet.HasDrawData()); EXPECT_EQ(packet.drawData.GetDrawListCount(), 1u); EXPECT_EQ(packet.drawData.GetTotalCommandCount(), 1u); EXPECT_EQ(packet.textAtlasProvider, &atlasProvider); compositor.ClearPendingRenderPacket(); EXPECT_FALSE(compositor.HasPendingRenderPacket()); EXPECT_FALSE(compositor.GetPendingRenderPacket().HasDrawData()); EXPECT_EQ(compositor.GetPendingRenderPacket().textAtlasProvider, nullptr); } TEST(NativeWindowUICompositorTest, InitializeAndShutdownResetStateAlongSafePaths) { NativeWindowUICompositor compositor = {}; D3D12WindowRenderer renderer = {}; compositor.SubmitRenderPacket(MakeDrawData(), nullptr); ASSERT_TRUE(compositor.HasPendingRenderPacket()); bool configureFontsCalled = false; EXPECT_FALSE(compositor.Initialize( nullptr, renderer, [&configureFontsCalled]() { configureFontsCalled = true; })); EXPECT_FALSE(configureFontsCalled); EXPECT_FALSE(compositor.HasPendingRenderPacket()); compositor.SubmitRenderPacket(MakeDrawData(), nullptr); EXPECT_TRUE(compositor.Initialize( MakeFakeHwnd(), renderer, [&configureFontsCalled]() { configureFontsCalled = true; })); EXPECT_FALSE(configureFontsCalled); EXPECT_FALSE(compositor.HasPendingRenderPacket()); compositor.Shutdown(); EXPECT_FALSE(compositor.HasPendingRenderPacket()); const XCUINativeWindowPresentStats& stats = compositor.GetLastPresentStats(); EXPECT_FALSE(stats.hadPendingPacket); EXPECT_FALSE(stats.renderedNativeOverlay); EXPECT_EQ(stats.submittedDrawListCount, 0u); EXPECT_EQ(stats.submittedCommandCount, 0u); } TEST(NativeWindowUICompositorTest, RenderFrameWithUnpreparedRendererSkipsCallbacksAndKeepsPendingPacket) { NativeWindowUICompositor compositor = {}; D3D12WindowRenderer renderer = {}; ASSERT_TRUE(compositor.Initialize(MakeFakeHwnd(), renderer, {})); compositor.SubmitRenderPacket(MakeDrawData(), nullptr); ASSERT_TRUE(compositor.HasPendingRenderPacket()); bool uiRendered = false; bool beforeUiRendered = false; bool afterUiRendered = false; compositor.RenderFrame( std::array{ 0.1f, 0.2f, 0.3f, 1.0f }.data(), [&uiRendered]() { uiRendered = true; }, [&beforeUiRendered](const ::XCEngine::Rendering::RenderContext&, const ::XCEngine::Rendering::RenderSurface&) { beforeUiRendered = true; }, [&afterUiRendered](const ::XCEngine::Rendering::RenderContext&, const ::XCEngine::Rendering::RenderSurface&) { afterUiRendered = true; }); EXPECT_FALSE(uiRendered); EXPECT_FALSE(beforeUiRendered); EXPECT_FALSE(afterUiRendered); EXPECT_TRUE(compositor.HasPendingRenderPacket()); const XCUINativeWindowPresentStats& stats = compositor.GetLastPresentStats(); EXPECT_FALSE(stats.hadPendingPacket); EXPECT_FALSE(stats.renderedNativeOverlay); EXPECT_EQ(stats.submittedDrawListCount, 0u); EXPECT_EQ(stats.submittedCommandCount, 0u); } TEST(NativeWindowUICompositorTest, InterfaceFactoryReturnsSafeNativeCompositorDefaults) { std::unique_ptr compositor = CreateNativeWindowUICompositor(); ASSERT_NE(compositor, nullptr); D3D12WindowRenderer renderer = {}; bool configureFontsCalled = false; EXPECT_FALSE(compositor->Initialize( nullptr, renderer, [&configureFontsCalled]() { configureFontsCalled = true; })); EXPECT_FALSE(configureFontsCalled); EXPECT_FALSE(compositor->HandleWindowMessage(MakeFakeHwnd(), WM_CLOSE, 0u, 0u)); UITextureRegistration registration = {}; EXPECT_FALSE(compositor->CreateTextureDescriptor(nullptr, nullptr, registration)); EXPECT_EQ(registration.texture.nativeHandle, 0u); bool uiRendered = false; compositor->RenderFrame( std::array{ 0.0f, 0.0f, 0.0f, 1.0f }.data(), [&uiRendered]() { uiRendered = true; }, {}, {}); EXPECT_FALSE(uiRendered); compositor->Shutdown(); } TEST(NativeWindowUICompositorTest, ShaderResourceViewRegistrationsStayValidWithoutGpuDescriptorHandle) { UITextureRegistration registration = {}; registration.cpuHandle.ptr = 17u; registration.texture.nativeHandle = 33u; registration.texture.width = 64u; registration.texture.height = 32u; registration.texture.kind = UITextureHandleKind::ShaderResourceView; EXPECT_TRUE(registration.IsValid()); registration.texture.kind = UITextureHandleKind::ImGuiDescriptor; EXPECT_FALSE(registration.IsValid()); registration.gpuHandle.ptr = 19u; EXPECT_TRUE(registration.IsValid()); } TEST(NativeWindowUICompositorTest, CreateTextureDescriptorPublishesShaderResourceViewAndFreeReleasesIt) { NativeWindowUICompositor compositor = {}; RecordingDevice device = {}; TrackingShaderViewState viewState = {}; device.nextShaderViewState = &viewState; FakeTexture texture(256u, 128u, Format::R8G8B8A8_UNorm, TextureType::Texture2DArray); UITextureRegistration registration = {}; ASSERT_TRUE(compositor.CreateTextureDescriptor(&device, &texture, registration)); EXPECT_EQ(device.createShaderViewCount, 1); EXPECT_EQ(device.lastShaderViewTexture, &texture); EXPECT_EQ(device.lastShaderViewDesc.format, static_cast(Format::R8G8B8A8_UNorm)); EXPECT_EQ(device.lastShaderViewDesc.dimension, ResourceViewDimension::Texture2DArray); EXPECT_TRUE(registration.IsValid()); EXPECT_NE(registration.cpuHandle.ptr, 0u); EXPECT_EQ(registration.gpuHandle.ptr, 0u); EXPECT_EQ(registration.texture.width, 256u); EXPECT_EQ(registration.texture.height, 128u); EXPECT_EQ(registration.texture.kind, UITextureHandleKind::ShaderResourceView); EXPECT_EQ(registration.cpuHandle.ptr, registration.texture.nativeHandle); compositor.FreeTextureDescriptor(registration); EXPECT_EQ(viewState.shutdownCount, 1); EXPECT_EQ(viewState.destructorCount, 1); } TEST(NativeWindowUICompositorTest, CreateTextureDescriptorRejectsInvalidShaderResourceViewAndCleansItUp) { NativeWindowUICompositor compositor = {}; RecordingDevice device = {}; TrackingShaderViewState viewState = {}; device.nextShaderViewState = &viewState; device.createShaderViewValid = false; FakeTexture texture(96u, 64u); UITextureRegistration registration = {}; EXPECT_FALSE(compositor.CreateTextureDescriptor(&device, &texture, registration)); EXPECT_EQ(device.createShaderViewCount, 1); EXPECT_FALSE(registration.IsValid()); EXPECT_EQ(registration.texture.nativeHandle, 0u); EXPECT_EQ(viewState.shutdownCount, 1); EXPECT_EQ(viewState.destructorCount, 1); } } // namespace