#include #include #include #include #include #include #include #include #include #include #if defined(XCENGINE_HAS_NANOVDB) #include #include #include #endif #include #include #include using namespace XCEngine::Resources; namespace { struct GeneratedNanoVDBVolume { size_t payloadSize = 0u; XCEngine::Math::Bounds bounds; XCEngine::Math::Vector3 voxelSize = XCEngine::Math::Vector3::Zero(); VolumeIndexBounds indexBounds = {}; XCEngine::Core::uint32 gridType = 0u; XCEngine::Core::uint32 gridClass = 0u; }; std::filesystem::path GetCloudVolumePath() { return std::filesystem::path(XCENGINE_TEST_CLOUD_NVDB_PATH); } void ExpectVector3Near( const XCEngine::Math::Vector3& actual, const XCEngine::Math::Vector3& expected, float epsilon = 1e-4f) { EXPECT_NEAR(actual.x, expected.x, epsilon); EXPECT_NEAR(actual.y, expected.y, epsilon); EXPECT_NEAR(actual.z, expected.z, epsilon); } void ExpectIndexBoundsEq( const VolumeIndexBounds& actual, const VolumeIndexBounds& expected) { EXPECT_EQ(actual.minX, expected.minX); EXPECT_EQ(actual.minY, expected.minY); EXPECT_EQ(actual.minZ, expected.minZ); EXPECT_EQ(actual.maxX, expected.maxX); EXPECT_EQ(actual.maxY, expected.maxY); EXPECT_EQ(actual.maxZ, expected.maxZ); } #if defined(XCENGINE_HAS_NANOVDB) XCEngine::Math::Vector3 ToVector3(const nanovdb::Vec3d& value) { return XCEngine::Math::Vector3( static_cast(value[0]), static_cast(value[1]), static_cast(value[2])); } VolumeIndexBounds ToIndexBounds(const nanovdb::CoordBBox& value) { VolumeIndexBounds bounds = {}; bounds.minX = value.min()[0]; bounds.minY = value.min()[1]; bounds.minZ = value.min()[2]; bounds.maxX = value.max()[0]; bounds.maxY = value.max()[1]; bounds.maxZ = value.max()[2]; return bounds; } GeneratedNanoVDBVolume ReadTestNanoVDBFileMetadata(const std::filesystem::path& path) { nanovdb::GridHandle handle = nanovdb::io::readGrid(path.string()); GeneratedNanoVDBVolume generated; generated.payloadSize = static_cast(handle.bufferSize()); const nanovdb::GridMetaData* metadata = handle.gridMetaData(); if (metadata != nullptr) { generated.bounds.SetMinMax( ToVector3(metadata->worldBBox().min()), ToVector3(metadata->worldBBox().max())); generated.voxelSize = ToVector3(metadata->voxelSize()); generated.indexBounds = ToIndexBounds(metadata->indexBBox()); generated.gridType = static_cast(metadata->gridType()); generated.gridClass = static_cast(metadata->gridClass()); } return generated; } #endif TEST(VolumeFieldLoader, GetResourceType) { VolumeFieldLoader loader; EXPECT_EQ(loader.GetResourceType(), ResourceType::VolumeField); } TEST(VolumeFieldLoader, CanLoad) { VolumeFieldLoader loader; EXPECT_TRUE(loader.CanLoad("cloud.nvdb")); EXPECT_TRUE(loader.CanLoad("cloud.xcvol")); EXPECT_FALSE(loader.CanLoad("cloud.txt")); } TEST(VolumeFieldLoader, LoadInvalidPath) { VolumeFieldLoader loader; LoadResult result = loader.Load("invalid/path/cloud.nvdb"); EXPECT_FALSE(result); } #if defined(XCENGINE_HAS_NANOVDB) TEST(VolumeFieldLoader, LoadSourceNanoVDBGridPayload) { namespace fs = std::filesystem; const fs::path volumePath = GetCloudVolumePath(); ASSERT_TRUE(fs::exists(volumePath)); const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(volumePath); VolumeFieldLoader loader; LoadResult result = loader.Load(volumePath.string().c_str()); ASSERT_TRUE(result); ASSERT_NE(result.resource, nullptr); auto* volumeField = static_cast(result.resource); EXPECT_EQ(volumeField->GetStorageKind(), VolumeStorageKind::NanoVDB); EXPECT_EQ(volumeField->GetPayloadSize(), generated.payloadSize); ExpectVector3Near(volumeField->GetBounds().GetMin(), generated.bounds.GetMin()); ExpectVector3Near(volumeField->GetBounds().GetMax(), generated.bounds.GetMax()); ExpectVector3Near(volumeField->GetVoxelSize(), generated.voxelSize); ExpectIndexBoundsEq(volumeField->GetIndexBounds(), generated.indexBounds); EXPECT_EQ(volumeField->GetGridType(), generated.gridType); EXPECT_EQ(volumeField->GetGridClass(), generated.gridClass); delete volumeField; } #endif #if defined(XCENGINE_HAS_NANOVDB) TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReimport) { namespace fs = std::filesystem; using namespace std::chrono_literals; const fs::path projectRoot = fs::temp_directory_path() / "xc_volume_library_cache_test"; const fs::path assetsDir = projectRoot / "Assets"; const fs::path volumePath = assetsDir / "cloud.nvdb"; const fs::path fixturePath = GetCloudVolumePath(); fs::remove_all(projectRoot); fs::create_directories(assetsDir); ASSERT_TRUE(fs::exists(fixturePath)); fs::copy_file(fixturePath, volumePath, fs::copy_options::overwrite_existing); const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(fixturePath); { AssetDatabase database; database.Initialize(projectRoot.string().c_str()); AssetDatabase::ResolvedAsset firstResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/cloud.nvdb", ResourceType::VolumeField, firstResolve)); ASSERT_TRUE(firstResolve.exists); ASSERT_TRUE(firstResolve.artifactReady); EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr())); EXPECT_FALSE(firstResolve.artifactMainEntryPath.Empty()); EXPECT_NE(firstResolve.artifactMainEntryPath, firstResolve.artifactMainPath); EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().generic_string(), ".xcvol"); XCEngine::Containers::Array artifactPayload; EXPECT_TRUE(ReadArtifactContainerMainEntryPayload( firstResolve.artifactMainPath, ResourceType::VolumeField, artifactPayload)); ASSERT_GE(artifactPayload.Size(), sizeof(VolumeFieldArtifactHeader)); VolumeFieldArtifactHeader artifactHeader = {}; std::memcpy(&artifactHeader, artifactPayload.Data(), sizeof(artifactHeader)); EXPECT_EQ(std::memcmp(artifactHeader.magic, "XCVOL02", 7), 0); VolumeFieldLoader loader; LoadResult artifactLoad = loader.Load(firstResolve.artifactMainPath); ASSERT_TRUE(artifactLoad); ASSERT_NE(artifactLoad.resource, nullptr); auto* artifactVolume = static_cast(artifactLoad.resource); EXPECT_EQ(artifactVolume->GetStorageKind(), VolumeStorageKind::NanoVDB); EXPECT_EQ(artifactVolume->GetPayloadSize(), generated.payloadSize); ExpectVector3Near(artifactVolume->GetBounds().GetMin(), generated.bounds.GetMin()); ExpectVector3Near(artifactVolume->GetBounds().GetMax(), generated.bounds.GetMax()); ExpectVector3Near(artifactVolume->GetVoxelSize(), generated.voxelSize); ExpectIndexBoundsEq(artifactVolume->GetIndexBounds(), generated.indexBounds); EXPECT_EQ(artifactVolume->GetGridType(), generated.gridType); EXPECT_EQ(artifactVolume->GetGridClass(), generated.gridClass); delete artifactVolume; LoadResult entryLoad = loader.Load(firstResolve.artifactMainEntryPath); ASSERT_TRUE(entryLoad); ASSERT_NE(entryLoad.resource, nullptr); delete entryLoad.resource; const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr()); std::this_thread::sleep_for(50ms); AssetDatabase::ResolvedAsset secondResolve; ASSERT_TRUE(database.EnsureArtifact("Assets/cloud.nvdb", ResourceType::VolumeField, secondResolve)); EXPECT_EQ(firstResolve.artifactMainPath, secondResolve.artifactMainPath); EXPECT_EQ(originalArtifactWriteTime, fs::last_write_time(secondResolve.artifactMainPath.CStr())); database.Shutdown(); } fs::remove_all(projectRoot); } #else TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReimport) { GTEST_SKIP() << "NanoVDB headers are unavailable in this build"; } #endif #if defined(XCENGINE_HAS_NANOVDB) TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) { namespace fs = std::filesystem; ResourceManager& manager = ResourceManager::Get(); manager.Initialize(); const fs::path projectRoot = fs::temp_directory_path() / "xc_volume_asset_ref_test"; const fs::path assetsDir = projectRoot / "Assets"; const fs::path volumePath = assetsDir / "cloud.nvdb"; const fs::path fixturePath = GetCloudVolumePath(); fs::remove_all(projectRoot); fs::create_directories(assetsDir); ASSERT_TRUE(fs::exists(fixturePath)); fs::copy_file(fixturePath, volumePath, fs::copy_options::overwrite_existing); const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(fixturePath); manager.SetResourceRoot(projectRoot.string().c_str()); { const auto firstHandle = manager.Load("Assets/cloud.nvdb"); ASSERT_TRUE(firstHandle.IsValid()); EXPECT_EQ(firstHandle->GetStorageKind(), VolumeStorageKind::NanoVDB); EXPECT_EQ(firstHandle->GetPayloadSize(), generated.payloadSize); ExpectVector3Near(firstHandle->GetBounds().GetMin(), generated.bounds.GetMin()); ExpectVector3Near(firstHandle->GetBounds().GetMax(), generated.bounds.GetMax()); ExpectVector3Near(firstHandle->GetVoxelSize(), generated.voxelSize); ExpectIndexBoundsEq(firstHandle->GetIndexBounds(), generated.indexBounds); EXPECT_EQ(firstHandle->GetGridType(), generated.gridType); EXPECT_EQ(firstHandle->GetGridClass(), generated.gridClass); AssetRef assetRef; ASSERT_TRUE(manager.TryGetAssetRef("Assets/cloud.nvdb", ResourceType::VolumeField, assetRef)); EXPECT_TRUE(assetRef.IsValid()); manager.UnloadAll(); const auto secondHandle = manager.Load(assetRef); ASSERT_TRUE(secondHandle.IsValid()); EXPECT_EQ(secondHandle->GetStorageKind(), VolumeStorageKind::NanoVDB); EXPECT_EQ(secondHandle->GetPayloadSize(), generated.payloadSize); ExpectVector3Near(secondHandle->GetBounds().GetMin(), generated.bounds.GetMin()); ExpectVector3Near(secondHandle->GetBounds().GetMax(), generated.bounds.GetMax()); ExpectVector3Near(secondHandle->GetVoxelSize(), generated.voxelSize); ExpectIndexBoundsEq(secondHandle->GetIndexBounds(), generated.indexBounds); EXPECT_EQ(secondHandle->GetGridType(), generated.gridType); EXPECT_EQ(secondHandle->GetGridClass(), generated.gridClass); } manager.SetResourceRoot(""); manager.Shutdown(); fs::remove_all(projectRoot); } #else TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) { GTEST_SKIP() << "NanoVDB headers are unavailable in this build"; } #endif } // namespace