#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); }