447 lines
17 KiB
C++
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
|