Files
XCEngine/tests/Rendering/unit/test_render_resource_cache.cpp

575 lines
23 KiB
C++
Raw Normal View History

2026-04-10 20:44:24 +08:00
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Math/Bounds.h>
#include <XCEngine/RHI/RHIBuffer.h>
#include <XCEngine/RHI/RHIDevice.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/Rendering/Caches/RenderResourceCache.h>
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <memory>
#include <string>
#include <vector>
using namespace XCEngine::Math;
using namespace XCEngine::Rendering;
using namespace XCEngine::Resources;
namespace {
struct SyntheticGaussianSplatVertex {
Vector3 position = Vector3::Zero();
Vector3 dc0 = Vector3::Zero();
float opacity = 0.0f;
Vector3 scaleLog = Vector3::Zero();
float rotationWXYZ[4] = { 1.0f, 0.0f, 0.0f, 0.0f };
float sh[kGaussianSplatSHCoefficientCount] = {};
};
struct SampleArtifactData {
GaussianSplatMetadata metadata;
XCEngine::Containers::Array<GaussianSplatSection> sections;
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
};
struct MockCacheAllocationState {
int createBufferCalls = 0;
int shutdownBufferCalls = 0;
int destroyBufferCalls = 0;
int createShaderViewCalls = 0;
int shutdownShaderViewCalls = 0;
int destroyShaderViewCalls = 0;
std::vector<XCEngine::RHI::BufferDesc> bufferDescs;
std::vector<XCEngine::RHI::ResourceViewDesc> viewDescs;
};
class MockCacheBuffer final : public XCEngine::RHI::RHIBuffer {
public:
MockCacheBuffer(std::shared_ptr<MockCacheAllocationState> state, const XCEngine::RHI::BufferDesc& desc)
: m_state(std::move(state))
, m_size(desc.size)
, m_stride(desc.stride)
, m_bufferType(static_cast<XCEngine::RHI::BufferType>(desc.bufferType))
, m_data(static_cast<size_t>(desc.size), 0u) {
}
~MockCacheBuffer() override {
++m_state->destroyBufferCalls;
}
void* Map() override {
return m_data.empty() ? nullptr : m_data.data();
}
void Unmap() override {}
void SetData(const void* data, size_t size, size_t offset = 0) override {
ASSERT_NE(data, nullptr);
ASSERT_LE(offset + size, m_data.size());
std::memcpy(m_data.data() + offset, data, size);
}
uint64_t GetSize() const override { return m_size; }
XCEngine::RHI::BufferType GetBufferType() const override { return m_bufferType; }
void SetBufferType(XCEngine::RHI::BufferType type) override { m_bufferType = type; }
uint32_t GetStride() const override { return m_stride; }
void SetStride(uint32_t stride) override { m_stride = stride; }
void* GetNativeHandle() override { return nullptr; }
XCEngine::RHI::ResourceStates GetState() const override { return m_stateValue; }
void SetState(XCEngine::RHI::ResourceStates state) override { m_stateValue = state; }
const std::string& GetName() const override { return m_name; }
void SetName(const std::string& name) override { m_name = name; }
void Shutdown() override {
++m_state->shutdownBufferCalls;
}
const std::vector<XCEngine::Core::uint8>& GetBytes() const { return m_data; }
private:
std::shared_ptr<MockCacheAllocationState> m_state;
uint64_t m_size = 0u;
uint32_t m_stride = 0u;
XCEngine::RHI::BufferType m_bufferType = XCEngine::RHI::BufferType::Storage;
XCEngine::RHI::ResourceStates m_stateValue = XCEngine::RHI::ResourceStates::Common;
std::string m_name;
std::vector<XCEngine::Core::uint8> m_data;
};
class MockCacheView final : public XCEngine::RHI::RHIResourceView {
public:
MockCacheView(
std::shared_ptr<MockCacheAllocationState> state,
XCEngine::RHI::ResourceViewType viewType,
const XCEngine::RHI::ResourceViewDesc& desc)
: m_state(std::move(state))
, m_viewType(viewType)
, m_dimension(desc.dimension)
, m_format(static_cast<XCEngine::RHI::Format>(desc.format)) {
}
~MockCacheView() override {
++m_state->destroyShaderViewCalls;
}
void Shutdown() override {
++m_state->shutdownShaderViewCalls;
}
void* GetNativeHandle() override { return nullptr; }
bool IsValid() const override { return true; }
XCEngine::RHI::ResourceViewType GetViewType() const override { return m_viewType; }
XCEngine::RHI::ResourceViewDimension GetDimension() const override { return m_dimension; }
XCEngine::RHI::Format GetFormat() const override { return m_format; }
private:
std::shared_ptr<MockCacheAllocationState> m_state;
XCEngine::RHI::ResourceViewType m_viewType = XCEngine::RHI::ResourceViewType::ShaderResource;
XCEngine::RHI::ResourceViewDimension m_dimension = XCEngine::RHI::ResourceViewDimension::Unknown;
XCEngine::RHI::Format m_format = XCEngine::RHI::Format::Unknown;
};
class MockCacheDevice final : public XCEngine::RHI::RHIDevice {
public:
explicit MockCacheDevice(std::shared_ptr<MockCacheAllocationState> state)
: m_state(std::move(state)) {
}
bool Initialize(const XCEngine::RHI::RHIDeviceDesc&) override { return true; }
void Shutdown() override {}
XCEngine::RHI::RHIBuffer* CreateBuffer(const XCEngine::RHI::BufferDesc& desc) override {
++m_state->createBufferCalls;
m_state->bufferDescs.push_back(desc);
return new MockCacheBuffer(m_state, desc);
}
XCEngine::RHI::RHITexture* CreateTexture(const XCEngine::RHI::TextureDesc&) override { return nullptr; }
XCEngine::RHI::RHITexture* CreateTexture(
const XCEngine::RHI::TextureDesc&,
const void*,
size_t,
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(
uint32_t,
const XCEngine::RHI::AttachmentDesc*,
const XCEngine::RHI::AttachmentDesc*) override { return nullptr; }
XCEngine::RHI::RHIFramebuffer* CreateFramebuffer(
XCEngine::RHI::RHIRenderPass*,
uint32_t,
uint32_t,
uint32_t,
XCEngine::RHI::RHIResourceView**,
XCEngine::RHI::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; }
XCEngine::RHI::RHIResourceView* CreateVertexBufferView(
XCEngine::RHI::RHIBuffer*,
const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; }
XCEngine::RHI::RHIResourceView* CreateIndexBufferView(
XCEngine::RHI::RHIBuffer*,
const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; }
XCEngine::RHI::RHIResourceView* CreateRenderTargetView(
XCEngine::RHI::RHITexture*,
const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; }
XCEngine::RHI::RHIResourceView* CreateDepthStencilView(
XCEngine::RHI::RHITexture*,
const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; }
XCEngine::RHI::RHIResourceView* CreateShaderResourceView(
XCEngine::RHI::RHIBuffer*,
const XCEngine::RHI::ResourceViewDesc& desc) override {
++m_state->createShaderViewCalls;
m_state->viewDescs.push_back(desc);
return new MockCacheView(m_state, XCEngine::RHI::ResourceViewType::ShaderResource, desc);
}
XCEngine::RHI::RHIResourceView* CreateShaderResourceView(
XCEngine::RHI::RHITexture*,
const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; }
XCEngine::RHI::RHIResourceView* CreateUnorderedAccessView(
XCEngine::RHI::RHIBuffer*,
const XCEngine::RHI::ResourceViewDesc&) override { return nullptr; }
XCEngine::RHI::RHIResourceView* CreateUnorderedAccessView(
XCEngine::RHI::RHITexture*,
const XCEngine::RHI::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 nullptr; }
private:
std::shared_ptr<MockCacheAllocationState> m_state;
XCEngine::RHI::RHICapabilities m_capabilities = {};
XCEngine::RHI::RHIDeviceInfo m_deviceInfo = {};
};
void WriteBinaryFloat(std::ofstream& output, float value) {
output.write(reinterpret_cast<const char*>(&value), sizeof(value));
}
void WriteSyntheticGaussianSplatPly(
const std::filesystem::path& path,
const std::vector<SyntheticGaussianSplatVertex>& vertices) {
std::ofstream output(path, std::ios::binary | std::ios::trunc);
ASSERT_TRUE(output.is_open());
output << "ply\n";
output << "format binary_little_endian 1.0\n";
output << "element vertex " << vertices.size() << "\n";
output << "property float opacity\n";
output << "property float y\n";
output << "property float scale_2\n";
output << "property float rot_3\n";
output << "property float f_dc_1\n";
output << "property float x\n";
output << "property float scale_0\n";
output << "property float rot_1\n";
output << "property float f_dc_2\n";
output << "property float z\n";
output << "property float scale_1\n";
output << "property float rot_0\n";
output << "property float f_dc_0\n";
output << "property float rot_2\n";
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
output << "property float f_rest_" << index << "\n";
}
output << "end_header\n";
for (const SyntheticGaussianSplatVertex& vertex : vertices) {
WriteBinaryFloat(output, vertex.opacity);
WriteBinaryFloat(output, vertex.position.y);
WriteBinaryFloat(output, vertex.scaleLog.z);
WriteBinaryFloat(output, vertex.rotationWXYZ[3]);
WriteBinaryFloat(output, vertex.dc0.y);
WriteBinaryFloat(output, vertex.position.x);
WriteBinaryFloat(output, vertex.scaleLog.x);
WriteBinaryFloat(output, vertex.rotationWXYZ[1]);
WriteBinaryFloat(output, vertex.dc0.z);
WriteBinaryFloat(output, vertex.position.z);
WriteBinaryFloat(output, vertex.scaleLog.y);
WriteBinaryFloat(output, vertex.rotationWXYZ[0]);
WriteBinaryFloat(output, vertex.dc0.x);
WriteBinaryFloat(output, vertex.rotationWXYZ[2]);
for (float coefficient : vertex.sh) {
WriteBinaryFloat(output, coefficient);
}
}
}
SampleArtifactData BuildSampleArtifactData() {
const GaussianSplatPositionRecord positions[2] = {
{ Vector3(0.0f, 1.0f, 2.0f) },
{ Vector3(3.0f, 4.0f, 5.0f) }
};
const GaussianSplatOtherRecord other[2] = {
{ Quaternion::Identity(), Vector3(1.0f, 1.0f, 1.0f), 0.0f },
{ Quaternion(0.0f, 0.5f, 0.0f, 0.8660254f), Vector3(2.0f, 2.0f, 2.0f), 0.0f }
};
const GaussianSplatColorRecord colors[2] = {
{ Vector4(1.0f, 0.0f, 0.0f, 0.25f) },
{ Vector4(0.0f, 1.0f, 0.0f, 0.75f) }
};
GaussianSplatSHRecord sh[2];
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
sh[0].coefficients[index] = 0.01f * static_cast<float>(index + 1u);
sh[1].coefficients[index] = -0.02f * static_cast<float>(index + 1u);
}
const GaussianSplatChunkRecord chunks[1] = {
{
0x00010002u,
0x00030004u,
0x00050006u,
0x00070008u,
Vector2(-1.0f, 2.0f),
Vector2(-3.0f, 4.0f),
Vector2(-5.0f, 6.0f),
0x0009000Au,
0x000B000Cu,
0x000D000Eu,
0x000F0010u,
0x00110012u,
0x00130014u
}
};
2026-04-10 20:44:24 +08:00
SampleArtifactData sample;
sample.metadata.contentVersion = 1u;
sample.metadata.splatCount = 2u;
sample.metadata.chunkCount = 1u;
2026-04-10 20:44:24 +08:00
sample.metadata.bounds.SetMinMax(Vector3(-2.0f, -1.0f, -3.0f), Vector3(5.0f, 4.0f, 6.0f));
sample.metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32;
sample.metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32;
sample.metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
sample.metadata.shFormat = GaussianSplatSectionFormat::SHFloat32;
sample.metadata.chunkFormat = GaussianSplatSectionFormat::ChunkFloat32;
2026-04-10 20:44:24 +08:00
sample.sections.Reserve(5u);
2026-04-10 20:44:24 +08:00
size_t payloadOffset = 0u;
auto appendSection = [&](GaussianSplatSectionType type,
GaussianSplatSectionFormat format,
const void* data,
size_t dataSize,
XCEngine::Core::uint32 elementCount,
XCEngine::Core::uint32 elementStride) {
GaussianSplatSection section;
section.type = type;
section.format = format;
section.dataOffset = payloadOffset;
section.dataSize = dataSize;
section.elementCount = elementCount;
section.elementStride = elementStride;
sample.sections.PushBack(section);
const size_t newPayloadSize = sample.payload.Size() + dataSize;
sample.payload.Resize(newPayloadSize);
std::memcpy(sample.payload.Data() + payloadOffset, data, dataSize);
payloadOffset = newPayloadSize;
};
appendSection(
GaussianSplatSectionType::Positions,
GaussianSplatSectionFormat::VectorFloat32,
positions,
sizeof(positions),
2u,
sizeof(GaussianSplatPositionRecord));
appendSection(
GaussianSplatSectionType::Other,
GaussianSplatSectionFormat::OtherFloat32,
other,
sizeof(other),
2u,
sizeof(GaussianSplatOtherRecord));
appendSection(
GaussianSplatSectionType::Color,
GaussianSplatSectionFormat::ColorRGBA32F,
colors,
sizeof(colors),
2u,
sizeof(GaussianSplatColorRecord));
appendSection(
GaussianSplatSectionType::SH,
GaussianSplatSectionFormat::SHFloat32,
sh,
sizeof(sh),
2u,
sizeof(GaussianSplatSHRecord));
appendSection(
GaussianSplatSectionType::Chunks,
GaussianSplatSectionFormat::ChunkFloat32,
chunks,
sizeof(chunks),
1u,
sizeof(GaussianSplatChunkRecord));
2026-04-10 20:44:24 +08:00
return sample;
}
GaussianSplat BuildSampleGaussianSplat(const char* artifactPath) {
SampleArtifactData sample = BuildSampleArtifactData();
GaussianSplat gaussianSplat;
XCEngine::Resources::IResource::ConstructParams params;
params.name = "sample.xcgsplat";
params.path = artifactPath;
params.guid = ResourceGUID::Generate(params.path);
gaussianSplat.Initialize(params);
EXPECT_TRUE(gaussianSplat.CreateOwned(
sample.metadata,
std::move(sample.sections),
std::move(sample.payload)));
return gaussianSplat;
}
std::filesystem::path CreateTestProjectRoot(const char* folderName) {
return std::filesystem::current_path() / "__xc_gaussian_splat_cache_test_runtime" / folderName;
}
TEST(RenderResourceCacheTest, GetOrCreateGaussianSplatUploadsStructuredSectionsAndReusesEntry) {
GaussianSplat gaussianSplat = BuildSampleGaussianSplat("sample.xcgsplat");
auto state = std::make_shared<MockCacheAllocationState>();
MockCacheDevice device(state);
RenderResourceCache cache;
const RenderResourceCache::CachedGaussianSplat* cached =
cache.GetOrCreateGaussianSplat(&device, &gaussianSplat);
ASSERT_NE(cached, nullptr);
EXPECT_EQ(cached->residencyState, RenderResourceCache::GaussianSplatResidencyState::GpuReady);
EXPECT_EQ(cached->contentVersion, 1u);
EXPECT_EQ(cached->splatCount, 2u);
EXPECT_EQ(cached->chunkCount, 1u);
EXPECT_EQ(cached->positions.elementStride, sizeof(float) * 4u);
2026-04-10 20:44:24 +08:00
EXPECT_EQ(cached->other.elementStride, sizeof(GaussianSplatOtherRecord));
EXPECT_EQ(cached->color.elementStride, sizeof(GaussianSplatColorRecord));
EXPECT_EQ(cached->sh.elementStride, sizeof(GaussianSplatSHRecord));
EXPECT_EQ(cached->chunks.elementStride, sizeof(GaussianSplatChunkRecord));
2026-04-10 20:44:24 +08:00
EXPECT_EQ(cached->positions.elementCount, 2u);
EXPECT_EQ(cached->other.elementCount, 2u);
EXPECT_EQ(cached->color.elementCount, 2u);
EXPECT_EQ(cached->sh.elementCount, 2u);
EXPECT_EQ(cached->chunks.elementCount, 1u);
2026-04-10 20:44:24 +08:00
ASSERT_NE(cached->positions.buffer, nullptr);
ASSERT_NE(cached->positions.shaderResourceView, nullptr);
ASSERT_NE(cached->color.buffer, nullptr);
ASSERT_NE(cached->sh.buffer, nullptr);
ASSERT_NE(cached->chunks.buffer, nullptr);
2026-04-10 20:44:24 +08:00
const auto* uploadedPositions = static_cast<const MockCacheBuffer*>(cached->positions.buffer);
ASSERT_GE(uploadedPositions->GetBytes().size(), sizeof(float) * 4u * 2u);
const auto* uploadedPositionWords = reinterpret_cast<const float*>(uploadedPositions->GetBytes().data());
EXPECT_FLOAT_EQ(uploadedPositionWords[0], 0.0f);
EXPECT_FLOAT_EQ(uploadedPositionWords[1], 1.0f);
EXPECT_FLOAT_EQ(uploadedPositionWords[2], 2.0f);
EXPECT_FLOAT_EQ(uploadedPositionWords[3], 0.0f);
EXPECT_FLOAT_EQ(uploadedPositionWords[4], 3.0f);
EXPECT_FLOAT_EQ(uploadedPositionWords[5], 4.0f);
EXPECT_FLOAT_EQ(uploadedPositionWords[6], 5.0f);
EXPECT_FLOAT_EQ(uploadedPositionWords[7], 0.0f);
EXPECT_EQ(state->createBufferCalls, 5);
EXPECT_EQ(state->createShaderViewCalls, 5);
2026-04-10 20:44:24 +08:00
const RenderResourceCache::CachedGaussianSplat* cachedAgain =
cache.GetOrCreateGaussianSplat(&device, &gaussianSplat);
EXPECT_EQ(cachedAgain, cached);
EXPECT_EQ(state->createBufferCalls, 5);
EXPECT_EQ(state->createShaderViewCalls, 5);
2026-04-10 20:44:24 +08:00
cache.Shutdown();
EXPECT_EQ(state->shutdownBufferCalls, 5);
EXPECT_EQ(state->destroyBufferCalls, 5);
EXPECT_EQ(state->shutdownShaderViewCalls, 5);
EXPECT_EQ(state->destroyShaderViewCalls, 5);
2026-04-10 20:44:24 +08:00
}
TEST(RenderResourceCacheTest, GetOrCreateGaussianSplatSupportsArtifactRuntimeLoadPath) {
namespace fs = std::filesystem;
const fs::path tempDir = fs::temp_directory_path() / "xc_gaussian_splat_render_cache_artifact";
const fs::path artifactPath = tempDir / "sample.xcgsplat";
fs::remove_all(tempDir);
fs::create_directories(tempDir);
const GaussianSplat source = BuildSampleGaussianSplat(artifactPath.string().c_str());
XCEngine::Containers::String errorMessage;
ASSERT_TRUE(WriteGaussianSplatArtifactFile(artifactPath.string().c_str(), source, &errorMessage))
<< errorMessage.CStr();
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
{
const auto handle = manager.Load<GaussianSplat>(artifactPath.string().c_str());
ASSERT_TRUE(handle.IsValid());
auto state = std::make_shared<MockCacheAllocationState>();
MockCacheDevice device(state);
RenderResourceCache cache;
const RenderResourceCache::CachedGaussianSplat* cached =
cache.GetOrCreateGaussianSplat(&device, handle.Get());
ASSERT_NE(cached, nullptr);
EXPECT_EQ(cached->residencyState, RenderResourceCache::GaussianSplatResidencyState::GpuReady);
EXPECT_EQ(cached->splatCount, 2u);
EXPECT_EQ(cached->chunkCount, 1u);
EXPECT_EQ(state->createBufferCalls, 5);
EXPECT_EQ(state->createShaderViewCalls, 5);
2026-04-10 20:44:24 +08:00
}
manager.UnloadAll();
manager.Shutdown();
fs::remove_all(tempDir);
}
TEST(RenderResourceCacheTest, GetOrCreateGaussianSplatSupportsSourceAssetImportPath) {
namespace fs = std::filesystem;
const fs::path projectRoot = CreateTestProjectRoot("source_asset_import");
const fs::path assetsDir = projectRoot / "Assets";
const fs::path sourcePath = assetsDir / "sample.ply";
fs::remove_all(projectRoot);
fs::create_directories(assetsDir);
std::vector<SyntheticGaussianSplatVertex> vertices(2);
vertices[0].position = Vector3(1.0f, 2.0f, 3.0f);
vertices[0].dc0 = Vector3(0.2f, -0.1f, 0.0f);
vertices[0].opacity = 0.25f;
vertices[0].scaleLog = Vector3(0.0f, std::log(2.0f), std::log(4.0f));
vertices[0].rotationWXYZ[0] = 1.0f;
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
vertices[0].sh[index] = 0.01f * static_cast<float>(index + 1u);
}
vertices[1].position = Vector3(-4.0f, -5.0f, -6.0f);
vertices[1].dc0 = Vector3(1.0f, 0.5f, -0.5f);
vertices[1].opacity = -1.0f;
vertices[1].scaleLog = Vector3(std::log(0.5f), 0.0f, std::log(3.0f));
vertices[1].rotationWXYZ[2] = 3.0f;
vertices[1].rotationWXYZ[3] = 4.0f;
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
vertices[1].sh[index] = -0.02f * static_cast<float>(index + 1u);
}
WriteSyntheticGaussianSplatPly(sourcePath, vertices);
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
manager.SetResourceRoot(projectRoot.string().c_str());
{
const auto handle = manager.Load<GaussianSplat>("Assets/sample.ply");
ASSERT_TRUE(handle.IsValid());
EXPECT_EQ(handle->GetSplatCount(), 2u);
auto state = std::make_shared<MockCacheAllocationState>();
MockCacheDevice device(state);
RenderResourceCache cache;
const RenderResourceCache::CachedGaussianSplat* cached =
cache.GetOrCreateGaussianSplat(&device, handle.Get());
ASSERT_NE(cached, nullptr);
EXPECT_EQ(cached->residencyState, RenderResourceCache::GaussianSplatResidencyState::GpuReady);
EXPECT_EQ(cached->splatCount, 2u);
EXPECT_EQ(state->createBufferCalls, 4);
EXPECT_EQ(state->createShaderViewCalls, 4);
const auto handleAgain = manager.Load<GaussianSplat>("Assets/sample.ply");
ASSERT_TRUE(handleAgain.IsValid());
const RenderResourceCache::CachedGaussianSplat* cachedAgain =
cache.GetOrCreateGaussianSplat(&device, handleAgain.Get());
EXPECT_EQ(cachedAgain, cached);
EXPECT_EQ(state->createBufferCalls, 4);
EXPECT_EQ(state->createShaderViewCalls, 4);
}
manager.UnloadAll();
manager.Shutdown();
fs::remove_all(projectRoot);
}
} // namespace