Files
XCEngine/tests/NewEditor/test_native_window_ui_compositor.cpp

447 lines
17 KiB
C++

#include <gtest/gtest.h>
#include "Platform/D3D12WindowRenderer.h"
#include "XCUIBackend/IWindowUICompositor.h"
#include "XCUIBackend/NativeWindowUICompositor.h"
#include "XCUIBackend/UITextureRegistration.h"
#include <XCEngine/RHI/RHICapabilities.h>
#include <XCEngine/RHI/RHIDevice.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHITexture.h>
#include <XCEngine/UI/DrawData.h>
#include <array>
#include <cstdint>
#include <memory>
#include <string>
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<HWND>(static_cast<std::uintptr_t>(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<Format>(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<float, 4>{ 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<IWindowUICompositor> 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<float, 4>{ 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<std::uint32_t>(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