Add gaussian splat asset caching groundwork
This commit is contained in:
@@ -28,10 +28,13 @@
|
||||
#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
@@ -48,12 +51,23 @@ namespace {
|
||||
constexpr const char* kD3D12Screenshot = "gaussian_splat_scene_d3d12.ppm";
|
||||
constexpr const char* kOpenGLScreenshot = "gaussian_splat_scene_opengl.ppm";
|
||||
constexpr const char* kVulkanScreenshot = "gaussian_splat_scene_vulkan.ppm";
|
||||
constexpr const char* kD3D12AlphaDebugScreenshot = "gaussian_splat_scene_d3d12_alpha.ppm";
|
||||
constexpr const char* kOpenGLAlphaDebugScreenshot = "gaussian_splat_scene_opengl_alpha.ppm";
|
||||
constexpr const char* kVulkanAlphaDebugScreenshot = "gaussian_splat_scene_vulkan_alpha.ppm";
|
||||
constexpr uint32_t kFrameWidth = 1280;
|
||||
constexpr uint32_t kFrameHeight = 720;
|
||||
constexpr uint32_t kBaselineSubsetSplatCount = 65536u;
|
||||
constexpr uint32_t kBaselineSubsetSplatCount = 262144u;
|
||||
constexpr const char* kSubsetGaussianSplatAssetPath = "Assets/room_subset.xcgsplat";
|
||||
constexpr float kTargetSceneExtent = 4.0f;
|
||||
constexpr float kGaussianPointScale = 3.00f;
|
||||
constexpr float kGaussianPointScale = 1.00f;
|
||||
const Vector3 kDefaultCameraPosition(0.0f, 1.0f, 1.0f);
|
||||
const Vector3 kDefaultCameraLookAt(0.0f, 1.0f, 0.0f);
|
||||
const Vector3 kDefaultRootPosition = Vector3::Zero();
|
||||
|
||||
enum class GaussianSplatDebugView : uint8_t {
|
||||
Scene = 0,
|
||||
Alpha = 1
|
||||
};
|
||||
|
||||
XCEngine::Core::uint16 FloatToHalfBits(float value) {
|
||||
uint32_t bits = 0u;
|
||||
@@ -128,15 +142,178 @@ void LinkOrCopyFixture(const std::filesystem::path& sourcePath, const std::files
|
||||
ASSERT_FALSE(ec) << ec.message();
|
||||
}
|
||||
|
||||
const char* GetScreenshotFilename(RHIType backendType) {
|
||||
GaussianSplatDebugView GetDebugViewFromEnvironment() {
|
||||
const char* debugViewValue = std::getenv("XCENGINE_GAUSSIAN_SPLAT_DEBUG_VIEW");
|
||||
if (debugViewValue == nullptr) {
|
||||
return GaussianSplatDebugView::Scene;
|
||||
}
|
||||
|
||||
if (_stricmp(debugViewValue, "alpha") == 0 ||
|
||||
std::strcmp(debugViewValue, "1") == 0) {
|
||||
return GaussianSplatDebugView::Alpha;
|
||||
}
|
||||
|
||||
return GaussianSplatDebugView::Scene;
|
||||
}
|
||||
|
||||
float GetFloatFromEnvironment(const char* name, float defaultValue) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
char* end = nullptr;
|
||||
const float parsedValue = std::strtof(value, &end);
|
||||
return end != value ? parsedValue : defaultValue;
|
||||
}
|
||||
|
||||
uint32_t GetUIntFromEnvironment(const char* name, uint32_t defaultValue) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
char* end = nullptr;
|
||||
const unsigned long parsedValue = std::strtoul(value, &end, 10);
|
||||
return end != value ? static_cast<uint32_t>(parsedValue) : defaultValue;
|
||||
}
|
||||
|
||||
Vector3 GetVector3FromEnvironment(const char* name, const Vector3& defaultValue) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
if (std::sscanf(value, "%f,%f,%f", &x, &y, &z) == 3) {
|
||||
return Vector3(x, y, z);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
void ExpectVector3Near(const Vector3& actual, const Vector3& expected, float epsilon = 1.0e-6f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
||||
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
||||
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
||||
}
|
||||
|
||||
void ExpectVector4Near(const Vector4& actual, const Vector4& expected, float epsilon = 1.0e-6f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
||||
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
||||
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
||||
EXPECT_NEAR(actual.w, expected.w, epsilon);
|
||||
}
|
||||
|
||||
void ExpectQuaternionNear(const Quaternion& actual, const Quaternion& expected, float epsilon = 1.0e-6f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
||||
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
||||
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
||||
EXPECT_NEAR(actual.w, expected.w, epsilon);
|
||||
}
|
||||
|
||||
void ExpectGaussianSplatRoundTripMatches(
|
||||
const GaussianSplat& source,
|
||||
const GaussianSplat& loaded) {
|
||||
ASSERT_EQ(source.GetSplatCount(), loaded.GetSplatCount());
|
||||
ASSERT_EQ(source.GetChunkCount(), loaded.GetChunkCount());
|
||||
ASSERT_EQ(source.GetSHOrder(), loaded.GetSHOrder());
|
||||
ExpectVector3Near(loaded.GetBounds().GetMin(), source.GetBounds().GetMin());
|
||||
ExpectVector3Near(loaded.GetBounds().GetMax(), source.GetBounds().GetMax());
|
||||
ASSERT_NE(source.GetPositionRecords(), nullptr);
|
||||
ASSERT_NE(loaded.GetPositionRecords(), nullptr);
|
||||
ASSERT_NE(source.GetOtherRecords(), nullptr);
|
||||
ASSERT_NE(loaded.GetOtherRecords(), nullptr);
|
||||
ASSERT_NE(source.GetColorRecords(), nullptr);
|
||||
ASSERT_NE(loaded.GetColorRecords(), nullptr);
|
||||
ASSERT_NE(source.GetSHRecords(), nullptr);
|
||||
ASSERT_NE(loaded.GetSHRecords(), nullptr);
|
||||
|
||||
const uint32_t sampleIndices[] = {
|
||||
0u,
|
||||
source.GetSplatCount() / 2u,
|
||||
source.GetSplatCount() - 1u
|
||||
};
|
||||
for (uint32_t sampleIndex : sampleIndices) {
|
||||
ExpectVector3Near(
|
||||
loaded.GetPositionRecords()[sampleIndex].position,
|
||||
source.GetPositionRecords()[sampleIndex].position);
|
||||
ExpectQuaternionNear(
|
||||
loaded.GetOtherRecords()[sampleIndex].rotation,
|
||||
source.GetOtherRecords()[sampleIndex].rotation);
|
||||
ExpectVector3Near(
|
||||
loaded.GetOtherRecords()[sampleIndex].scale,
|
||||
source.GetOtherRecords()[sampleIndex].scale);
|
||||
ExpectVector4Near(
|
||||
loaded.GetColorRecords()[sampleIndex].colorOpacity,
|
||||
source.GetColorRecords()[sampleIndex].colorOpacity);
|
||||
|
||||
for (uint32_t coefficientIndex = 0u; coefficientIndex < kGaussianSplatSHCoefficientCount; ++coefficientIndex) {
|
||||
EXPECT_NEAR(
|
||||
loaded.GetSHRecords()[sampleIndex].coefficients[coefficientIndex],
|
||||
source.GetSHRecords()[sampleIndex].coefficients[coefficientIndex],
|
||||
1.0e-6f);
|
||||
}
|
||||
}
|
||||
|
||||
const auto* sourceChunkSection = static_cast<const GaussianSplatChunkRecord*>(
|
||||
source.GetSectionData(GaussianSplatSectionType::Chunks));
|
||||
const auto* loadedChunkSection = static_cast<const GaussianSplatChunkRecord*>(
|
||||
loaded.GetSectionData(GaussianSplatSectionType::Chunks));
|
||||
ASSERT_NE(sourceChunkSection, nullptr);
|
||||
ASSERT_NE(loadedChunkSection, nullptr);
|
||||
if (source.GetChunkCount() > 0u) {
|
||||
const uint32_t chunkIndices[] = { 0u, source.GetChunkCount() - 1u };
|
||||
for (uint32_t chunkIndex : chunkIndices) {
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].colR, sourceChunkSection[chunkIndex].colR);
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].colG, sourceChunkSection[chunkIndex].colG);
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].colB, sourceChunkSection[chunkIndex].colB);
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].colA, sourceChunkSection[chunkIndex].colA);
|
||||
ExpectVector3Near(
|
||||
Vector3(
|
||||
loadedChunkSection[chunkIndex].posX.x,
|
||||
loadedChunkSection[chunkIndex].posY.x,
|
||||
loadedChunkSection[chunkIndex].posZ.x),
|
||||
Vector3(
|
||||
sourceChunkSection[chunkIndex].posX.x,
|
||||
sourceChunkSection[chunkIndex].posY.x,
|
||||
sourceChunkSection[chunkIndex].posZ.x));
|
||||
ExpectVector3Near(
|
||||
Vector3(
|
||||
loadedChunkSection[chunkIndex].posX.y,
|
||||
loadedChunkSection[chunkIndex].posY.y,
|
||||
loadedChunkSection[chunkIndex].posZ.y),
|
||||
Vector3(
|
||||
sourceChunkSection[chunkIndex].posX.y,
|
||||
sourceChunkSection[chunkIndex].posY.y,
|
||||
sourceChunkSection[chunkIndex].posZ.y));
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].sclX, sourceChunkSection[chunkIndex].sclX);
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].sclY, sourceChunkSection[chunkIndex].sclY);
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].sclZ, sourceChunkSection[chunkIndex].sclZ);
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].shR, sourceChunkSection[chunkIndex].shR);
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].shG, sourceChunkSection[chunkIndex].shG);
|
||||
EXPECT_EQ(loadedChunkSection[chunkIndex].shB, sourceChunkSection[chunkIndex].shB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* GetScreenshotFilename(RHIType backendType, GaussianSplatDebugView debugView) {
|
||||
switch (backendType) {
|
||||
case RHIType::D3D12:
|
||||
return kD3D12Screenshot;
|
||||
return debugView == GaussianSplatDebugView::Alpha
|
||||
? kD3D12AlphaDebugScreenshot
|
||||
: kD3D12Screenshot;
|
||||
case RHIType::Vulkan:
|
||||
return kVulkanScreenshot;
|
||||
return debugView == GaussianSplatDebugView::Alpha
|
||||
? kVulkanAlphaDebugScreenshot
|
||||
: kVulkanScreenshot;
|
||||
case RHIType::OpenGL:
|
||||
default:
|
||||
return kOpenGLScreenshot;
|
||||
return debugView == GaussianSplatDebugView::Alpha
|
||||
? kOpenGLAlphaDebugScreenshot
|
||||
: kOpenGLScreenshot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +357,8 @@ GaussianSplat* CreateGaussianSplatSubset(
|
||||
std::vector<GaussianSplatOtherRecord> subsetOther(subsetSplatCount);
|
||||
std::vector<GaussianSplatColorRecord> subsetColors(subsetSplatCount);
|
||||
std::vector<GaussianSplatSHRecord> subsetSh(shSection != nullptr && sh != nullptr ? subsetSplatCount : 0u);
|
||||
Bounds subsetBounds;
|
||||
bool hasSubsetBounds = false;
|
||||
for (uint32_t subsetIndex = 0u; subsetIndex < subsetSplatCount; ++subsetIndex) {
|
||||
const uint32_t sourceIndex = selectedIndices[subsetIndex];
|
||||
subsetPositions[subsetIndex] = positions[sourceIndex];
|
||||
@@ -188,6 +367,14 @@ GaussianSplat* CreateGaussianSplatSubset(
|
||||
if (!subsetSh.empty()) {
|
||||
subsetSh[subsetIndex] = sh[sourceIndex];
|
||||
}
|
||||
|
||||
const Vector3& position = subsetPositions[subsetIndex].position;
|
||||
if (!hasSubsetBounds) {
|
||||
subsetBounds.SetMinMax(position, position);
|
||||
hasSubsetBounds = true;
|
||||
} else {
|
||||
subsetBounds.Encapsulate(position);
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t subsetChunkCount =
|
||||
@@ -296,7 +483,7 @@ GaussianSplat* CreateGaussianSplatSubset(
|
||||
|
||||
GaussianSplatMetadata metadata = source.GetMetadata();
|
||||
metadata.splatCount = subsetSplatCount;
|
||||
metadata.bounds = source.GetBounds();
|
||||
metadata.bounds = subsetBounds;
|
||||
metadata.chunkCount = subsetChunkCount;
|
||||
metadata.cameraCount = 0u;
|
||||
metadata.chunkFormat = GaussianSplatSectionFormat::ChunkFloat32;
|
||||
@@ -336,10 +523,12 @@ private:
|
||||
ResourceHandle<GaussianSplat> m_gaussianSplat;
|
||||
ResourceHandle<GaussianSplat> m_subsetGaussianSplat;
|
||||
Material* m_material = nullptr;
|
||||
GaussianSplatDebugView m_debugView = GaussianSplatDebugView::Scene;
|
||||
};
|
||||
|
||||
void GaussianSplatSceneTest::SetUp() {
|
||||
RHIIntegrationFixture::SetUp();
|
||||
m_debugView = GetDebugViewFromEnvironment();
|
||||
PrepareRuntimeProject();
|
||||
|
||||
m_sceneRenderer = std::make_unique<SceneRenderer>();
|
||||
@@ -452,7 +641,7 @@ void GaussianSplatSceneTest::PrepareRuntimeProject() {
|
||||
std::unique_ptr<GaussianSplat> subsetGaussianSplat(
|
||||
CreateGaussianSplatSubset(
|
||||
*m_gaussianSplat.Get(),
|
||||
kBaselineSubsetSplatCount,
|
||||
GetUIntFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_SUBSET_COUNT", kBaselineSubsetSplatCount),
|
||||
kSubsetGaussianSplatAssetPath));
|
||||
ASSERT_NE(subsetGaussianSplat, nullptr);
|
||||
ASSERT_TRUE(subsetGaussianSplat->IsValid());
|
||||
@@ -474,6 +663,7 @@ void GaussianSplatSceneTest::PrepareRuntimeProject() {
|
||||
ASSERT_GT(m_subsetGaussianSplat->GetSplatCount(), 0u);
|
||||
ASSERT_EQ(m_subsetGaussianSplat->GetSHOrder(), 3u);
|
||||
ASSERT_NE(m_subsetGaussianSplat->FindSection(GaussianSplatSectionType::Chunks), nullptr);
|
||||
ExpectGaussianSplatRoundTripMatches(*subsetGaussianSplat, *m_subsetGaussianSplat.Get());
|
||||
|
||||
database.Shutdown();
|
||||
}
|
||||
@@ -490,8 +680,11 @@ void GaussianSplatSceneTest::BuildScene() {
|
||||
m_material->Initialize(params);
|
||||
m_material->SetShader(ResourceManager::Get().Load<Shader>(GetBuiltinGaussianSplatShaderPath()));
|
||||
m_material->SetRenderQueue(MaterialRenderQueue::Transparent);
|
||||
m_material->SetFloat("_PointScale", kGaussianPointScale);
|
||||
m_material->SetFloat(
|
||||
"_PointScale",
|
||||
GetFloatFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_POINT_SCALE", kGaussianPointScale));
|
||||
m_material->SetFloat("_OpacityScale", 1.0f);
|
||||
m_material->SetFloat("_DebugViewMode", m_debugView == GaussianSplatDebugView::Alpha ? 1.0f : 0.0f);
|
||||
|
||||
GameObject* cameraObject = m_scene->CreateGameObject("MainCamera");
|
||||
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
||||
@@ -512,10 +705,12 @@ void GaussianSplatSceneTest::BuildScene() {
|
||||
const float sizeY = std::max(boundsMax.y - boundsMin.y, 0.001f);
|
||||
const float sizeZ = std::max(boundsMax.z - boundsMin.z, 0.001f);
|
||||
const float maxExtent = std::max(sizeX, std::max(sizeY, sizeZ));
|
||||
const float uniformScale = kTargetSceneExtent / maxExtent;
|
||||
const float uniformScale =
|
||||
GetFloatFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_TARGET_EXTENT", kTargetSceneExtent) / maxExtent;
|
||||
|
||||
GameObject* root = m_scene->CreateGameObject("GaussianSplatRoot");
|
||||
root->GetTransform()->SetLocalPosition(Vector3::Zero());
|
||||
root->GetTransform()->SetLocalPosition(
|
||||
GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_ROOT_POS", kDefaultRootPosition));
|
||||
root->GetTransform()->SetLocalScale(Vector3(uniformScale, uniformScale, uniformScale));
|
||||
|
||||
GameObject* splatObject = m_scene->CreateGameObject("RoomGaussianSplat");
|
||||
@@ -527,7 +722,11 @@ void GaussianSplatSceneTest::BuildScene() {
|
||||
splatRenderer->SetMaterial(m_material);
|
||||
splatRenderer->SetCastShadows(false);
|
||||
splatRenderer->SetReceiveShadows(false);
|
||||
cameraObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 1.0f, 1.0f));
|
||||
|
||||
cameraObject->GetTransform()->SetLocalPosition(
|
||||
GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_CAMERA_POS", kDefaultCameraPosition));
|
||||
cameraObject->GetTransform()->LookAt(
|
||||
GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_CAMERA_LOOK_AT", kDefaultCameraLookAt));
|
||||
}
|
||||
|
||||
RHIResourceView* GaussianSplatSceneTest::GetCurrentBackBufferView() {
|
||||
@@ -585,7 +784,8 @@ TEST_P(GaussianSplatSceneTest, RenderRoomGaussianSplatScene) {
|
||||
ASSERT_NE(swapChain, nullptr);
|
||||
|
||||
constexpr int kTargetFrameCount = 2;
|
||||
const char* screenshotFilename = GetScreenshotFilename(GetBackendType());
|
||||
const GaussianSplatDebugView debugView = GetDebugViewFromEnvironment();
|
||||
const char* screenshotFilename = GetScreenshotFilename(GetBackendType(), debugView);
|
||||
|
||||
for (int frameCount = 0; frameCount <= kTargetFrameCount; ++frameCount) {
|
||||
if (frameCount > 0) {
|
||||
@@ -599,6 +799,11 @@ TEST_P(GaussianSplatSceneTest, RenderRoomGaussianSplatScene) {
|
||||
commandQueue->WaitForIdle();
|
||||
ASSERT_TRUE(TakeScreenshot(screenshotFilename));
|
||||
|
||||
if (debugView != GaussianSplatDebugView::Scene) {
|
||||
SUCCEED() << "Debug view screenshot captured for inspection: " << screenshotFilename;
|
||||
break;
|
||||
}
|
||||
|
||||
const std::filesystem::path gtPath = ResolveRuntimePath("GT.ppm");
|
||||
if (!std::filesystem::exists(gtPath)) {
|
||||
GTEST_SKIP() << "GT.ppm missing, screenshot captured for baseline generation: " << screenshotFilename;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
@@ -109,6 +110,19 @@ bool PumpAsyncLoadsUntil(ResourceManager& manager,
|
||||
return condition();
|
||||
}
|
||||
|
||||
std::filesystem::path GetRepositoryRoot() {
|
||||
std::filesystem::path current = std::filesystem::path(__FILE__).parent_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "project") &&
|
||||
std::filesystem::exists(current / "engine")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
|
||||
return std::filesystem::path(__FILE__).parent_path();
|
||||
}
|
||||
|
||||
bool DirectoryHasEntries(const std::filesystem::path& directoryPath) {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(directoryPath, ec) || !std::filesystem::is_directory(directoryPath, ec)) {
|
||||
@@ -118,6 +132,38 @@ bool DirectoryHasEntries(const std::filesystem::path& directoryPath) {
|
||||
return std::filesystem::directory_iterator(directoryPath) != std::filesystem::directory_iterator();
|
||||
}
|
||||
|
||||
std::vector<std::string> SplitTabSeparatedLine(const std::string& line) {
|
||||
std::vector<std::string> fields;
|
||||
std::stringstream stream(line);
|
||||
std::string field;
|
||||
while (std::getline(stream, field, '\t')) {
|
||||
fields.push_back(field);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
std::string ReadSourceHashFromAssetsDb(const std::filesystem::path& assetsDbPath,
|
||||
const std::string& relativePath) {
|
||||
std::ifstream input(assetsDbPath, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::vector<std::string> fields = SplitTabSeparatedLine(line);
|
||||
if (fields.size() > 7 && fields[1] == relativePath) {
|
||||
return fields[7];
|
||||
}
|
||||
}
|
||||
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> ListArtifactEntries(const std::filesystem::path& artifactsRoot) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -424,6 +470,47 @@ TEST(AssetImportService_Test, EnsureArtifactExposesContainerEntryRuntimeLoadPath
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(AssetImportService_Test, BootstrapProjectDefersSourceHashUntilArtifactIsNeeded) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
AssetImportService importService;
|
||||
importService.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_deferred_source_hash_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
importService.SetProjectRoot(projectRoot.string().c_str());
|
||||
ASSERT_TRUE(importService.BootstrapProject());
|
||||
|
||||
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
||||
const fs::path assetsDbPath = libraryRoot / "assets.db";
|
||||
ASSERT_TRUE(fs::exists(assetsDbPath));
|
||||
EXPECT_FALSE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
||||
EXPECT_TRUE(ReadSourceHashFromAssetsDb(assetsDbPath, "Assets/runtime.material").empty());
|
||||
|
||||
AssetImportService::ImportedAsset importedAsset;
|
||||
ASSERT_TRUE(importService.EnsureArtifact("Assets/runtime.material", ResourceType::Material, importedAsset));
|
||||
EXPECT_TRUE(importedAsset.exists);
|
||||
EXPECT_TRUE(importedAsset.artifactReady);
|
||||
EXPECT_TRUE(importedAsset.imported);
|
||||
EXPECT_FALSE(ReadSourceHashFromAssetsDb(assetsDbPath, "Assets/runtime.material").empty());
|
||||
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
||||
|
||||
importService.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ResourceManager_Test, RebuildProjectAssetCacheRefreshesLookupState) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -545,6 +632,58 @@ TEST(ResourceManager_Test, SetResourceRootBootstrapsProjectAssetCache) {
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ResourceManager_ProjectSample, BootstrapProjectKeepsCloudSourceHashDeferred) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path repositoryRoot = GetRepositoryRoot();
|
||||
const fs::path projectRoot = repositoryRoot / "project";
|
||||
const fs::path volumePath = projectRoot / "Assets" / "cloud.nvdb";
|
||||
|
||||
if (!fs::exists(volumePath)) {
|
||||
GTEST_SKIP() << "Project cloud volume fixture is not available.";
|
||||
}
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
struct ResourceManagerGuard {
|
||||
ResourceManager* manager = nullptr;
|
||||
~ResourceManagerGuard() {
|
||||
if (manager != nullptr) {
|
||||
manager->Shutdown();
|
||||
}
|
||||
}
|
||||
} resourceManagerGuard{ &manager };
|
||||
|
||||
struct CurrentPathGuard {
|
||||
fs::path previousPath;
|
||||
~CurrentPathGuard() {
|
||||
if (!previousPath.empty()) {
|
||||
fs::current_path(previousPath);
|
||||
}
|
||||
}
|
||||
} currentPathGuard{ fs::current_path() };
|
||||
|
||||
fs::current_path(projectRoot);
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
AssetRef volumeRef;
|
||||
EXPECT_TRUE(manager.TryGetAssetRef("Assets/cloud.nvdb", ResourceType::VolumeField, volumeRef));
|
||||
EXPECT_TRUE(volumeRef.IsValid());
|
||||
|
||||
const AssetImportService::ImportStatusSnapshot status = manager.GetProjectAssetImportStatus();
|
||||
EXPECT_TRUE(status.HasValue());
|
||||
EXPECT_FALSE(status.inProgress);
|
||||
EXPECT_TRUE(status.success);
|
||||
EXPECT_EQ(std::string(status.operation.CStr()), "Bootstrap Project");
|
||||
|
||||
const fs::path assetsDbPath = projectRoot / "Library" / "assets.db";
|
||||
ASSERT_TRUE(fs::exists(assetsDbPath));
|
||||
EXPECT_TRUE(ReadSourceHashFromAssetsDb(assetsDbPath, "Assets/cloud.nvdb").empty());
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
}
|
||||
|
||||
TEST(AssetImportService_Test, ClearLibraryAndReimportAllAssetsManageArtifactsExplicitly) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ std::shared_ptr<XCEngine::Editor::AssetItem> MakeModelAssetItem(
|
||||
void ConfigurePreviewCamera(XCEngine::Components::GameObject& gameObject) {
|
||||
using namespace XCEngine;
|
||||
|
||||
gameObject.GetTransform()->SetLocalPosition(Math::Vector3(0.0f, 1.2f, -4.25f));
|
||||
gameObject.GetTransform()->SetLocalPosition(Math::Vector3(0.0f, 1.3f, -2.75f));
|
||||
gameObject.GetTransform()->SetLocalRotation(Math::Quaternion(0.104528f, 0.0f, 0.0f, 0.994522f));
|
||||
|
||||
auto* camera = gameObject.AddComponent<Components::CameraComponent>();
|
||||
|
||||
Reference in New Issue
Block a user