diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index 6d0c7b02..1f424e20 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include "Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.h" #include +#include #include #include #include @@ -63,7 +65,24 @@ Containers::String ToContainersString(const std::string& value) { Containers::String NormalizeArtifactPathString(const fs::path& path); Containers::String NormalizeArtifactPathString(const Containers::String& path); +bool SerializeTextureArtifactPayload(const Texture& texture, + Containers::Array& outPayload); +bool SerializeMaterialArtifactPayload( + const Material& material, + Containers::Array& outPayload, + const std::unordered_map& textureArtifactPaths, + const std::unordered_map& textureAssetRefs, + const AssetDatabase* assetDatabase); +bool SerializeShaderArtifactPayload(const Shader& shader, + Containers::Array& outPayload); +bool WriteSingleEntryArtifactContainerFile(const fs::path& artifactPath, + ResourceType resourceType, + const Containers::Array& payload); constexpr const char* kModelSubAssetManifestFileName = "subassets.tsv"; +constexpr const char* kSourceAssetDbFileName = "assets.db"; +constexpr const char* kArtifactDbFileName = "artifacts.db"; +constexpr const char* kLegacySourceAssetDbDirectory = "SourceAssetDB"; +constexpr const char* kLegacyArtifactDbDirectory = "ArtifactDB"; struct ModelSubAssetManifestEntry { LocalID localID = kInvalidLocalID; @@ -114,6 +133,39 @@ bool IsProjectRelativePath(const fs::path& path) { generic.rfind("../", 0) != 0; } +void TryMigrateLegacyLibraryDatabaseFile( + const fs::path& libraryRoot, + const char* legacyDirectoryName, + const char* fileName) { + const fs::path legacyFilePath = libraryRoot / legacyDirectoryName / fileName; + const fs::path currentFilePath = libraryRoot / fileName; + + std::error_code ec; + if (fs::exists(currentFilePath, ec)) { + return; + } + + ec.clear(); + if (!fs::exists(legacyFilePath, ec)) { + return; + } + + fs::rename(legacyFilePath, currentFilePath, ec); + if (ec) { + ec.clear(); + fs::copy_file(legacyFilePath, currentFilePath, fs::copy_options::overwrite_existing, ec); + if (ec) { + return; + } + + ec.clear(); + fs::remove(legacyFilePath, ec); + } + + ec.clear(); + fs::remove(legacyFilePath.parent_path(), ec); +} + void AddUniqueDependencyPath(const fs::path& path, std::unordered_set& seenPaths, std::vector& outPaths) { @@ -133,6 +185,23 @@ void AddUniqueDependencyPath(const fs::path& path, } bool IsCurrentShaderArtifactFile(const fs::path& artifactPath) { + Containers::Array data; + Containers::String payloadError; + if (ReadArtifactContainerMainEntryPayload( + NormalizeArtifactPathString(artifactPath), + ResourceType::Shader, + data, + &payloadError)) { + if (data.Size() < sizeof(ShaderArtifactFileHeader)) { + return false; + } + + ShaderArtifactFileHeader header = {}; + std::memcpy(&header, data.Data(), sizeof(header)); + return std::memcmp(header.magic, "XCSHD06", 7) == 0 && + header.schemaVersion == kShaderArtifactSchemaVersion; + } + std::ifstream input(artifactPath, std::ios::binary); if (!input.is_open()) { return false; @@ -330,12 +399,19 @@ bool TryPrecompileShaderVariantForBackend(const Containers::String& shaderPath, RHI::CompiledSpirvShader compiledShader = {}; std::string errorMessage; + const RHI::SpirvTargetEnvironment spirvTargetEnvironment = + (targetBackend == ShaderBackend::OpenGL && + compileDesc.sourceLanguage == RHI::ShaderLanguage::HLSL) + ? RHI::SpirvTargetEnvironment::Vulkan + : (targetBackend == ShaderBackend::Vulkan + ? RHI::SpirvTargetEnvironment::Vulkan + : RHI::SpirvTargetEnvironment::OpenGL); const bool compiled = targetBackend == ShaderBackend::Vulkan ? RHI::CompileVulkanShader(compileDesc, compiledShader, &errorMessage) : RHI::CompileSpirvShader( compileDesc, - RHI::SpirvTargetEnvironment::OpenGL, + spirvTargetEnvironment, compiledShader, &errorMessage); if (!compiled) { @@ -705,7 +781,7 @@ bool TryResolveModelSubAssetArtifactPath(const fs::path& manifestPath, return false; } -void WriteString(std::ofstream& stream, const Containers::String& value) { +void WriteString(std::ostream& stream, const Containers::String& value) { const Core::uint32 length = static_cast(value.Length()); stream.write(reinterpret_cast(&length), sizeof(length)); if (length > 0) { @@ -713,6 +789,45 @@ void WriteString(std::ofstream& stream, const Containers::String& value) { } } +Containers::Array ToByteArray(const std::string& text) { + Containers::Array bytes; + if (text.empty()) { + return bytes; + } + + bytes.ResizeUninitialized(text.size()); + std::memcpy(bytes.Data(), text.data(), text.size()); + return bytes; +} + +bool SerializeTextureArtifactPayload(const Texture& texture, + Containers::Array& outPayload) { + std::ostringstream output(std::ios::binary | std::ios::out); + + TextureArtifactHeader header; + header.textureType = static_cast(texture.GetTextureType()); + header.textureFormat = static_cast(texture.GetFormat()); + header.width = texture.GetWidth(); + header.height = texture.GetHeight(); + header.depth = texture.GetDepth(); + header.mipLevels = texture.GetMipLevels(); + header.arraySize = texture.GetArraySize(); + header.pixelDataSize = static_cast(texture.GetPixelDataSize()); + + output.write(reinterpret_cast(&header), sizeof(header)); + if (texture.GetPixelDataSize() > 0) { + output.write(static_cast(texture.GetPixelData()), texture.GetPixelDataSize()); + } + + if (!output) { + outPayload.Clear(); + return false; + } + + outPayload = ToByteArray(output.str()); + return true; +} + Containers::String ReadString(std::ifstream& stream) { Core::uint32 length = 0; stream.read(reinterpret_cast(&length), sizeof(length)); @@ -730,35 +845,24 @@ Containers::String ReadString(std::ifstream& stream) { } bool WriteTextureArtifactFile(const fs::path& artifactPath, const Texture& texture) { + Containers::Array payload; + if (!SerializeTextureArtifactPayload(texture, payload)) { + return false; + } + std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); if (!output.is_open()) { return false; } - - TextureArtifactHeader header; - header.textureType = static_cast(texture.GetTextureType()); - header.textureFormat = static_cast(texture.GetFormat()); - header.width = texture.GetWidth(); - header.height = texture.GetHeight(); - header.depth = texture.GetDepth(); - header.mipLevels = texture.GetMipLevels(); - header.arraySize = texture.GetArraySize(); - header.pixelDataSize = static_cast(texture.GetPixelDataSize()); - - output.write(reinterpret_cast(&header), sizeof(header)); - if (texture.GetPixelDataSize() > 0) { - output.write(static_cast(texture.GetPixelData()), texture.GetPixelDataSize()); + if (!payload.Empty()) { + output.write(reinterpret_cast(payload.Data()), + static_cast(payload.Size())); } - return static_cast(output); } bool WriteVolumeFieldArtifactFile(const fs::path& artifactPath, const VolumeField& volumeField) { - std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); - if (!output.is_open()) { - return false; - } - + Containers::Array payload; VolumeFieldArtifactHeader header; header.storageKind = static_cast(volumeField.GetStorageKind()); header.boundsMin = volumeField.GetBounds().GetMin(); @@ -774,9 +878,22 @@ bool WriteVolumeFieldArtifactFile(const fs::path& artifactPath, const VolumeFiel header.gridClass = volumeField.GetGridClass(); header.payloadSize = static_cast(volumeField.GetPayloadSize()); - output.write(reinterpret_cast(&header), sizeof(header)); + payload.Resize(sizeof(header) + volumeField.GetPayloadSize()); + std::memcpy(payload.Data(), &header, sizeof(header)); if (volumeField.GetPayloadSize() > 0) { - output.write(static_cast(volumeField.GetPayloadData()), volumeField.GetPayloadSize()); + std::memcpy( + payload.Data() + sizeof(header), + volumeField.GetPayloadData(), + volumeField.GetPayloadSize()); + } + + std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + return false; + } + + if (!payload.Empty()) { + output.write(reinterpret_cast(payload.Data()), static_cast(payload.Size())); } return static_cast(output); @@ -856,17 +973,41 @@ Containers::String ResolveTextureBindingPath( return NormalizeArtifactPathString(material.GetTextureBindingPath(bindingIndex)); } - bool WriteMaterialArtifactFile( const fs::path& artifactPath, const Material& material, const std::unordered_map& textureArtifactPaths = {}, const std::unordered_map& textureAssetRefs = {}, const AssetDatabase* assetDatabase = nullptr) { + Containers::Array payload; + if (!SerializeMaterialArtifactPayload( + material, + payload, + textureArtifactPaths, + textureAssetRefs, + assetDatabase)) { + return false; + } + std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); if (!output.is_open()) { return false; } + if (!payload.Empty()) { + output.write(reinterpret_cast(payload.Data()), + static_cast(payload.Size())); + } + + return static_cast(output); +} + +bool SerializeMaterialArtifactPayload( + const Material& material, + Containers::Array& outPayload, + const std::unordered_map& textureArtifactPaths, + const std::unordered_map& textureAssetRefs, + const AssetDatabase* assetDatabase) { + std::ostringstream output(std::ios::binary | std::ios::out); MaterialArtifactFileHeader fileHeader; output.write(reinterpret_cast(&fileHeader), sizeof(fileHeader)); @@ -929,15 +1070,32 @@ bool WriteMaterialArtifactFile( WriteString(output, ResolveTextureBindingPath(material, bindingIndex, textureArtifactPaths)); } - return static_cast(output); -} - -bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) { - std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); - if (!output.is_open()) { + if (!output) { + outPayload.Clear(); return false; } + outPayload = ToByteArray(output.str()); + return true; +} + +bool WriteSingleEntryArtifactContainerFile(const fs::path& artifactPath, + ResourceType resourceType, + const Containers::Array& payload) { + ArtifactContainerWriter writer; + ArtifactContainerEntry entry; + entry.name = "main"; + entry.resourceType = resourceType; + entry.localID = kMainAssetLocalID; + entry.payload = payload; + writer.AddEntry(std::move(entry)); + return writer.WriteToFile(NormalizeArtifactPathString(artifactPath)); +} + +bool SerializeShaderArtifactPayload(const Shader& shader, + Containers::Array& outPayload) { + std::ostringstream output(std::ios::binary | std::ios::out); + ShaderArtifactFileHeader fileHeader; output.write(reinterpret_cast(&fileHeader), sizeof(fileHeader)); @@ -1037,6 +1195,30 @@ bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) } } + if (!output) { + outPayload.Clear(); + return false; + } + + outPayload = ToByteArray(output.str()); + return true; +} + +bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) { + Containers::Array payload; + if (!SerializeShaderArtifactPayload(shader, payload)) { + return false; + } + + std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + return false; + } + if (!payload.Empty()) { + output.write(reinterpret_cast(payload.Data()), + static_cast(payload.Size())); + } + return static_cast(output); } @@ -1103,10 +1285,20 @@ void AssetDatabase::Initialize(const Containers::String& projectRoot) { m_projectRoot = NormalizePathString(projectRoot); m_assetsRoot = NormalizePathString(fs::path(m_projectRoot.CStr()) / "Assets"); m_libraryRoot = NormalizePathString(fs::path(m_projectRoot.CStr()) / "Library"); - m_sourceDbPath = NormalizePathString(fs::path(m_libraryRoot.CStr()) / "SourceAssetDB" / "assets.db"); - m_artifactDbPath = NormalizePathString(fs::path(m_libraryRoot.CStr()) / "ArtifactDB" / "artifacts.db"); + m_sourceDbPath = + NormalizePathString(fs::path(m_libraryRoot.CStr()) / kSourceAssetDbFileName); + m_artifactDbPath = + NormalizePathString(fs::path(m_libraryRoot.CStr()) / kArtifactDbFileName); EnsureProjectLayout(); + TryMigrateLegacyLibraryDatabaseFile( + fs::path(m_libraryRoot.CStr()), + kLegacySourceAssetDbDirectory, + kSourceAssetDbFileName); + TryMigrateLegacyLibraryDatabaseFile( + fs::path(m_libraryRoot.CStr()), + kLegacyArtifactDbDirectory, + kArtifactDbFileName); LoadSourceAssetDB(); LoadArtifactDB(); } @@ -1441,9 +1633,7 @@ void AssetDatabase::EnsureProjectLayout() { std::error_code ec; fs::create_directories(fs::path(m_assetsRoot.CStr()), ec); ec.clear(); - fs::create_directories(fs::path(m_libraryRoot.CStr()) / "SourceAssetDB", ec); - ec.clear(); - fs::create_directories(fs::path(m_libraryRoot.CStr()) / "ArtifactDB", ec); + fs::create_directories(fs::path(m_libraryRoot.CStr()), ec); ec.clear(); fs::create_directories(fs::path(m_libraryRoot.CStr()) / "Artifacts", ec); } @@ -1666,12 +1856,15 @@ Core::uint32 AssetDatabase::CleanupOrphanedArtifacts() const { } std::unordered_set retainedArtifactPathKeys; - retainedArtifactPathKeys.reserve(m_artifactsByGuid.size()); + retainedArtifactPathKeys.reserve(m_artifactsByGuid.size() * 2u); for (const auto& [guid, record] : m_artifactsByGuid) { (void)guid; if (!record.artifactDirectory.Empty()) { retainedArtifactPathKeys.insert(ToStdString(MakeKey(record.artifactDirectory))); } + if (!record.mainArtifactPath.Empty()) { + retainedArtifactPathKeys.insert(ToStdString(MakeKey(record.mainArtifactPath))); + } } Core::uint32 removedArtifactCount = 0; @@ -1743,13 +1936,13 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, outRecord = SourceAssetRecord(); outRecord.relativePath = relativePath; outRecord.importerName = GetImporterNameForPath(relativePath, isFolder); - outRecord.importerVersion = kCurrentImporterVersion; + outRecord.importerVersion = GetCurrentImporterVersion(outRecord.importerName); } outRecord.relativePath = relativePath; outRecord.isFolder = isFolder; outRecord.importerName = GetImporterNameForPath(relativePath, isFolder); - outRecord.importerVersion = kCurrentImporterVersion; + outRecord.importerVersion = GetCurrentImporterVersion(outRecord.importerName); if (UsesExternalSyntheticSourceRecord(relativePath)) { if (!outRecord.guid.IsValid()) { @@ -1795,6 +1988,7 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, const fs::path metaPath(sourcePath.string() + ".meta"); outRecord.metaPath = NormalizeRelativePath(metaPath); const Containers::String expectedImporterName = GetImporterNameForPath(relativePath, isFolder); + const Core::uint32 expectedImporterVersion = GetCurrentImporterVersion(expectedImporterName); bool shouldRewriteMeta = false; if (!fs::exists(metaPath) || !ReadMetaFile(metaPath, outRecord) || !outRecord.guid.IsValid()) { @@ -1807,8 +2001,8 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, outRecord.importerName = expectedImporterName; shouldRewriteMeta = true; } - if (outRecord.importerVersion != kCurrentImporterVersion) { - outRecord.importerVersion = kCurrentImporterVersion; + if (outRecord.importerVersion != expectedImporterVersion) { + outRecord.importerVersion = expectedImporterVersion; shouldRewriteMeta = true; } @@ -1879,7 +2073,7 @@ bool AssetDatabase::ReadMetaFile(const fs::path& metaPath, inOutRecord.importerName = GetImporterNameForPath(inOutRecord.relativePath, inOutRecord.isFolder); } if (inOutRecord.importerVersion == 0) { - inOutRecord.importerVersion = kCurrentImporterVersion; + inOutRecord.importerVersion = GetCurrentImporterVersion(inOutRecord.importerName); } return true; @@ -1962,6 +2156,34 @@ Containers::String AssetDatabase::GetImporterNameForPath(const Containers::Strin return Containers::String("DefaultImporter"); } +Core::uint32 AssetDatabase::GetCurrentImporterVersion(const Containers::String& importerName) { + if (importerName == "TextureImporter") { + return 9; + } + + if (importerName == "MaterialImporter") { + return 8; + } + + if (importerName == "ShaderImporter") { + return 8; + } + + if (importerName == "ModelImporter") { + return 10; + } + + if (importerName == "GaussianSplatImporter") { + return 2; + } + + if (importerName == "VolumeFieldImporter") { + return 2; + } + + return kBaseImporterVersion; +} + ResourceType AssetDatabase::GetPrimaryResourceTypeForImporter(const Containers::String& importerName) { if (importerName == "UIViewImporter") { return ResourceType::UIView; @@ -2187,27 +2409,41 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath, bool AssetDatabase::ImportTextureAsset(const SourceAssetRecord& sourceRecord, ArtifactRecord& outRecord) { TextureLoader loader; - const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); - LoadResult result = loader.Load(absolutePath); + const Containers::String absolutePath = + NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); + const TextureImportSettings importSettings = + BuildTextureImportSettingsFromIdentifier(sourceRecord.relativePath); + LoadResult result = loader.Load(absolutePath, &importSettings); if (!result || result.resource == nullptr) { return false; } Texture* texture = static_cast(result.resource); const Containers::String artifactKey = BuildArtifactKey(sourceRecord); - const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); - const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xctex"); + const Containers::String legacyArtifactDir = BuildArtifactDirectory(artifactKey); + const Containers::String mainArtifactPath = BuildArtifactFilePath(artifactKey, ".xctex"); + const Containers::String artifactDir = + NormalizePathString(fs::path(mainArtifactPath.CStr()).parent_path()); std::error_code ec; - fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::remove_all(fs::path(m_projectRoot.CStr()) / legacyArtifactDir.CStr(), ec); ec.clear(); - fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::create_directories( + (fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()).parent_path(), + ec); if (ec) { delete texture; return false; } - const bool writeOk = WriteTextureArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *texture); + Containers::Array payload; + const bool serialized = SerializeTextureArtifactPayload(*texture, payload); + const bool writeOk = + serialized && + WriteSingleEntryArtifactContainerFile( + fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), + ResourceType::Texture, + payload); delete texture; if (!writeOk) { return false; @@ -2243,21 +2479,30 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord, std::vector dependencies; CollectMaterialDependencies(*material, dependencies); const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies); - const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); - const Containers::String mainArtifactPath = - NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmat"); + const Containers::String legacyArtifactDir = BuildArtifactDirectory(artifactKey); + const Containers::String mainArtifactPath = BuildArtifactFilePath(artifactKey, ".xcmat"); + const Containers::String artifactDir = + NormalizePathString(fs::path(mainArtifactPath.CStr()).parent_path()); std::error_code ec; - fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::remove_all(fs::path(m_projectRoot.CStr()) / legacyArtifactDir.CStr(), ec); ec.clear(); - fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::create_directories( + (fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()).parent_path(), + ec); if (ec) { delete material; return false; } + Containers::Array payload; + const bool serialized = SerializeMaterialArtifactPayload(*material, payload, {}, {}, this); const bool writeOk = - WriteMaterialArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *material, {}, {}, this); + serialized && + WriteSingleEntryArtifactContainerFile( + fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), + ResourceType::Material, + payload); delete material; if (!writeOk) { return false; @@ -2285,6 +2530,11 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord, NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); MeshImportSettings importSettings; + const std::string extension = ToLowerCopy(fs::path(sourceRecord.relativePath.CStr()).extension().string()); + if (extension == ".fbx") { + importSettings.AddImportFlag(MeshImportFlags::FlipUVs); + } + ImportedModelData importedModel; Containers::String importErrorMessage; if (!ImportAssimpModel(absolutePath, importSettings, importedModel, &importErrorMessage)) { @@ -2460,21 +2710,30 @@ bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord, return false; } const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies); - const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); - const Containers::String mainArtifactPath = - NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcshader"); + const Containers::String legacyArtifactDir = BuildArtifactDirectory(artifactKey); + const Containers::String mainArtifactPath = BuildArtifactFilePath(artifactKey, ".xcshader"); + const Containers::String artifactDir = + NormalizePathString(fs::path(mainArtifactPath.CStr()).parent_path()); std::error_code ec; - fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::remove_all(fs::path(m_projectRoot.CStr()) / legacyArtifactDir.CStr(), ec); ec.clear(); - fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::create_directories( + (fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()).parent_path(), + ec); if (ec) { delete shader; return false; } + Containers::Array payload; + const bool serialized = SerializeShaderArtifactPayload(*shader, payload); const bool writeOk = - WriteShaderArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *shader); + serialized && + WriteSingleEntryArtifactContainerFile( + fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), + ResourceType::Shader, + payload); delete shader; if (!writeOk) { return false; @@ -2511,26 +2770,32 @@ bool AssetDatabase::ImportGaussianSplatAsset(const SourceAssetRecord& sourceReco GaussianSplat* gaussianSplat = static_cast(result.resource); const Containers::String artifactKey = BuildArtifactKey(sourceRecord); - const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); - const Containers::String mainArtifactPath = - NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcgsplat"); + const Containers::String legacyArtifactDir = BuildArtifactDirectory(artifactKey); + const Containers::String mainArtifactPath = BuildArtifactFilePath(artifactKey, ".xcgsplat"); + const Containers::String artifactDir = + NormalizePathString(fs::path(mainArtifactPath.CStr()).parent_path()); std::error_code ec; - fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::remove_all(fs::path(m_projectRoot.CStr()) / legacyArtifactDir.CStr(), ec); ec.clear(); - fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::create_directories( + (fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()).parent_path(), + ec); if (ec) { delete gaussianSplat; return false; } Containers::String writeErrorMessage; - const Containers::String gaussianSplatArtifactWritePath = - NormalizePathString(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()); - const bool writeOk = WriteGaussianSplatArtifactFile( - gaussianSplatArtifactWritePath, - *gaussianSplat, - &writeErrorMessage); + Containers::Array payload; + const bool serialized = + SerializeGaussianSplatArtifactPayload(*gaussianSplat, payload, &writeErrorMessage); + const bool writeOk = + serialized && + WriteSingleEntryArtifactContainerFile( + fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), + ResourceType::GaussianSplat, + payload); delete gaussianSplat; if (!writeOk) { if (!writeErrorMessage.Empty()) { @@ -2567,21 +2832,52 @@ bool AssetDatabase::ImportVolumeFieldAsset(const SourceAssetRecord& sourceRecord VolumeField* volumeField = static_cast(result.resource); const Containers::String artifactKey = BuildArtifactKey(sourceRecord); - const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); - const Containers::String mainArtifactPath = - NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcvol"); + const Containers::String legacyArtifactDir = BuildArtifactDirectory(artifactKey); + const Containers::String mainArtifactPath = BuildArtifactFilePath(artifactKey, ".xcvol"); + const Containers::String artifactDir = + NormalizePathString(fs::path(mainArtifactPath.CStr()).parent_path()); std::error_code ec; - fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::remove_all(fs::path(m_projectRoot.CStr()) / legacyArtifactDir.CStr(), ec); ec.clear(); - fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + fs::create_directories( + (fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()).parent_path(), + ec); if (ec) { delete volumeField; return false; } + Containers::Array payload; + VolumeFieldArtifactHeader header; + header.storageKind = static_cast(volumeField->GetStorageKind()); + header.boundsMin = volumeField->GetBounds().GetMin(); + header.boundsMax = volumeField->GetBounds().GetMax(); + header.voxelSize = volumeField->GetVoxelSize(); + header.indexBoundsMin[0] = volumeField->GetIndexBounds().minX; + header.indexBoundsMin[1] = volumeField->GetIndexBounds().minY; + header.indexBoundsMin[2] = volumeField->GetIndexBounds().minZ; + header.indexBoundsMax[0] = volumeField->GetIndexBounds().maxX; + header.indexBoundsMax[1] = volumeField->GetIndexBounds().maxY; + header.indexBoundsMax[2] = volumeField->GetIndexBounds().maxZ; + header.gridType = volumeField->GetGridType(); + header.gridClass = volumeField->GetGridClass(); + header.payloadSize = static_cast(volumeField->GetPayloadSize()); + + payload.Resize(sizeof(header) + volumeField->GetPayloadSize()); + std::memcpy(payload.Data(), &header, sizeof(header)); + if (volumeField->GetPayloadSize() > 0) { + std::memcpy( + payload.Data() + sizeof(header), + volumeField->GetPayloadData(), + volumeField->GetPayloadSize()); + } + const bool writeOk = - WriteVolumeFieldArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *volumeField); + WriteSingleEntryArtifactContainerFile( + fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), + ResourceType::VolumeField, + payload); delete volumeField; if (!writeOk) { return false; @@ -2922,6 +3218,21 @@ Containers::String AssetDatabase::BuildArtifactDirectory(const Containers::Strin return Containers::String("Library/Artifacts/") + shard + "/" + artifactKey; } +Containers::String AssetDatabase::BuildArtifactFilePath(const Containers::String& artifactKey, + const char* extension) const { + if (artifactKey.Length() < 2) { + return Containers::String("Library/Artifacts/00/invalid") + + Containers::String(extension == nullptr ? "" : extension); + } + + const Containers::String shard = artifactKey.Substring(0, 2); + return Containers::String("Library/Artifacts/") + + shard + + "/" + + artifactKey + + Containers::String(extension == nullptr ? "" : extension); +} + Containers::String AssetDatabase::ReadWholeFileText(const fs::path& path) { std::ifstream input(path, std::ios::binary); if (!input.is_open()) { diff --git a/engine/src/Resources/Mesh/MeshLoader.cpp b/engine/src/Resources/Mesh/MeshLoader.cpp index 81fcbe93..b77fd048 100644 --- a/engine/src/Resources/Mesh/MeshLoader.cpp +++ b/engine/src/Resources/Mesh/MeshLoader.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -93,7 +94,7 @@ Core::uint32 BuildPostProcessFlags(const MeshImportSettings& settings) { if (settings.GetAxisConversion()) { flags |= aiProcess_MakeLeftHanded; - flags |= aiProcess_FlipWindingOrder; + // The engine treats CCW as front-facing, so keep the imported winding unchanged. } if (settings.HasImportFlag(MeshImportFlags::FlipUVs)) { @@ -174,11 +175,7 @@ Containers::String BuildSubResourcePath(const Containers::String& sourcePath, } TextureImportSettings BuildMaterialTextureImportSettings(const char* propertyName) { - TextureImportSettings settings; - (void)propertyName; - settings.SetSRGB(false); - settings.SetTargetFormat(TextureFormat::RGBA8_UNORM); - return settings; + return BuildTextureImportSettingsForMaterialProperty(propertyName); } std::string BuildTextureCacheKey(const std::string& pathKey, const TextureImportSettings& settings) { diff --git a/engine/src/Resources/Model/AssimpModelImporter.cpp b/engine/src/Resources/Model/AssimpModelImporter.cpp index 18b8150e..c6cfa7fc 100644 --- a/engine/src/Resources/Model/AssimpModelImporter.cpp +++ b/engine/src/Resources/Model/AssimpModelImporter.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -185,7 +186,7 @@ Core::uint32 BuildPostProcessFlags(const MeshImportSettings& settings) { if (settings.GetAxisConversion()) { flags |= aiProcess_MakeLeftHanded; - flags |= aiProcess_FlipWindingOrder; + // The engine treats CCW as front-facing, so keep the imported winding unchanged. } if (settings.HasImportFlag(MeshImportFlags::FlipUVs)) { @@ -214,11 +215,7 @@ Core::uint32 BuildPostProcessFlags(const MeshImportSettings& settings) { } TextureImportSettings BuildMaterialTextureImportSettings(const char* propertyName) { - TextureImportSettings settings; - (void)propertyName; - settings.SetSRGB(false); - settings.SetTargetFormat(TextureFormat::RGBA8_UNORM); - return settings; + return BuildTextureImportSettingsForMaterialProperty(propertyName); } std::string BuildTextureCacheKey(const std::string& pathKey, const TextureImportSettings& settings) { diff --git a/tests/Rendering/integration/nahida_preview_scene/main.cpp b/tests/Rendering/integration/nahida_preview_scene/main.cpp new file mode 100644 index 00000000..e9358791 --- /dev/null +++ b/tests/Rendering/integration/nahida_preview_scene/main.cpp @@ -0,0 +1,736 @@ +#define NOMINMAX +#include + +#include + +#include "../RenderingIntegrationImageAssert.h" +#include "../RenderingIntegrationMain.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Rendering; +using namespace XCEngine::Resources; +using namespace XCEngine::RHI; +using namespace XCEngine::RHI::Integration; +using namespace RenderingIntegrationTestUtils; + +namespace { + +constexpr const char* kD3D12Screenshot = "nahida_preview_scene_d3d12.ppm"; +constexpr uint32_t kFrameWidth = 1280; +constexpr uint32_t kFrameHeight = 720; +constexpr int kWarmupFrames = 45; + +enum class DiagnosticMode { + Original, + NoShadows, + ForwardLit, + Unlit +}; + +DiagnosticMode GetDiagnosticMode() { + const char* value = std::getenv("XC_NAHIDA_DIAG_MODE"); + if (value == nullptr) { + return DiagnosticMode::Original; + } + + const std::string mode(value); + if (mode == "no_shadows") { + return DiagnosticMode::NoShadows; + } + if (mode == "forward_lit") { + return DiagnosticMode::ForwardLit; + } + if (mode == "unlit") { + return DiagnosticMode::Unlit; + } + + return DiagnosticMode::Original; +} + +const char* GetDiagnosticModeName(DiagnosticMode mode) { + switch (mode) { + case DiagnosticMode::Original: return "original"; + case DiagnosticMode::NoShadows: return "no_shadows"; + case DiagnosticMode::ForwardLit: return "forward_lit"; + case DiagnosticMode::Unlit: return "unlit"; + default: return "unknown"; + } +} + +std::unordered_set GetIsolationObjectNames() { + std::unordered_set result; + const char* value = std::getenv("XC_NAHIDA_DIAG_ONLY"); + if (value == nullptr) { + return result; + } + + std::stringstream stream(value); + std::string token; + while (std::getline(stream, token, ',')) { + const size_t first = token.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + continue; + } + + const size_t last = token.find_last_not_of(" \t\r\n"); + result.emplace(token.substr(first, last - first + 1)); + } + + return result; +} + +std::string DescribeVertexAttributes(VertexAttribute attributes) { + std::string result; + const auto appendFlag = [&result](const char* name) { + if (!result.empty()) { + result += "|"; + } + result += name; + }; + + if (HasVertexAttribute(attributes, VertexAttribute::Position)) { + appendFlag("Position"); + } + if (HasVertexAttribute(attributes, VertexAttribute::Normal)) { + appendFlag("Normal"); + } + if (HasVertexAttribute(attributes, VertexAttribute::Tangent)) { + appendFlag("Tangent"); + } + if (HasVertexAttribute(attributes, VertexAttribute::Bitangent)) { + appendFlag("Bitangent"); + } + if (HasVertexAttribute(attributes, VertexAttribute::Color)) { + appendFlag("Color"); + } + if (HasVertexAttribute(attributes, VertexAttribute::UV0)) { + appendFlag("UV0"); + } + if (HasVertexAttribute(attributes, VertexAttribute::UV1)) { + appendFlag("UV1"); + } + + return result; +} + +void DumpMeshDiagnostics(const char* label, const Mesh* mesh) { + if (mesh == nullptr) { + std::printf("[NahidaDiag] %s mesh=null\n", label); + return; + } + + const XCEngine::Math::Bounds& bounds = mesh->GetBounds(); + std::printf( + "[NahidaDiag] %s meshPath=%s vertices=%u indices=%u stride=%u attrs=%s center=(%.4f, %.4f, %.4f) extents=(%.4f, %.4f, %.4f) sections=%zu\n", + label, + mesh->GetPath().CStr(), + mesh->GetVertexCount(), + mesh->GetIndexCount(), + mesh->GetVertexStride(), + DescribeVertexAttributes(mesh->GetVertexAttributes()).c_str(), + bounds.center.x, + bounds.center.y, + bounds.center.z, + bounds.extents.x, + bounds.extents.y, + bounds.extents.z, + mesh->GetSections().Size()); + + if (mesh->GetVertexStride() != sizeof(StaticMeshVertex) || mesh->GetVertexCount() == 0) { + return; + } + + const auto* vertices = static_cast(mesh->GetVertexData()); + XCEngine::Math::Vector2 uvMin( + std::numeric_limits::max(), + std::numeric_limits::max()); + XCEngine::Math::Vector2 uvMax( + std::numeric_limits::lowest(), + std::numeric_limits::lowest()); + for (uint32_t vertexIndex = 0; vertexIndex < mesh->GetVertexCount(); ++vertexIndex) { + uvMin.x = std::min(uvMin.x, vertices[vertexIndex].uv0.x); + uvMin.y = std::min(uvMin.y, vertices[vertexIndex].uv0.y); + uvMax.x = std::max(uvMax.x, vertices[vertexIndex].uv0.x); + uvMax.y = std::max(uvMax.y, vertices[vertexIndex].uv0.y); + } + + const StaticMeshVertex& firstVertex = vertices[0]; + std::printf( + "[NahidaDiag] %s firstVertex pos=(%.4f, %.4f, %.4f) normal=(%.4f, %.4f, %.4f) tangent=(%.4f, %.4f, %.4f) bitangent=(%.4f, %.4f, %.4f) uv=(%.4f, %.4f)\n", + label, + firstVertex.position.x, + firstVertex.position.y, + firstVertex.position.z, + firstVertex.normal.x, + firstVertex.normal.y, + firstVertex.normal.z, + firstVertex.tangent.x, + firstVertex.tangent.y, + firstVertex.tangent.z, + firstVertex.bitangent.x, + firstVertex.bitangent.y, + firstVertex.bitangent.z, + firstVertex.uv0.x, + firstVertex.uv0.y); + std::printf( + "[NahidaDiag] %s uvRange min=(%.4f, %.4f) max=(%.4f, %.4f)\n", + label, + uvMin.x, + uvMin.y, + uvMax.x, + uvMax.y); +} + +void DumpMaterialDiagnostics(const char* label, Material* material) { + if (material == nullptr) { + std::printf("[NahidaDiag] %s material=null\n", label); + return; + } + + std::printf( + "[NahidaDiag] %s materialPath=%s shader=%s constants=%zu textures=%u keywords=%u renderQueue=%d\n", + label, + material->GetPath().CStr(), + material->GetShader() != nullptr ? material->GetShader()->GetPath().CStr() : "", + material->GetConstantLayout().Size(), + material->GetTextureBindingCount(), + material->GetKeywordCount(), + material->GetRenderQueue()); + + std::printf( + "[NahidaDiag] %s props _BaseColor=(%.3f, %.3f, %.3f, %.3f) _ShadowColor=(%.3f, %.3f, %.3f, %.3f) _UseNormalMap=%.3f _UseSpecular=%.3f _UseEmission=%.3f _UseRim=%.3f _IsFace=%.3f _UseCustomMaterialType=%.3f\n", + label, + material->GetFloat4("_BaseColor").x, + material->GetFloat4("_BaseColor").y, + material->GetFloat4("_BaseColor").z, + material->GetFloat4("_BaseColor").w, + material->GetFloat4("_ShadowColor").x, + material->GetFloat4("_ShadowColor").y, + material->GetFloat4("_ShadowColor").z, + material->GetFloat4("_ShadowColor").w, + material->GetFloat("_UseNormalMap"), + material->GetFloat("_UseSpecular"), + material->GetFloat("_UseEmission"), + material->GetFloat("_UseRim"), + material->GetFloat("_IsFace"), + material->GetFloat("_UseCustomMaterialType")); + + for (uint32_t keywordIndex = 0; keywordIndex < material->GetKeywordCount(); ++keywordIndex) { + std::printf("[NahidaDiag] %s keyword[%u]=%s\n", label, keywordIndex, material->GetKeyword(keywordIndex).CStr()); + } + + for (uint32_t bindingIndex = 0; bindingIndex < material->GetTextureBindingCount(); ++bindingIndex) { + const ResourceHandle textureHandle = material->GetTextureBindingTexture(bindingIndex); + Texture* texture = textureHandle.Get(); + std::printf( + "[NahidaDiag] %s texture[%u] name=%s path=%s loaded=%d resolved=%s\n", + label, + bindingIndex, + material->GetTextureBindingName(bindingIndex).CStr(), + material->GetTextureBindingPath(bindingIndex).CStr(), + texture != nullptr ? 1 : 0, + texture != nullptr ? texture->GetPath().CStr() : ""); + } +} + +void DumpSceneDiagnostics(Scene& scene) { + RenderSceneExtractor extractor; + RenderSceneData sceneData = extractor.Extract(scene, nullptr, kFrameWidth, kFrameHeight); + std::printf( + "[NahidaDiag] extracted cameraPos=(%.3f, %.3f, %.3f) visibleItems=%zu additionalLights=%u\n", + sceneData.cameraData.worldPosition.x, + sceneData.cameraData.worldPosition.y, + sceneData.cameraData.worldPosition.z, + sceneData.visibleItems.size(), + sceneData.lighting.additionalLightCount); + + for (size_t visibleIndex = 0; visibleIndex < sceneData.visibleItems.size(); ++visibleIndex) { + const VisibleRenderItem& item = sceneData.visibleItems[visibleIndex]; + const char* objectName = item.gameObject != nullptr ? item.gameObject->GetName().c_str() : ""; + const char* meshPath = item.mesh != nullptr ? item.mesh->GetPath().CStr() : ""; + const char* materialPath = item.material != nullptr ? item.material->GetPath().CStr() : ""; + std::printf( + "[NahidaDiag] visible[%zu] object=%s section=%u hasSection=%d materialIndex=%u queue=%d mesh=%s material=%s\n", + visibleIndex, + objectName, + item.sectionIndex, + item.hasSection ? 1 : 0, + item.materialIndex, + item.renderQueue, + meshPath, + materialPath); + } +} + +std::filesystem::path GetRepositoryRoot() { + return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path().parent_path().parent_path(); +} + +std::filesystem::path GetProjectRoot() { + return GetRepositoryRoot() / "project"; +} + +std::filesystem::path GetScenePath() { + return GetProjectRoot() / "Assets" / "Scenes" / "NahidaPreview.xc"; +} + +std::filesystem::path GetAssimpDllPath() { + return GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll"; +} + +class NahidaPreviewSceneTest : public RHIIntegrationFixture { +protected: + void SetUp() override; + void TearDown() override; + void RenderFrame() override; + +private: + void InitializeProjectResources(); + void PreloadSceneResources(); + void PumpSceneLoads(std::chrono::milliseconds timeout); + void TouchSceneMaterialsAndTextures(); + void ApplyIsolationFilter(); + void ApplyDiagnosticOverrides(); + void DumpTargetDiagnostics(); + RHIResourceView* GetCurrentBackBufferView(); + Material* CreateOverrideMaterial(Material* sourceMaterial, const char* shaderPath); + + HMODULE m_assimpModule = nullptr; + std::unique_ptr m_scene; + std::unique_ptr m_sceneRenderer; + std::vector> m_overrideMaterials; + std::vector m_backBufferViews; + RHITexture* m_depthTexture = nullptr; + RHIResourceView* m_depthView = nullptr; +}; + +void NahidaPreviewSceneTest::SetUp() { + RHIIntegrationFixture::SetUp(); + + const std::filesystem::path assimpDllPath = GetAssimpDllPath(); + ASSERT_TRUE(std::filesystem::exists(assimpDllPath)) << assimpDllPath.string(); + m_assimpModule = LoadLibraryW(assimpDllPath.wstring().c_str()); + ASSERT_NE(m_assimpModule, nullptr); + + InitializeProjectResources(); + + const std::filesystem::path scenePath = GetScenePath(); + ASSERT_TRUE(std::filesystem::exists(scenePath)) << scenePath.string(); + + m_sceneRenderer = std::make_unique(); + m_scene = std::make_unique("NahidaPreview"); + m_scene->Load(scenePath.string()); + + ASSERT_NE(m_scene->Find("Preview Camera"), nullptr); + ASSERT_NE(m_scene->Find("NahidaUnityModel"), nullptr); + ASSERT_NE(m_scene->Find("Body_Mesh0"), nullptr); + ASSERT_NE(m_scene->Find("Face"), nullptr); + + PreloadSceneResources(); + ApplyIsolationFilter(); + ApplyDiagnosticOverrides(); + DumpTargetDiagnostics(); + + TextureDesc depthDesc = {}; + depthDesc.width = kFrameWidth; + depthDesc.height = kFrameHeight; + depthDesc.depth = 1; + depthDesc.mipLevels = 1; + depthDesc.arraySize = 1; + depthDesc.format = static_cast(Format::D24_UNorm_S8_UInt); + depthDesc.textureType = static_cast(XCEngine::RHI::TextureType::Texture2D); + depthDesc.sampleCount = 1; + depthDesc.sampleQuality = 0; + depthDesc.flags = 0; + m_depthTexture = GetDevice()->CreateTexture(depthDesc); + ASSERT_NE(m_depthTexture, nullptr); + + ResourceViewDesc depthViewDesc = {}; + depthViewDesc.format = static_cast(Format::D24_UNorm_S8_UInt); + depthViewDesc.dimension = ResourceViewDimension::Texture2D; + depthViewDesc.mipLevel = 0; + m_depthView = GetDevice()->CreateDepthStencilView(m_depthTexture, depthViewDesc); + ASSERT_NE(m_depthView, nullptr); + + m_backBufferViews.resize(2, nullptr); +} + +void NahidaPreviewSceneTest::TearDown() { + m_sceneRenderer.reset(); + + if (m_depthView != nullptr) { + m_depthView->Shutdown(); + delete m_depthView; + m_depthView = nullptr; + } + + if (m_depthTexture != nullptr) { + m_depthTexture->Shutdown(); + delete m_depthTexture; + m_depthTexture = nullptr; + } + + for (RHIResourceView*& backBufferView : m_backBufferViews) { + if (backBufferView != nullptr) { + backBufferView->Shutdown(); + delete backBufferView; + backBufferView = nullptr; + } + } + m_backBufferViews.clear(); + + m_scene.reset(); + + ResourceManager& manager = ResourceManager::Get(); + manager.UnloadAll(); + manager.SetResourceRoot(""); + manager.Shutdown(); + + if (m_assimpModule != nullptr) { + FreeLibrary(m_assimpModule); + m_assimpModule = nullptr; + } + + RHIIntegrationFixture::TearDown(); +} + +void NahidaPreviewSceneTest::InitializeProjectResources() { + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + manager.SetResourceRoot(GetProjectRoot().string().c_str()); +} + +void NahidaPreviewSceneTest::PreloadSceneResources() { + ASSERT_NE(m_scene, nullptr); + PumpSceneLoads(std::chrono::milliseconds(4000)); +} + +void NahidaPreviewSceneTest::PumpSceneLoads(std::chrono::milliseconds timeout) { + ResourceManager& manager = ResourceManager::Get(); + const auto deadline = std::chrono::steady_clock::now() + timeout; + + do { + TouchSceneMaterialsAndTextures(); + manager.UpdateAsyncLoads(); + + if (!manager.IsAsyncLoading()) { + TouchSceneMaterialsAndTextures(); + manager.UpdateAsyncLoads(); + if (!manager.IsAsyncLoading()) { + break; + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } while (std::chrono::steady_clock::now() < deadline); +} + +void NahidaPreviewSceneTest::TouchSceneMaterialsAndTextures() { + ASSERT_NE(m_scene, nullptr); + + const std::vector meshFilters = m_scene->FindObjectsOfType(); + for (MeshFilterComponent* meshFilter : meshFilters) { + if (meshFilter == nullptr) { + continue; + } + + meshFilter->GetMesh(); + } + + const std::vector meshRenderers = m_scene->FindObjectsOfType(); + for (MeshRendererComponent* meshRenderer : meshRenderers) { + if (meshRenderer == nullptr) { + continue; + } + + for (size_t materialIndex = 0; materialIndex < meshRenderer->GetMaterialCount(); ++materialIndex) { + Material* material = meshRenderer->GetMaterial(materialIndex); + if (material == nullptr) { + continue; + } + + for (uint32_t bindingIndex = 0; bindingIndex < material->GetTextureBindingCount(); ++bindingIndex) { + material->GetTextureBindingTexture(bindingIndex); + } + } + } +} + +void NahidaPreviewSceneTest::ApplyIsolationFilter() { + ASSERT_NE(m_scene, nullptr); + + const std::unordered_set isolatedObjects = GetIsolationObjectNames(); + if (isolatedObjects.empty()) { + return; + } + + std::printf("[NahidaDiag] isolation="); + bool first = true; + for (const std::string& name : isolatedObjects) { + std::printf("%s%s", first ? "" : ",", name.c_str()); + first = false; + } + std::printf("\n"); + + const std::vector meshRenderers = m_scene->FindObjectsOfType(); + for (MeshRendererComponent* meshRenderer : meshRenderers) { + if (meshRenderer == nullptr || meshRenderer->GetGameObject() == nullptr) { + continue; + } + + GameObject* gameObject = meshRenderer->GetGameObject(); + const bool keep = isolatedObjects.find(gameObject->GetName()) != isolatedObjects.end(); + gameObject->SetActive(keep); + } +} + +Material* NahidaPreviewSceneTest::CreateOverrideMaterial(Material* sourceMaterial, const char* shaderPath) { + if (sourceMaterial == nullptr || shaderPath == nullptr) { + return nullptr; + } + + auto material = std::make_unique(); + IResource::ConstructParams params = {}; + params.name = XCEngine::Containers::String("NahidaDiagnosticMaterial"); + params.path = XCEngine::Containers::String(("memory://nahida/" + std::string(shaderPath)).c_str()); + params.guid = ResourceGUID::Generate(params.path); + material->Initialize(params); + material->SetShader(ResourceManager::Get().Load(shaderPath)); + + const ResourceHandle baseMap = sourceMaterial->GetTexture("_BaseMap"); + if (baseMap.Get() != nullptr) { + material->SetTexture("_MainTex", baseMap); + } + + if (std::string(shaderPath) == "builtin://shaders/forward-lit") { + material->EnableKeyword("XC_ALPHA_TEST"); + material->SetFloat("_Cutoff", sourceMaterial->GetFloat("_Cutoff")); + } + + material->SetFloat4("_BaseColor", sourceMaterial->GetFloat4("_BaseColor")); + + m_overrideMaterials.push_back(std::move(material)); + return m_overrideMaterials.back().get(); +} + +void NahidaPreviewSceneTest::ApplyDiagnosticOverrides() { + ASSERT_NE(m_scene, nullptr); + + const DiagnosticMode mode = GetDiagnosticMode(); + std::printf("[NahidaDiag] diagnosticMode=%s\n", GetDiagnosticModeName(mode)); + + if (mode == DiagnosticMode::Original) { + return; + } + + if (mode == DiagnosticMode::NoShadows) { + const std::vector lights = m_scene->FindObjectsOfType(); + for (LightComponent* light : lights) { + if (light != nullptr) { + light->SetCastsShadows(false); + } + } + return; + } + + const char* shaderPath = mode == DiagnosticMode::ForwardLit + ? "builtin://shaders/forward-lit" + : "builtin://shaders/unlit"; + + std::unordered_map overrideBySource; + const std::vector meshRenderers = m_scene->FindObjectsOfType(); + for (MeshRendererComponent* meshRenderer : meshRenderers) { + if (meshRenderer == nullptr) { + continue; + } + + for (size_t materialIndex = 0; materialIndex < meshRenderer->GetMaterialCount(); ++materialIndex) { + Material* sourceMaterial = meshRenderer->GetMaterial(materialIndex); + if (sourceMaterial == nullptr || sourceMaterial->GetShader() == nullptr) { + continue; + } + + if (std::string(sourceMaterial->GetShader()->GetPath().CStr()) != "Assets/Shaders/XCCharacterToon.shader") { + continue; + } + + Material* overrideMaterial = nullptr; + const auto found = overrideBySource.find(sourceMaterial); + if (found != overrideBySource.end()) { + overrideMaterial = found->second; + } else { + overrideMaterial = CreateOverrideMaterial(sourceMaterial, shaderPath); + overrideBySource.emplace(sourceMaterial, overrideMaterial); + } + + if (overrideMaterial != nullptr) { + meshRenderer->SetMaterial(materialIndex, overrideMaterial); + } + } + } +} + +void NahidaPreviewSceneTest::DumpTargetDiagnostics() { + ASSERT_NE(m_scene, nullptr); + + DumpSceneDiagnostics(*m_scene); + + const char* const targetObjects[] = { + "Body_Mesh0", + "Brow", + "EyeStar", + "Dress_Mesh0", + "Hair_Mesh0", + "Face", + "Face_Eye" + }; + + for (const char* objectName : targetObjects) { + GameObject* gameObject = m_scene->Find(objectName); + if (gameObject == nullptr) { + std::printf("[NahidaDiag] object=%s missing\n", objectName); + continue; + } + + auto* meshFilter = gameObject->GetComponent(); + auto* meshRenderer = gameObject->GetComponent(); + std::printf( + "[NahidaDiag] object=%s materialCount=%zu active=%d\n", + objectName, + meshRenderer != nullptr ? meshRenderer->GetMaterialCount() : 0u, + gameObject->IsActiveInHierarchy() ? 1 : 0); + + DumpMeshDiagnostics(objectName, meshFilter != nullptr ? meshFilter->GetMesh() : nullptr); + + if (meshRenderer == nullptr) { + continue; + } + + for (size_t materialIndex = 0; materialIndex < meshRenderer->GetMaterialCount(); ++materialIndex) { + std::string label = std::string(objectName) + "#mat" + std::to_string(materialIndex); + DumpMaterialDiagnostics(label.c_str(), meshRenderer->GetMaterial(materialIndex)); + } + } +} + +RHIResourceView* NahidaPreviewSceneTest::GetCurrentBackBufferView() { + const int backBufferIndex = GetCurrentBackBufferIndex(); + if (backBufferIndex < 0) { + return nullptr; + } + + if (static_cast(backBufferIndex) >= m_backBufferViews.size()) { + m_backBufferViews.resize(static_cast(backBufferIndex) + 1, nullptr); + } + + if (m_backBufferViews[backBufferIndex] == nullptr) { + ResourceViewDesc viewDesc = {}; + viewDesc.format = static_cast(Format::R8G8B8A8_UNorm); + viewDesc.dimension = ResourceViewDimension::Texture2D; + viewDesc.mipLevel = 0; + m_backBufferViews[backBufferIndex] = GetDevice()->CreateRenderTargetView(GetCurrentBackBuffer(), viewDesc); + } + + return m_backBufferViews[backBufferIndex]; +} + +void NahidaPreviewSceneTest::RenderFrame() { + ASSERT_NE(m_scene, nullptr); + ASSERT_NE(m_sceneRenderer, nullptr); + + TouchSceneMaterialsAndTextures(); + ResourceManager::Get().UpdateAsyncLoads(); + + RHICommandList* commandList = GetCommandList(); + ASSERT_NE(commandList, nullptr); + + commandList->Reset(); + + RenderSurface surface(kFrameWidth, kFrameHeight); + surface.SetColorAttachment(GetCurrentBackBufferView()); + surface.SetDepthAttachment(m_depthView); + + RenderContext renderContext = {}; + renderContext.device = GetDevice(); + renderContext.commandList = commandList; + renderContext.commandQueue = GetCommandQueue(); + renderContext.backendType = GetBackendType(); + + ASSERT_TRUE(m_sceneRenderer->Render(*m_scene, nullptr, renderContext, surface)); + + commandList->Close(); + void* commandLists[] = { commandList }; + GetCommandQueue()->ExecuteCommandLists(1, commandLists); +} + +TEST_P(NahidaPreviewSceneTest, RenderNahidaPreviewScene) { + RHICommandQueue* commandQueue = GetCommandQueue(); + RHISwapChain* swapChain = GetSwapChain(); + ASSERT_NE(commandQueue, nullptr); + ASSERT_NE(swapChain, nullptr); + + for (int frameIndex = 0; frameIndex <= kWarmupFrames; ++frameIndex) { + if (frameIndex > 0) { + commandQueue->WaitForPreviousFrame(); + } + + BeginRender(); + RenderFrame(); + + if (frameIndex >= kWarmupFrames) { + commandQueue->WaitForIdle(); + ASSERT_TRUE(TakeScreenshot(kD3D12Screenshot)); + + const PpmImage image = LoadPpmImage(kD3D12Screenshot); + ASSERT_EQ(image.width, kFrameWidth); + ASSERT_EQ(image.height, kFrameHeight); + + const std::filesystem::path gtPath = ResolveRuntimePath("GT.ppm"); + if (!std::filesystem::exists(gtPath)) { + GTEST_SKIP() << "GT.ppm missing, screenshot captured for manual review: " << kD3D12Screenshot; + } + + ASSERT_TRUE(CompareWithGoldenTemplate(kD3D12Screenshot, "GT.ppm", 10.0f)); + break; + } + + swapChain->Present(0, 0); + } +} + +} // namespace + +INSTANTIATE_TEST_SUITE_P(D3D12, NahidaPreviewSceneTest, ::testing::Values(RHIType::D3D12)); + +GTEST_API_ int main(int argc, char** argv) { + return RunRenderingIntegrationTestMain(argc, argv); +}