#include #include "XCUIBackend/XCUIRHIRenderBackend.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { using XCEngine::Editor::XCUIBackend::IXCUITextAtlasProvider; using XCEngine::Editor::XCUIBackend::XCUIRHIRenderBackend; using XCEngine::Rendering::RenderContext; using XCEngine::Rendering::RenderSurface; using XCEngine::RHI::BlendDesc; using XCEngine::RHI::BufferDesc; using XCEngine::RHI::BufferType; using XCEngine::RHI::CommandQueueType; using XCEngine::RHI::ComparisonFunc; using XCEngine::RHI::DepthStencilStateDesc; using XCEngine::RHI::DescriptorHeapType; using XCEngine::RHI::DescriptorPoolDesc; using XCEngine::RHI::DescriptorSetLayoutBinding; using XCEngine::RHI::DescriptorSetLayoutDesc; using XCEngine::RHI::Format; using XCEngine::RHI::GraphicsPipelineDesc; using XCEngine::RHI::InputLayoutDesc; using XCEngine::RHI::PipelineStateHash; using XCEngine::RHI::PipelineType; using XCEngine::RHI::PrimitiveTopology; using XCEngine::RHI::RasterizerDesc; using XCEngine::RHI::Rect; using XCEngine::RHI::ResourceStates; using XCEngine::RHI::ResourceViewDesc; using XCEngine::RHI::ResourceViewDimension; using XCEngine::RHI::ResourceViewType; using XCEngine::RHI::RHICommandList; using XCEngine::RHI::RHICommandQueue; using XCEngine::RHI::RHIDescriptorPool; using XCEngine::RHI::RHIDescriptorSet; using XCEngine::RHI::RHIDevice; using XCEngine::RHI::RHIPipelineLayout; using XCEngine::RHI::RHIPipelineState; using XCEngine::RHI::RHIResourceView; using XCEngine::RHI::RHISampler; using XCEngine::RHI::RHIBuffer; using XCEngine::RHI::RHIShader; using XCEngine::RHI::RHIType; using XCEngine::RHI::SamplerDesc; using XCEngine::RHI::Viewport; using XCEngine::UI::UIColor; using XCEngine::UI::UIDrawData; using XCEngine::UI::UIDrawList; using XCEngine::UI::UIRect; using XCEngine::UI::UITextureHandle; using XCEngine::UI::UITextureHandleKind; class FakeResourceViewBase { public: explicit FakeResourceViewBase( ResourceViewType viewType, ResourceViewDimension dimension = ResourceViewDimension::Texture2D, Format format = Format::R8G8B8A8_UNorm) : m_viewType(viewType) , m_dimension(dimension) , m_format(format) { } void ShutdownBase() { m_valid = false; } void* GetNativeHandleBase() { return this; } bool IsValidBase() const { return m_valid; } ResourceViewType GetViewTypeBase() const { return m_viewType; } ResourceViewDimension GetDimensionBase() const { return m_dimension; } Format GetFormatBase() const { return m_format; } void SetValid(bool valid) { m_valid = valid; } private: ResourceViewType m_viewType = ResourceViewType::ShaderResource; ResourceViewDimension m_dimension = ResourceViewDimension::Texture2D; Format m_format = Format::R8G8B8A8_UNorm; bool m_valid = true; }; class FakeShaderResourceView final : public XCEngine::RHI::RHIShaderResourceView { public: explicit FakeShaderResourceView(bool valid = true) : m_base(ResourceViewType::ShaderResource) { m_base.SetValid(valid); } void Shutdown() override { m_base.ShutdownBase(); } void* GetNativeHandle() override { return m_base.GetNativeHandleBase(); } bool IsValid() const override { return m_base.IsValidBase(); } ResourceViewType GetViewType() const override { return m_base.GetViewTypeBase(); } ResourceViewDimension GetDimension() const override { return m_base.GetDimensionBase(); } Format GetFormat() const override { return m_base.GetFormatBase(); } private: FakeResourceViewBase m_base; }; class FakeRenderTargetView final : public XCEngine::RHI::RHIRenderTargetView { public: FakeRenderTargetView() : m_base(ResourceViewType::RenderTarget) { } void Shutdown() override { m_base.ShutdownBase(); } void* GetNativeHandle() override { return m_base.GetNativeHandleBase(); } bool IsValid() const override { return m_base.IsValidBase(); } ResourceViewType GetViewType() const override { return m_base.GetViewTypeBase(); } ResourceViewDimension GetDimension() const override { return m_base.GetDimensionBase(); } Format GetFormat() const override { return m_base.GetFormatBase(); } private: FakeResourceViewBase m_base; }; class FakeVertexBufferView final : public XCEngine::RHI::RHIVertexBufferView { public: explicit FakeVertexBufferView(std::uint32_t size, std::uint32_t stride) : m_size(size) , m_stride(stride) , m_base(ResourceViewType::VertexBuffer, ResourceViewDimension::Buffer, Format::Unknown) { } void Shutdown() override { m_base.ShutdownBase(); } void* GetNativeHandle() override { return m_base.GetNativeHandleBase(); } bool IsValid() const override { return m_base.IsValidBase(); } ResourceViewType GetViewType() const override { return m_base.GetViewTypeBase(); } ResourceViewDimension GetDimension() const override { return m_base.GetDimensionBase(); } Format GetFormat() const override { return m_base.GetFormatBase(); } std::uint64_t GetBufferAddress() const override { return 0u; } std::uint32_t GetSize() const override { return m_size; } std::uint32_t GetStride() const override { return m_stride; } private: std::uint32_t m_size = 0; std::uint32_t m_stride = 0; FakeResourceViewBase m_base; }; class FakeBuffer final : public RHIBuffer { public: explicit FakeBuffer(const BufferDesc& desc) : m_size(desc.size) , m_stride(desc.stride) , m_type(static_cast(desc.bufferType)) , m_storage(static_cast(desc.size), 0u) { } void* Map() override { return m_storage.data(); } void Unmap() override {} void SetData(const void* data, size_t size, size_t offset = 0) override { if (data == nullptr || offset + size > m_storage.size()) { return; } std::memcpy(m_storage.data() + offset, data, size); } std::uint64_t GetSize() const override { return m_size; } BufferType GetBufferType() const override { return m_type; } void SetBufferType(BufferType type) override { m_type = type; } std::uint32_t GetStride() const override { return m_stride; } void SetStride(std::uint32_t stride) override { m_stride = stride; } void* GetNativeHandle() override { return this; } ResourceStates GetState() const override { return m_state; } void SetState(ResourceStates state) override { m_state = state; } const std::string& GetName() const override { return m_name; } void SetName(const std::string& name) override { m_name = name; } void Shutdown() override { m_storage.clear(); } private: std::uint64_t m_size = 0; std::uint32_t m_stride = 0; BufferType m_type = BufferType::Vertex; ResourceStates m_state = ResourceStates::Common; std::string m_name = {}; std::vector m_storage = {}; }; class FakeDescriptorSet final : public RHIDescriptorSet { public: explicit FakeDescriptorSet(const DescriptorSetLayoutDesc& layout) { if (layout.bindings != nullptr && layout.bindingCount > 0u) { m_bindings.assign(layout.bindings, layout.bindings + layout.bindingCount); m_views.resize(layout.bindingCount, nullptr); m_samplers.resize(layout.bindingCount, nullptr); } } void Shutdown() override { m_views.clear(); m_samplers.clear(); m_constantBuffer.clear(); m_constantDirty = false; } void Bind() override {} void Unbind() override {} void Update(std::uint32_t offset, RHIResourceView* view) override { if (offset >= m_views.size()) { m_views.resize(offset + 1u, nullptr); } m_views[offset] = view; } void UpdateSampler(std::uint32_t offset, RHISampler* sampler) override { if (offset >= m_samplers.size()) { m_samplers.resize(offset + 1u, nullptr); } m_samplers[offset] = sampler; } void WriteConstant(std::uint32_t, const void* data, size_t size, size_t offset = 0) override { if (data == nullptr) { return; } if (m_constantBuffer.size() < offset + size) { m_constantBuffer.resize(offset + size, 0u); } std::memcpy(m_constantBuffer.data() + offset, data, size); m_constantDirty = true; } std::uint32_t GetBindingCount() const override { return static_cast(m_bindings.size()); } const DescriptorSetLayoutBinding* GetBindings() const override { return m_bindings.empty() ? nullptr : m_bindings.data(); } void* GetConstantBufferData() override { return m_constantBuffer.empty() ? nullptr : m_constantBuffer.data(); } size_t GetConstantBufferSize() const override { return m_constantBuffer.size(); } bool IsConstantDirty() const override { return m_constantDirty; } void MarkConstantClean() override { m_constantDirty = false; } private: std::vector m_bindings = {}; std::vector m_views = {}; std::vector m_samplers = {}; std::vector m_constantBuffer = {}; bool m_constantDirty = false; }; class FakeDescriptorPool final : public RHIDescriptorPool { public: explicit FakeDescriptorPool(const DescriptorPoolDesc& desc) : m_desc(desc) { } bool Initialize(const DescriptorPoolDesc& desc) override { m_desc = desc; return true; } void Shutdown() override {} void* GetNativeHandle() override { return this; } std::uint32_t GetDescriptorCount() const override { return m_desc.descriptorCount; } DescriptorHeapType GetType() const override { return m_desc.type; } RHIDescriptorSet* AllocateSet(const DescriptorSetLayoutDesc& layout) override { ++m_allocationCount; return new FakeDescriptorSet(layout); } void FreeSet(RHIDescriptorSet* set) override { delete set; } std::uint32_t GetAllocationCount() const { return m_allocationCount; } private: DescriptorPoolDesc m_desc = {}; std::uint32_t m_allocationCount = 0; }; class FakePipelineLayout final : public RHIPipelineLayout { public: bool Initialize(const XCEngine::RHI::RHIPipelineLayoutDesc& desc) override { m_desc = desc; return true; } void Shutdown() override {} void* GetNativeHandle() override { return this; } private: XCEngine::RHI::RHIPipelineLayoutDesc m_desc = {}; }; class FakePipelineState final : public RHIPipelineState { public: explicit FakePipelineState(const GraphicsPipelineDesc& desc) : m_inputLayout(desc.inputLayout) , m_rasterizer(desc.rasterizerState) , m_blend(desc.blendState) , m_depthStencil(desc.depthStencilState) { } void SetInputLayout(const InputLayoutDesc& layout) override { m_inputLayout = layout; } void SetRasterizerState(const RasterizerDesc& state) override { m_rasterizer = state; } void SetBlendState(const BlendDesc& state) override { m_blend = state; } void SetDepthStencilState(const DepthStencilStateDesc& state) override { m_depthStencil = state; } void SetTopology(std::uint32_t topologyType) override { m_topologyType = topologyType; } void SetRenderTargetFormats(std::uint32_t, const std::uint32_t*, std::uint32_t) override {} void SetSampleCount(std::uint32_t count) override { m_sampleCount = count; } void SetComputeShader(RHIShader* shader) override { m_computeShader = shader; } const RasterizerDesc& GetRasterizerState() const override { return m_rasterizer; } const BlendDesc& GetBlendState() const override { return m_blend; } const DepthStencilStateDesc& GetDepthStencilState() const override { return m_depthStencil; } const InputLayoutDesc& GetInputLayout() const override { return m_inputLayout; } PipelineStateHash GetHash() const override { return {}; } RHIShader* GetComputeShader() const override { return m_computeShader; } bool HasComputeShader() const override { return m_computeShader != nullptr; } bool IsValid() const override { return true; } void EnsureValid() override {} void Shutdown() override {} void Bind() override {} void Unbind() override {} void* GetNativeHandle() override { return this; } PipelineType GetType() const override { return PipelineType::Graphics; } private: InputLayoutDesc m_inputLayout = {}; RasterizerDesc m_rasterizer = {}; BlendDesc m_blend = {}; DepthStencilStateDesc m_depthStencil = {}; std::uint32_t m_topologyType = 0u; std::uint32_t m_sampleCount = 1u; RHIShader* m_computeShader = nullptr; }; class FakeSampler final : public RHISampler { public: void Shutdown() override {} void Bind(unsigned int) override {} void Unbind(unsigned int) override {} void* GetNativeHandle() override { return this; } unsigned int GetID() override { return 1u; } }; class FakeCommandQueue final : public RHICommandQueue { public: void Shutdown() override {} void ExecuteCommandLists(std::uint32_t, void**) override {} void Signal(XCEngine::RHI::RHIFence*, std::uint64_t) override {} void Wait(XCEngine::RHI::RHIFence*, std::uint64_t) override {} std::uint64_t GetCompletedValue() override { return 0u; } void WaitForIdle() override {} CommandQueueType GetType() const override { return CommandQueueType::Direct; } std::uint64_t GetTimestampFrequency() const override { return 0u; } void* GetNativeHandle() override { return this; } void WaitForPreviousFrame() override {} std::uint64_t GetCurrentFrame() const override { return 0u; } }; class FakeCommandList final : public RHICommandList { public: void Shutdown() override {} void Reset() override {} void Close() override {} void TransitionBarrier(RHIResourceView*, ResourceStates, ResourceStates) override {} void BeginRenderPass(XCEngine::RHI::RHIRenderPass*, XCEngine::RHI::RHIFramebuffer*, const Rect&, std::uint32_t, const XCEngine::RHI::ClearValue*) override {} void EndRenderPass() override {} void SetShader(RHIShader*) override {} void SetPipelineState(RHIPipelineState* pso) override { pipelineStateHistory.push_back(pso); } void SetGraphicsDescriptorSets( std::uint32_t, std::uint32_t count, RHIDescriptorSet** descriptorSets, RHIPipelineLayout*) override { if (count >= 2u && descriptorSets != nullptr) { textureDescriptorSetHistory.push_back(descriptorSets[1]); } } void SetComputeDescriptorSets(std::uint32_t, std::uint32_t, RHIDescriptorSet**, RHIPipelineLayout*) override {} void SetPrimitiveTopology(PrimitiveTopology topology) override { primitiveTopologyHistory.push_back(topology); } void SetViewport(const Viewport& viewport) override { viewportHistory.push_back(viewport); } void SetViewports(std::uint32_t count, const Viewport* viewports) override { for (std::uint32_t index = 0; index < count; ++index) { viewportHistory.push_back(viewports[index]); } } void SetScissorRect(const Rect& rect) override { scissorHistory.push_back(rect); } void SetScissorRects(std::uint32_t count, const Rect* rects) override { for (std::uint32_t index = 0; index < count; ++index) { scissorHistory.push_back(rects[index]); } } void SetRenderTargets(std::uint32_t count, RHIResourceView**, RHIResourceView* = nullptr) override { renderTargetBindHistory.push_back(count); } void SetStencilRef(std::uint8_t) override {} void SetBlendFactor(const float[4]) override {} void SetVertexBuffers(std::uint32_t, std::uint32_t, RHIResourceView**, const std::uint64_t*, const std::uint32_t*) override {} void SetIndexBuffer(RHIResourceView*, std::uint64_t) override {} void Draw(std::uint32_t vertexCount, std::uint32_t, std::uint32_t startVertex, std::uint32_t) override { drawVertexCounts.push_back(vertexCount); drawStartVertices.push_back(startVertex); } void DrawIndexed(std::uint32_t, std::uint32_t, std::uint32_t, std::int32_t, std::uint32_t) override {} void Clear(float, float, float, float, std::uint32_t) override {} void ClearRenderTarget(RHIResourceView*, const float[4], std::uint32_t = 0, const Rect* = nullptr) override {} void ClearDepthStencil(RHIResourceView*, float, std::uint8_t, std::uint32_t = 0, const Rect* = nullptr) override {} void CopyResource(RHIResourceView*, RHIResourceView*) override {} void Dispatch(std::uint32_t, std::uint32_t, std::uint32_t) override {} void* GetNativeHandle() override { return this; } std::vector pipelineStateHistory = {}; std::vector primitiveTopologyHistory = {}; std::vector viewportHistory = {}; std::vector scissorHistory = {}; std::vector renderTargetBindHistory = {}; std::vector textureDescriptorSetHistory = {}; std::vector drawVertexCounts = {}; std::vector drawStartVertices = {}; }; class FakeDevice final : public RHIDevice { public: bool Initialize(const XCEngine::RHI::RHIDeviceDesc&) override { return true; } void Shutdown() override {} RHIBuffer* CreateBuffer(const BufferDesc& desc) override { return new FakeBuffer(desc); } 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 = 0) override { return nullptr; } XCEngine::RHI::RHISwapChain* CreateSwapChain(const XCEngine::RHI::SwapChainDesc&, RHICommandQueue*) override { return nullptr; } RHICommandList* CreateCommandList(const XCEngine::RHI::CommandListDesc&) override { return nullptr; } RHICommandQueue* CreateCommandQueue(const XCEngine::RHI::CommandQueueDesc&) override { return nullptr; } RHIShader* CreateShader(const XCEngine::RHI::ShaderCompileDesc&) override { return nullptr; } RHIPipelineState* CreatePipelineState(const GraphicsPipelineDesc& desc) override { return new FakePipelineState(desc); } RHIPipelineLayout* CreatePipelineLayout(const XCEngine::RHI::RHIPipelineLayoutDesc&) override { return new FakePipelineLayout(); } XCEngine::RHI::RHIFence* CreateFence(const XCEngine::RHI::FenceDesc&) override { return nullptr; } RHISampler* CreateSampler(const SamplerDesc&) override { return new FakeSampler(); } 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; } RHIDescriptorPool* CreateDescriptorPool(const DescriptorPoolDesc& desc) override { return new FakeDescriptorPool(desc); } RHIDescriptorSet* CreateDescriptorSet(RHIDescriptorPool*, const DescriptorSetLayoutDesc&) override { return nullptr; } RHIResourceView* CreateVertexBufferView(RHIBuffer* buffer, const ResourceViewDesc& desc) override { return new FakeVertexBufferView(static_cast(buffer != nullptr ? buffer->GetSize() : 0u), desc.structureByteStride); } RHIResourceView* CreateIndexBufferView(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*, const ResourceViewDesc&) override { return nullptr; } 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 = {}; }; UITextureHandle MakeExternalTextureHandle(FakeShaderResourceView& shaderView, std::uint32_t width = 32u, std::uint32_t height = 32u) { UITextureHandle texture = {}; texture.nativeHandle = reinterpret_cast(&shaderView); texture.width = width; texture.height = height; texture.kind = UITextureHandleKind::ShaderResourceView; return texture; } } // namespace TEST(XCUIRHIRenderBackendTest, ExposesNativeOverlayEntryAndAtlasInjection) { static_assert(std::is_same_v< decltype(std::declval().Render( std::declval(), std::declval(), std::declval())), bool>); static_assert(std::is_same_v< decltype(std::declval().SetTextAtlasProvider( std::declval())) , void>); static_assert(std::is_same_v< decltype(std::declval().GetLastOverlayStats().drawListCount), std::size_t>); SUCCEED(); } TEST(XCUIRHIRenderBackendTest, RenderRejectsInvalidContextAndLeavesStatsClear) { XCUIRHIRenderBackend backend = {}; RenderSurface surface(128, 72); UIDrawData drawData = {}; EXPECT_FALSE(backend.Render(RenderContext(), surface, drawData)); EXPECT_EQ(backend.GetLastOverlayStats().commandCount, 0u); EXPECT_EQ(backend.GetLastOverlayStats().renderedCommandCount, 0u); } TEST(XCUIRHIRenderBackendTest, SetTextAtlasProviderStoresPointerAndResetStatsClearsCounters) { XCUIRHIRenderBackend backend = {}; class StubAtlasProvider final : public IXCUITextAtlasProvider { public: bool GetAtlasTextureView(PixelFormat, AtlasTextureView& outView) const override { outView = {}; return false; } std::size_t GetFontCount() const override { return 0u; } FontHandle GetFont(std::size_t) const override { return {}; } FontHandle GetDefaultFont() const override { return {}; } bool GetFontInfo(FontHandle, FontInfo& outInfo) const override { outInfo = {}; return false; } bool GetBakedFontInfo(FontHandle, float, BakedFontInfo& outInfo) const override { outInfo = {}; return false; } bool FindGlyph(FontHandle, float, std::uint32_t, GlyphInfo& outInfo) const override { outInfo = {}; return false; } } atlasProvider; backend.SetTextAtlasProvider(&atlasProvider); EXPECT_EQ(backend.GetTextAtlasProvider(), &atlasProvider); backend.SetTextAtlasProvider(nullptr); EXPECT_EQ(backend.GetTextAtlasProvider(), nullptr); backend.ResetStats(); EXPECT_EQ(backend.GetLastStats().drawListCount, 0u); EXPECT_EQ(backend.GetLastStats().commandCount, 0u); EXPECT_EQ(backend.GetLastStats().batchCount, 0u); EXPECT_EQ(backend.GetLastStats().renderedCommandCount, 0u); EXPECT_EQ(backend.GetLastStats().skippedCommandCount, 0u); EXPECT_EQ(backend.GetLastStats().textureResolveCount, 0u); EXPECT_EQ(backend.GetLastStats().textureCacheHitCount, 0u); } TEST(XCUIRHIRenderBackendTest, RenderReusesExternalTextureBindingsAcrossTexturedBatches) { FakeDevice device = {}; FakeCommandList commandList = {}; FakeCommandQueue commandQueue = {}; FakeRenderTargetView renderTarget = {}; FakeShaderResourceView externalTextureView(true); RenderContext context = {}; context.device = &device; context.commandList = &commandList; context.commandQueue = &commandQueue; context.backendType = RHIType::D3D12; RenderSurface surface(64, 48); surface.SetColorAttachment(&renderTarget); UIDrawData drawData = {}; UIDrawList& firstDrawList = drawData.EmplaceDrawList("FirstTexturedBatch"); firstDrawList.PushClipRect(UIRect(4.0f, 6.0f, 12.0f, 10.0f)); firstDrawList.AddImage( UIRect(0.0f, 0.0f, 18.0f, 18.0f), MakeExternalTextureHandle(externalTextureView), UIColor(1.0f, 0.8f, 0.6f, 1.0f)); firstDrawList.PopClipRect(); UIDrawList& secondDrawList = drawData.EmplaceDrawList("SecondTexturedBatch"); secondDrawList.PushClipRect(UIRect(20.0f, 8.0f, 10.0f, 12.0f)); secondDrawList.AddImage( UIRect(18.0f, 4.0f, 16.0f, 16.0f), MakeExternalTextureHandle(externalTextureView), UIColor(0.7f, 0.9f, 1.0f, 1.0f)); secondDrawList.PopClipRect(); XCUIRHIRenderBackend backend = {}; ASSERT_TRUE(backend.Render(context, surface, drawData)); const XCUIRHIRenderBackend::OverlayStats& stats = backend.GetLastOverlayStats(); EXPECT_EQ(stats.drawListCount, 2u); EXPECT_EQ(stats.commandCount, 6u); EXPECT_EQ(stats.batchCount, 2u); EXPECT_EQ(stats.colorBatchCount, 0u); EXPECT_EQ(stats.texturedBatchCount, 2u); EXPECT_EQ(stats.scissoredBatchCount, 2u); EXPECT_EQ(stats.renderedCommandCount, 2u); EXPECT_EQ(stats.skippedCommandCount, 0u); EXPECT_EQ(stats.textureResolveCount, 2u); EXPECT_EQ(stats.textureCacheHitCount, 1u); EXPECT_EQ(stats.vertexCount, 12u); EXPECT_EQ(stats.triangleCount, 4u); ASSERT_EQ(commandList.renderTargetBindHistory.size(), 1u); ASSERT_EQ(commandList.viewportHistory.size(), 1u); ASSERT_EQ(commandList.drawVertexCounts.size(), 2u); EXPECT_EQ(commandList.drawVertexCounts[0], 6u); EXPECT_EQ(commandList.drawVertexCounts[1], 6u); ASSERT_EQ(commandList.textureDescriptorSetHistory.size(), 2u); EXPECT_EQ(commandList.textureDescriptorSetHistory[0], commandList.textureDescriptorSetHistory[1]); ASSERT_EQ(commandList.scissorHistory.size(), 4u); EXPECT_EQ(commandList.scissorHistory[0].left, 0); EXPECT_EQ(commandList.scissorHistory[0].top, 0); EXPECT_EQ(commandList.scissorHistory[0].right, 64); EXPECT_EQ(commandList.scissorHistory[0].bottom, 48); EXPECT_EQ(commandList.scissorHistory[1].left, 4); EXPECT_EQ(commandList.scissorHistory[1].top, 6); EXPECT_EQ(commandList.scissorHistory[1].right, 16); EXPECT_EQ(commandList.scissorHistory[1].bottom, 16); EXPECT_EQ(commandList.scissorHistory[2].left, 20); EXPECT_EQ(commandList.scissorHistory[2].top, 8); EXPECT_EQ(commandList.scissorHistory[2].right, 30); EXPECT_EQ(commandList.scissorHistory[2].bottom, 20); EXPECT_EQ(commandList.scissorHistory[3].left, 0); EXPECT_EQ(commandList.scissorHistory[3].top, 0); EXPECT_EQ(commandList.scissorHistory[3].right, 64); EXPECT_EQ(commandList.scissorHistory[3].bottom, 48); } TEST(XCUIRHIRenderBackendTest, RenderSkipsTexturedBatchWhenExternalTextureViewIsInvalid) { FakeDevice device = {}; FakeCommandList commandList = {}; FakeCommandQueue commandQueue = {}; FakeRenderTargetView renderTarget = {}; FakeShaderResourceView invalidTextureView(false); RenderContext context = {}; context.device = &device; context.commandList = &commandList; context.commandQueue = &commandQueue; context.backendType = RHIType::D3D12; RenderSurface surface(32, 32); surface.SetColorAttachment(&renderTarget); UIDrawData drawData = {}; UIDrawList& drawList = drawData.EmplaceDrawList("InvalidExternalTexture"); drawList.AddImage( UIRect(2.0f, 4.0f, 12.0f, 8.0f), MakeExternalTextureHandle(invalidTextureView), UIColor(1.0f, 1.0f, 1.0f, 1.0f)); XCUIRHIRenderBackend backend = {}; ASSERT_TRUE(backend.Render(context, surface, drawData)); const XCUIRHIRenderBackend::OverlayStats& stats = backend.GetLastOverlayStats(); EXPECT_EQ(stats.commandCount, 1u); EXPECT_EQ(stats.batchCount, 1u); EXPECT_EQ(stats.texturedBatchCount, 1u); EXPECT_EQ(stats.renderedCommandCount, 0u); EXPECT_EQ(stats.skippedCommandCount, 1u); EXPECT_EQ(stats.textureResolveCount, 1u); EXPECT_EQ(stats.textureCacheHitCount, 0u); EXPECT_EQ(commandList.drawVertexCounts.size(), 0u); EXPECT_EQ(commandList.textureDescriptorSetHistory.size(), 0u); }