chore: checkpoint current workspace changes
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
# ============================================================
|
||||
|
||||
set(ASSET_TEST_SOURCES
|
||||
test_artifact_container.cpp
|
||||
test_iresource.cpp
|
||||
test_resource_types.cpp
|
||||
test_resource_guid.cpp
|
||||
@@ -10,6 +11,7 @@ set(ASSET_TEST_SOURCES
|
||||
test_resource_manager.cpp
|
||||
test_resource_cache.cpp
|
||||
test_resource_dependency.cpp
|
||||
test_shader_compilation_cache.cpp
|
||||
)
|
||||
|
||||
add_executable(asset_tests ${ASSET_TEST_SOURCES})
|
||||
@@ -34,3 +36,20 @@ target_include_directories(asset_tests PRIVATE
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(asset_tests)
|
||||
|
||||
add_executable(artifact_inspect artifact_inspect.cpp)
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(artifact_inspect PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(artifact_inspect
|
||||
PRIVATE
|
||||
XCEngine
|
||||
)
|
||||
|
||||
target_include_directories(artifact_inspect PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
113
tests/Core/Asset/artifact_inspect.cpp
Normal file
113
tests/Core/Asset/artifact_inspect.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using namespace XCEngine::Resources;
|
||||
|
||||
namespace {
|
||||
|
||||
void PrintUsage() {
|
||||
std::cout
|
||||
<< "Usage:\n"
|
||||
<< " artifact_inspect <artifact-path>\n"
|
||||
<< " artifact_inspect <artifact-path> --extract <entry-name> <output-path>\n";
|
||||
}
|
||||
|
||||
bool WritePayloadToFile(const fs::path& outputPath,
|
||||
const XCEngine::Containers::Array<XCEngine::Core::uint8>& payload) {
|
||||
std::error_code ec;
|
||||
const fs::path parent = outputPath.parent_path();
|
||||
if (!parent.empty()) {
|
||||
fs::create_directories(parent, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Failed to create output directory: " << parent.string() << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream output(outputPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
std::cerr << "Failed to open output file: " << outputPath.string() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!payload.Empty()) {
|
||||
output.write(reinterpret_cast<const char*>(payload.Data()),
|
||||
static_cast<std::streamsize>(payload.Size()));
|
||||
}
|
||||
|
||||
if (!output) {
|
||||
std::cerr << "Failed to write output file: " << outputPath.string() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PrintEntry(const ArtifactContainerEntryView& entry) {
|
||||
std::cout
|
||||
<< "- name: " << entry.name.CStr()
|
||||
<< ", type: " << GetResourceTypeName(entry.resourceType)
|
||||
<< ", localID: " << entry.localID
|
||||
<< ", size: " << entry.payloadSize
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2 && argc != 5) {
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const XCEngine::Containers::String artifactPath(argv[1] == nullptr ? "" : argv[1]);
|
||||
|
||||
ArtifactContainerReader reader;
|
||||
XCEngine::Containers::String errorMessage;
|
||||
if (!reader.Open(artifactPath, &errorMessage)) {
|
||||
std::cerr
|
||||
<< "Failed to open artifact container: "
|
||||
<< (errorMessage.Empty() ? artifactPath.CStr() : errorMessage.CStr())
|
||||
<< "\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::cout << "Artifact: " << reader.GetPath().CStr() << "\n";
|
||||
std::cout << "Entries: " << reader.GetEntryCount() << "\n";
|
||||
for (const ArtifactContainerEntryView& entry : reader.GetEntries()) {
|
||||
PrintEntry(entry);
|
||||
}
|
||||
|
||||
if (argc == 5) {
|
||||
const std::string mode = argv[2] == nullptr ? std::string() : std::string(argv[2]);
|
||||
if (mode != "--extract") {
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const XCEngine::Containers::String entryName(argv[3] == nullptr ? "" : argv[3]);
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
||||
if (!reader.ReadEntryPayload(entryName, payload, &errorMessage)) {
|
||||
std::cerr
|
||||
<< "Failed to read entry payload: "
|
||||
<< (errorMessage.Empty() ? entryName.CStr() : errorMessage.CStr())
|
||||
<< "\n";
|
||||
return 3;
|
||||
}
|
||||
|
||||
const fs::path outputPath(argv[4] == nullptr ? "" : argv[4]);
|
||||
if (!WritePayloadToFile(outputPath, payload)) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
std::cout
|
||||
<< "Extracted entry '" << entryName.CStr() << "' to " << outputPath.string() << "\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
147
tests/Core/Asset/test_artifact_container.cpp
Normal file
147
tests/Core/Asset/test_artifact_container.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
namespace Containers = XCEngine::Containers;
|
||||
namespace Core = XCEngine::Core;
|
||||
|
||||
namespace {
|
||||
|
||||
void FillPayload(Containers::Array<Core::uint8>& payload,
|
||||
std::initializer_list<Core::uint8> bytes) {
|
||||
payload.Clear();
|
||||
payload.Reserve(bytes.size());
|
||||
for (const Core::uint8 value : bytes) {
|
||||
payload.PushBack(value);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ArtifactContainer, WritesAndReadsMultipleEntries) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_artifact_container_test";
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(projectRoot);
|
||||
|
||||
ArtifactContainerWriter writer;
|
||||
|
||||
ArtifactContainerEntry mainEntry;
|
||||
mainEntry.name = "main";
|
||||
mainEntry.resourceType = ResourceType::Model;
|
||||
mainEntry.localID = kMainAssetLocalID;
|
||||
FillPayload(mainEntry.payload, { 1, 2, 3, 4 });
|
||||
writer.AddEntry(mainEntry);
|
||||
|
||||
ArtifactContainerEntry meshEntry;
|
||||
meshEntry.name = "mesh/0";
|
||||
meshEntry.resourceType = ResourceType::Mesh;
|
||||
meshEntry.localID = 42;
|
||||
meshEntry.flags = 7;
|
||||
FillPayload(meshEntry.payload, { 9, 8, 7 });
|
||||
writer.AddEntry(std::move(meshEntry));
|
||||
|
||||
Containers::String errorMessage;
|
||||
const fs::path artifactPath = projectRoot / "Library" / "Artifacts" / "ab" / "artifact.xca";
|
||||
ASSERT_TRUE(writer.WriteToFile(artifactPath.string().c_str(), &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
ASSERT_TRUE(fs::exists(artifactPath));
|
||||
|
||||
ArtifactContainerReader reader;
|
||||
ASSERT_TRUE(reader.Open(artifactPath.string().c_str(), &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
EXPECT_EQ(reader.GetEntryCount(), 2u);
|
||||
|
||||
const ArtifactContainerEntryView* mainView = reader.FindEntryByName("main");
|
||||
ASSERT_NE(mainView, nullptr);
|
||||
EXPECT_EQ(mainView->resourceType, ResourceType::Model);
|
||||
EXPECT_EQ(mainView->localID, kMainAssetLocalID);
|
||||
|
||||
const ArtifactContainerEntryView* meshView = reader.FindEntry(ResourceType::Mesh, 42);
|
||||
ASSERT_NE(meshView, nullptr);
|
||||
EXPECT_EQ(meshView->name, Containers::String("mesh/0"));
|
||||
EXPECT_EQ(meshView->flags, 7u);
|
||||
|
||||
Containers::Array<Core::uint8> payload;
|
||||
ASSERT_TRUE(reader.ReadEntryPayload(*meshView, payload, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
ASSERT_EQ(payload.Size(), 3u);
|
||||
EXPECT_EQ(payload[0], 9u);
|
||||
EXPECT_EQ(payload[1], 8u);
|
||||
EXPECT_EQ(payload[2], 7u);
|
||||
}
|
||||
|
||||
TEST(ArtifactContainer, RejectsCorruptFileHeader) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_artifact_container_corrupt_test";
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(projectRoot);
|
||||
|
||||
const fs::path artifactPath = projectRoot / "broken.xca";
|
||||
{
|
||||
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
|
||||
output << "broken";
|
||||
}
|
||||
|
||||
ArtifactContainerReader reader;
|
||||
Containers::String errorMessage;
|
||||
EXPECT_FALSE(reader.Open(artifactPath.string().c_str(), &errorMessage));
|
||||
EXPECT_FALSE(errorMessage.Empty());
|
||||
}
|
||||
|
||||
TEST(ArtifactContainer, ResolvesEntryVirtualPaths) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_artifact_container_entry_path_test";
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(projectRoot);
|
||||
|
||||
ArtifactContainerWriter writer;
|
||||
|
||||
ArtifactContainerEntry mainEntry;
|
||||
mainEntry.name = "main";
|
||||
mainEntry.resourceType = ResourceType::Model;
|
||||
mainEntry.localID = kMainAssetLocalID;
|
||||
FillPayload(mainEntry.payload, { 1, 2, 3 });
|
||||
writer.AddEntry(mainEntry);
|
||||
|
||||
ArtifactContainerEntry meshEntry;
|
||||
meshEntry.name = "mesh_0.xcmesh";
|
||||
meshEntry.resourceType = ResourceType::Mesh;
|
||||
meshEntry.localID = 77;
|
||||
FillPayload(meshEntry.payload, { 5, 6, 7, 8 });
|
||||
writer.AddEntry(meshEntry);
|
||||
|
||||
const Containers::String containerPath =
|
||||
(projectRoot / "Library" / "Artifacts" / "ab" / "artifact.xcmodel").string().c_str();
|
||||
Containers::String errorMessage;
|
||||
ASSERT_TRUE(writer.WriteToFile(containerPath, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
|
||||
const Containers::String virtualPath =
|
||||
BuildArtifactContainerEntryPath(containerPath, "mesh_0.xcmesh");
|
||||
Containers::String parsedContainerPath;
|
||||
Containers::String parsedEntryName;
|
||||
ASSERT_TRUE(TryParseArtifactContainerEntryPath(
|
||||
virtualPath,
|
||||
parsedContainerPath,
|
||||
parsedEntryName));
|
||||
EXPECT_EQ(parsedContainerPath, containerPath);
|
||||
EXPECT_EQ(parsedEntryName, "mesh_0.xcmesh");
|
||||
|
||||
Containers::Array<Core::uint8> payload;
|
||||
ASSERT_TRUE(ReadArtifactContainerPayloadByPath(
|
||||
virtualPath,
|
||||
ResourceType::Mesh,
|
||||
payload,
|
||||
&errorMessage)) << errorMessage.CStr();
|
||||
ASSERT_EQ(payload.Size(), 4u);
|
||||
EXPECT_EQ(payload[0], 5u);
|
||||
EXPECT_EQ(payload[3], 8u);
|
||||
}
|
||||
92
tests/Core/Asset/test_shader_compilation_cache.cpp
Normal file
92
tests/Core/Asset/test_shader_compilation_cache.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Resources/Shader/ShaderCompilationCache.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
namespace Containers = XCEngine::Containers;
|
||||
namespace Core = XCEngine::Core;
|
||||
|
||||
namespace {
|
||||
|
||||
void FillPayload(Containers::Array<Core::uint8>& payload,
|
||||
std::initializer_list<Core::uint8> bytes) {
|
||||
payload.Clear();
|
||||
payload.Reserve(bytes.size());
|
||||
for (const Core::uint8 value : bytes) {
|
||||
payload.PushBack(value);
|
||||
}
|
||||
}
|
||||
|
||||
ShaderCompileKey MakeBaseCompileKey() {
|
||||
ShaderCompileKey key;
|
||||
key.shaderPath = "Assets/Shaders/Cloud.shader";
|
||||
key.sourceHash = "sourcehash";
|
||||
key.dependencyHash = "dephash";
|
||||
key.passName = "Forward";
|
||||
key.entryPoint = "frag";
|
||||
key.profile = "ps_6_0";
|
||||
key.compilerName = "DXC";
|
||||
key.compilerVersion = "1.8";
|
||||
key.optionsSignature = "O3;Zi=0";
|
||||
key.stage = ShaderType::Fragment;
|
||||
key.sourceLanguage = ShaderLanguage::HLSL;
|
||||
key.backend = ShaderBackend::D3D12;
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ShaderCompilationCache, BuildCacheKeyCanonicalizesKeywordOrder) {
|
||||
ShaderCompileKey firstKey = MakeBaseCompileKey();
|
||||
firstKey.keywords.PushBack("FOG_ON");
|
||||
firstKey.keywords.PushBack(" SHADOWS_ON ");
|
||||
firstKey.keywords.PushBack("FOG_ON");
|
||||
|
||||
ShaderCompileKey secondKey = MakeBaseCompileKey();
|
||||
secondKey.keywords.PushBack("SHADOWS_ON");
|
||||
secondKey.keywords.PushBack("FOG_ON");
|
||||
|
||||
EXPECT_EQ(firstKey.BuildCacheKey(), secondKey.BuildCacheKey());
|
||||
EXPECT_EQ(firstKey.BuildSignature(), secondKey.BuildSignature());
|
||||
}
|
||||
|
||||
TEST(ShaderCompilationCache, StoresAndLoadsBinaryUnderLibraryRoot) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_shader_compilation_cache_test";
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(projectRoot / "Library");
|
||||
|
||||
ShaderCompilationCache cache;
|
||||
cache.Initialize((projectRoot / "Library").string().c_str());
|
||||
|
||||
ShaderCacheEntry entry;
|
||||
entry.key = MakeBaseCompileKey();
|
||||
entry.key.keywords.PushBack("CLOUD_LIT");
|
||||
entry.format = ShaderBytecodeFormat::DXIL;
|
||||
FillPayload(entry.payload, { 0x10, 0x20, 0x30, 0x40 });
|
||||
|
||||
Containers::String errorMessage;
|
||||
ASSERT_TRUE(cache.Store(entry, &errorMessage)) << errorMessage.CStr();
|
||||
|
||||
const fs::path databasePath(cache.GetDatabasePath().CStr());
|
||||
EXPECT_TRUE(fs::exists(databasePath));
|
||||
|
||||
const Containers::String absoluteCachePath = cache.BuildCacheAbsolutePath(entry.key);
|
||||
EXPECT_FALSE(absoluteCachePath.Empty());
|
||||
EXPECT_TRUE(fs::exists(fs::path(absoluteCachePath.CStr())));
|
||||
EXPECT_NE(std::string(absoluteCachePath.CStr()).find("ShaderCache/D3D12"),
|
||||
std::string::npos);
|
||||
EXPECT_EQ(cache.GetRecordCount(), 1u);
|
||||
|
||||
ShaderCacheEntry loadedEntry;
|
||||
ASSERT_TRUE(cache.TryLoad(entry.key, loadedEntry, &errorMessage)) << errorMessage.CStr();
|
||||
EXPECT_EQ(loadedEntry.format, ShaderBytecodeFormat::DXIL);
|
||||
ASSERT_EQ(loadedEntry.payload.Size(), 4u);
|
||||
EXPECT_EQ(loadedEntry.payload[0], 0x10u);
|
||||
EXPECT_EQ(loadedEntry.payload[1], 0x20u);
|
||||
EXPECT_EQ(loadedEntry.payload[2], 0x30u);
|
||||
EXPECT_EQ(loadedEntry.payload[3], 0x40u);
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -203,6 +204,56 @@ VSOutput MainVS(VSInput input) {
|
||||
ASSERT_TRUE(failures.empty()) << failures.front();
|
||||
}
|
||||
|
||||
TEST(OpenGLShaderCompiler_Test, CompileSpirvShader_GlslOpenGLTarget_SupportsIncludeDirectoriesWithSpaces) {
|
||||
if (!SupportsOpenGLHlslToolchainForTests()) {
|
||||
GTEST_SKIP() << "glslangValidator.exe or spirv-cross.exe was not found.";
|
||||
}
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path includeDirectory =
|
||||
fs::temp_directory_path() / "xcengine glslang include regression";
|
||||
std::error_code ec;
|
||||
fs::remove_all(includeDirectory, ec);
|
||||
ec.clear();
|
||||
fs::create_directories(includeDirectory, ec);
|
||||
ASSERT_FALSE(ec) << ec.message();
|
||||
|
||||
const fs::path includeFile = includeDirectory / "shared_include.glsl";
|
||||
{
|
||||
std::ofstream stream(includeFile, std::ios::binary | std::ios::trunc);
|
||||
ASSERT_TRUE(stream.is_open());
|
||||
stream << "const vec4 kOffset = vec4(0.0, 0.0, 0.0, 0.0);\n";
|
||||
}
|
||||
|
||||
static const char* vertexSource = R"(#version 450 core
|
||||
#extension GL_GOOGLE_include_directive : require
|
||||
#include "shared_include.glsl"
|
||||
|
||||
layout(location = 0) in vec4 inPosition;
|
||||
|
||||
void main() {
|
||||
gl_Position = inPosition + kOffset;
|
||||
}
|
||||
)";
|
||||
|
||||
ShaderCompileDesc shaderDesc = {};
|
||||
shaderDesc.source.assign(vertexSource, vertexSource + std::strlen(vertexSource));
|
||||
shaderDesc.sourceLanguage = ShaderLanguage::GLSL;
|
||||
shaderDesc.fileName = L"shader.vert";
|
||||
shaderDesc.includeDirectories.push_back(includeDirectory.wstring());
|
||||
|
||||
CompiledSpirvShader compiledShader = {};
|
||||
std::string errorMessage;
|
||||
EXPECT_TRUE(CompileSpirvShader(
|
||||
shaderDesc,
|
||||
SpirvTargetEnvironment::OpenGL,
|
||||
compiledShader,
|
||||
&errorMessage)) << errorMessage;
|
||||
|
||||
fs::remove_all(includeDirectory, ec);
|
||||
}
|
||||
|
||||
TEST_F(OpenGLTestFixture, Device_CreatePipelineState_HlslGraphicsShaders_UsesTranspiledHlslPath) {
|
||||
ASSERT_TRUE(GetDevice()->MakeContextCurrent());
|
||||
if (!SupportsOpenGLHlslToolchainForTests()) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -9,6 +9,7 @@
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/GaussianSplatRendererComponent.h>
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
@@ -16,6 +17,7 @@
|
||||
#include <XCEngine/Rendering/Execution/SceneRenderer.h>
|
||||
#include <XCEngine/Rendering/RenderContext.h>
|
||||
#include <XCEngine/Rendering/RenderSurface.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
#include <XCEngine/Resources/Material/Material.h>
|
||||
#include <XCEngine/Resources/BuiltinResources.h>
|
||||
@@ -49,6 +51,9 @@ constexpr const char* kVulkanScreenshot = "gaussian_splat_scene_vulkan.ppm";
|
||||
constexpr uint32_t kFrameWidth = 1280;
|
||||
constexpr uint32_t kFrameHeight = 720;
|
||||
constexpr uint32_t kBaselineSubsetSplatCount = 65536u;
|
||||
constexpr const char* kSubsetGaussianSplatAssetPath = "Assets/room_subset.xcgsplat";
|
||||
constexpr float kTargetSceneExtent = 4.0f;
|
||||
constexpr float kGaussianPointScale = 3.00f;
|
||||
|
||||
XCEngine::Core::uint16 FloatToHalfBits(float value) {
|
||||
uint32_t bits = 0u;
|
||||
@@ -329,7 +334,7 @@ private:
|
||||
RHITexture* m_depthTexture = nullptr;
|
||||
RHIResourceView* m_depthView = nullptr;
|
||||
ResourceHandle<GaussianSplat> m_gaussianSplat;
|
||||
GaussianSplat* m_subsetGaussianSplat = nullptr;
|
||||
ResourceHandle<GaussianSplat> m_subsetGaussianSplat;
|
||||
Material* m_material = nullptr;
|
||||
};
|
||||
|
||||
@@ -394,8 +399,7 @@ void GaussianSplatSceneTest::TearDown() {
|
||||
|
||||
delete m_material;
|
||||
m_material = nullptr;
|
||||
delete m_subsetGaussianSplat;
|
||||
m_subsetGaussianSplat = nullptr;
|
||||
m_subsetGaussianSplat.Reset();
|
||||
m_gaussianSplat.Reset();
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
@@ -430,25 +434,53 @@ void GaussianSplatSceneTest::PrepareRuntimeProject() {
|
||||
manager.Initialize();
|
||||
manager.SetResourceRoot(m_projectRoot.string().c_str());
|
||||
|
||||
m_gaussianSplat = manager.Load<GaussianSplat>("Assets/room.ply");
|
||||
AssetDatabase database;
|
||||
database.Initialize(m_projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset roomResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/room.ply", ResourceType::GaussianSplat, roomResolve));
|
||||
ASSERT_TRUE(roomResolve.artifactReady);
|
||||
ASSERT_FALSE(roomResolve.artifactMainPath.Empty());
|
||||
|
||||
m_gaussianSplat = manager.Load<GaussianSplat>(roomResolve.artifactMainPath);
|
||||
ASSERT_TRUE(m_gaussianSplat.IsValid());
|
||||
ASSERT_NE(m_gaussianSplat.Get(), nullptr);
|
||||
ASSERT_TRUE(m_gaussianSplat->IsValid());
|
||||
ASSERT_GT(m_gaussianSplat->GetSplatCount(), 0u);
|
||||
ASSERT_EQ(m_gaussianSplat->GetSHOrder(), 3u);
|
||||
m_subsetGaussianSplat = CreateGaussianSplatSubset(
|
||||
*m_gaussianSplat.Get(),
|
||||
kBaselineSubsetSplatCount,
|
||||
"Tests/Rendering/GaussianSplatScene/RoomSubset.xcgsplat");
|
||||
ASSERT_NE(m_subsetGaussianSplat, nullptr);
|
||||
|
||||
std::unique_ptr<GaussianSplat> subsetGaussianSplat(
|
||||
CreateGaussianSplatSubset(
|
||||
*m_gaussianSplat.Get(),
|
||||
kBaselineSubsetSplatCount,
|
||||
kSubsetGaussianSplatAssetPath));
|
||||
ASSERT_NE(subsetGaussianSplat, nullptr);
|
||||
ASSERT_TRUE(subsetGaussianSplat->IsValid());
|
||||
ASSERT_GT(subsetGaussianSplat->GetSplatCount(), 0u);
|
||||
ASSERT_EQ(subsetGaussianSplat->GetSHOrder(), 3u);
|
||||
|
||||
const std::filesystem::path subsetArtifactPath = assetsDir / "room_subset.xcgsplat";
|
||||
XCEngine::Containers::String subsetWriteError;
|
||||
ASSERT_TRUE(WriteGaussianSplatArtifactFile(
|
||||
subsetArtifactPath.string().c_str(),
|
||||
*subsetGaussianSplat,
|
||||
&subsetWriteError)) << subsetWriteError.CStr();
|
||||
|
||||
m_subsetGaussianSplat = manager.Load<GaussianSplat>(
|
||||
XCEngine::Containers::String(subsetArtifactPath.string().c_str()));
|
||||
ASSERT_TRUE(m_subsetGaussianSplat.IsValid());
|
||||
ASSERT_NE(m_subsetGaussianSplat.Get(), nullptr);
|
||||
ASSERT_TRUE(m_subsetGaussianSplat->IsValid());
|
||||
ASSERT_GT(m_subsetGaussianSplat->GetSplatCount(), 0u);
|
||||
ASSERT_EQ(m_subsetGaussianSplat->GetSHOrder(), 3u);
|
||||
ASSERT_NE(m_subsetGaussianSplat->FindSection(GaussianSplatSectionType::Chunks), nullptr);
|
||||
|
||||
database.Shutdown();
|
||||
}
|
||||
|
||||
void GaussianSplatSceneTest::BuildScene() {
|
||||
ASSERT_NE(m_scene, nullptr);
|
||||
ASSERT_NE(m_subsetGaussianSplat, nullptr);
|
||||
ASSERT_TRUE(m_subsetGaussianSplat.IsValid());
|
||||
|
||||
m_material = new Material();
|
||||
IResource::ConstructParams params = {};
|
||||
@@ -458,7 +490,7 @@ void GaussianSplatSceneTest::BuildScene() {
|
||||
m_material->Initialize(params);
|
||||
m_material->SetShader(ResourceManager::Get().Load<Shader>(GetBuiltinGaussianSplatShaderPath()));
|
||||
m_material->SetRenderQueue(MaterialRenderQueue::Transparent);
|
||||
m_material->SetFloat("_PointScale", 1.0f);
|
||||
m_material->SetFloat("_PointScale", kGaussianPointScale);
|
||||
m_material->SetFloat("_OpacityScale", 1.0f);
|
||||
|
||||
GameObject* cameraObject = m_scene->CreateGameObject("MainCamera");
|
||||
@@ -480,10 +512,10 @@ 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 = 3.0f / maxExtent;
|
||||
const float uniformScale = kTargetSceneExtent / maxExtent;
|
||||
|
||||
GameObject* root = m_scene->CreateGameObject("GaussianSplatRoot");
|
||||
root->GetTransform()->SetLocalPosition(Vector3(0.0f, -0.35f, 3.2f));
|
||||
root->GetTransform()->SetLocalPosition(Vector3::Zero());
|
||||
root->GetTransform()->SetLocalScale(Vector3(uniformScale, uniformScale, uniformScale));
|
||||
|
||||
GameObject* splatObject = m_scene->CreateGameObject("RoomGaussianSplat");
|
||||
@@ -491,10 +523,11 @@ void GaussianSplatSceneTest::BuildScene() {
|
||||
splatObject->GetTransform()->SetLocalPosition(Vector3(-center.x, -center.y, -center.z));
|
||||
|
||||
auto* splatRenderer = splatObject->AddComponent<GaussianSplatRendererComponent>();
|
||||
splatRenderer->SetGaussianSplat(m_subsetGaussianSplat);
|
||||
splatRenderer->SetGaussianSplat(m_subsetGaussianSplat.Get());
|
||||
splatRenderer->SetMaterial(m_material);
|
||||
splatRenderer->SetCastShadows(false);
|
||||
splatRenderer->SetReceiveShadows(false);
|
||||
cameraObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 1.0f, 1.0f));
|
||||
}
|
||||
|
||||
RHIResourceView* GaussianSplatSceneTest::GetCurrentBackBufferView() {
|
||||
@@ -551,7 +584,7 @@ TEST_P(GaussianSplatSceneTest, RenderRoomGaussianSplatScene) {
|
||||
ASSERT_NE(commandQueue, nullptr);
|
||||
ASSERT_NE(swapChain, nullptr);
|
||||
|
||||
constexpr int kTargetFrameCount = 1;
|
||||
constexpr int kTargetFrameCount = 2;
|
||||
const char* screenshotFilename = GetScreenshotFilename(GetBackendType());
|
||||
|
||||
for (int frameCount = 0; frameCount <= kTargetFrameCount; ++frameCount) {
|
||||
|
||||
@@ -67,5 +67,29 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/GT.no_shadows.ppm)
|
||||
add_custom_command(TARGET rendering_integration_nahida_preview_scene POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GT.no_shadows.ppm
|
||||
$<TARGET_FILE_DIR:rendering_integration_nahida_preview_scene>/GT.no_shadows.ppm
|
||||
)
|
||||
endif()
|
||||
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/GT.forward_lit.ppm)
|
||||
add_custom_command(TARGET rendering_integration_nahida_preview_scene POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GT.forward_lit.ppm
|
||||
$<TARGET_FILE_DIR:rendering_integration_nahida_preview_scene>/GT.forward_lit.ppm
|
||||
)
|
||||
endif()
|
||||
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/GT.unlit.ppm)
|
||||
add_custom_command(TARGET rendering_integration_nahida_preview_scene POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GT.unlit.ppm
|
||||
$<TARGET_FILE_DIR:rendering_integration_nahida_preview_scene>/GT.unlit.ppm
|
||||
)
|
||||
endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(rendering_integration_nahida_preview_scene)
|
||||
|
||||
@@ -86,6 +86,16 @@ const char* GetDiagnosticModeName(DiagnosticMode mode) {
|
||||
}
|
||||
}
|
||||
|
||||
const char* GetGoldenFileName(DiagnosticMode mode) {
|
||||
switch (mode) {
|
||||
case DiagnosticMode::Original: return "GT.ppm";
|
||||
case DiagnosticMode::NoShadows: return "GT.no_shadows.ppm";
|
||||
case DiagnosticMode::ForwardLit: return "GT.forward_lit.ppm";
|
||||
case DiagnosticMode::Unlit: return "GT.unlit.ppm";
|
||||
default: return "GT.ppm";
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> GetIsolationObjectNames() {
|
||||
std::unordered_set<std::string> result;
|
||||
const char* value = std::getenv("XC_NAHIDA_DIAG_ONLY");
|
||||
@@ -713,6 +723,8 @@ TEST_P(NahidaPreviewSceneTest, RenderNahidaPreviewScene) {
|
||||
RHISwapChain* swapChain = GetSwapChain();
|
||||
ASSERT_NE(commandQueue, nullptr);
|
||||
ASSERT_NE(swapChain, nullptr);
|
||||
const DiagnosticMode diagnosticMode = GetDiagnosticMode();
|
||||
const char* goldenFileName = GetGoldenFileName(diagnosticMode);
|
||||
|
||||
for (int frameIndex = 0; frameIndex <= kWarmupFrames; ++frameIndex) {
|
||||
if (frameIndex > 0) {
|
||||
@@ -730,12 +742,12 @@ TEST_P(NahidaPreviewSceneTest, RenderNahidaPreviewScene) {
|
||||
ASSERT_EQ(image.width, kFrameWidth);
|
||||
ASSERT_EQ(image.height, kFrameHeight);
|
||||
|
||||
const std::filesystem::path gtPath = ResolveRuntimePath("GT.ppm");
|
||||
const std::filesystem::path gtPath = ResolveRuntimePath(goldenFileName);
|
||||
if (!std::filesystem::exists(gtPath)) {
|
||||
GTEST_SKIP() << "GT.ppm missing, screenshot captured for manual review: " << kD3D12Screenshot;
|
||||
GTEST_SKIP() << goldenFileName << " missing, screenshot captured for manual review: " << kD3D12Screenshot;
|
||||
}
|
||||
|
||||
ASSERT_TRUE(CompareWithGoldenTemplate(kD3D12Screenshot, "GT.ppm", 10.0f));
|
||||
ASSERT_TRUE(CompareWithGoldenTemplate(kD3D12Screenshot, goldenFileName, 10.0f));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -364,6 +364,7 @@ void PostProcessSceneTest::RenderFrame() {
|
||||
request.surface = finalSurface;
|
||||
request.postProcess.sourceSurface = mainSceneSurface;
|
||||
request.postProcess.sourceColorView = mSceneColorShaderView;
|
||||
request.postProcess.sourceColorState = ResourceStates::PixelShaderResource;
|
||||
request.postProcess.destinationSurface = finalSurface;
|
||||
request.postProcess.passes = &mPostProcessPasses;
|
||||
|
||||
|
||||
@@ -83,7 +83,10 @@ TEST(RenderPassSequence_Test, InitializesAndExecutesInInsertionOrderThenShutsDow
|
||||
const RenderPassContext passContext = {
|
||||
context,
|
||||
surface,
|
||||
sceneData
|
||||
sceneData,
|
||||
nullptr,
|
||||
nullptr,
|
||||
XCEngine::RHI::ResourceStates::Common
|
||||
};
|
||||
|
||||
ASSERT_TRUE(sequence.Execute(passContext));
|
||||
@@ -115,7 +118,10 @@ TEST(RenderPassSequence_Test, StopsExecutingWhenAPassFails) {
|
||||
const RenderPassContext passContext = {
|
||||
context,
|
||||
surface,
|
||||
sceneData
|
||||
sceneData,
|
||||
nullptr,
|
||||
nullptr,
|
||||
XCEngine::RHI::ResourceStates::Common
|
||||
};
|
||||
|
||||
EXPECT_FALSE(sequence.Execute(passContext));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
@@ -446,7 +448,18 @@ TEST(GaussianSplatLoader, AssetDatabaseImportsSyntheticPlyAndLinearizesData) {
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/sample.ply", ResourceType::GaussianSplat, resolved));
|
||||
ASSERT_TRUE(resolved.artifactReady);
|
||||
EXPECT_TRUE(fs::exists(resolved.artifactMainPath.CStr()));
|
||||
EXPECT_FALSE(resolved.artifactMainEntryPath.Empty());
|
||||
EXPECT_NE(resolved.artifactMainEntryPath, resolved.artifactMainPath);
|
||||
EXPECT_EQ(fs::path(resolved.artifactMainPath.CStr()).extension().generic_string(), ".xcgsplat");
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> artifactPayload;
|
||||
EXPECT_TRUE(ReadArtifactContainerMainEntryPayload(
|
||||
resolved.artifactMainPath,
|
||||
ResourceType::GaussianSplat,
|
||||
artifactPayload));
|
||||
ASSERT_GE(artifactPayload.Size(), sizeof(GaussianSplatArtifactFileHeader));
|
||||
GaussianSplatArtifactFileHeader artifactFileHeader = {};
|
||||
std::memcpy(&artifactFileHeader, artifactPayload.Data(), sizeof(artifactFileHeader));
|
||||
EXPECT_EQ(std::memcmp(artifactFileHeader.magic, "XCGSP01", 7), 0);
|
||||
|
||||
GaussianSplatLoader loader;
|
||||
LoadResult result = loader.Load(resolved.artifactMainPath);
|
||||
@@ -485,6 +498,12 @@ TEST(GaussianSplatLoader, AssetDatabaseImportsSyntheticPlyAndLinearizesData) {
|
||||
EXPECT_NEAR(gaussianSplat->GetSHRecords()[1].coefficients[44], vertices[1].sh[44], 1e-6f);
|
||||
|
||||
delete gaussianSplat;
|
||||
|
||||
LoadResult entryResult = loader.Load(resolved.artifactMainEntryPath);
|
||||
ASSERT_TRUE(entryResult);
|
||||
ASSERT_NE(entryResult.resource, nullptr);
|
||||
delete entryResult.resource;
|
||||
|
||||
database.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Resources/BuiltinResources.h>
|
||||
@@ -8,6 +9,7 @@
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -16,6 +18,7 @@
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Containers;
|
||||
namespace Core = XCEngine::Core;
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -31,6 +34,20 @@ void WriteArtifactString(std::ofstream& output, const XCEngine::Containers::Stri
|
||||
}
|
||||
}
|
||||
|
||||
void AppendBytes(Array<Core::uint8>& payload, const void* data, size_t size) {
|
||||
const size_t oldSize = payload.Size();
|
||||
payload.Resize(oldSize + size);
|
||||
std::memcpy(payload.Data() + oldSize, data, size);
|
||||
}
|
||||
|
||||
void AppendArtifactString(Array<Core::uint8>& payload, const String& value) {
|
||||
const Core::uint32 length = static_cast<Core::uint32>(value.Length());
|
||||
AppendBytes(payload, &length, sizeof(length));
|
||||
if (length > 0) {
|
||||
AppendBytes(payload, value.CStr(), length);
|
||||
}
|
||||
}
|
||||
|
||||
bool PumpAsyncLoadsUntilIdle(ResourceManager& manager,
|
||||
std::chrono::milliseconds timeout = std::chrono::milliseconds(4000)) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
@@ -769,6 +786,10 @@ TEST(MaterialLoader, AssetDatabaseCreatesMaterialArtifact) {
|
||||
EXPECT_TRUE(resolvedAsset.artifactReady);
|
||||
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).extension().string(), ".xcmat");
|
||||
EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr()));
|
||||
const fs::path artifactPath(resolvedAsset.artifactMainPath.CStr());
|
||||
ASSERT_TRUE(artifactPath.has_parent_path());
|
||||
EXPECT_EQ(artifactPath.parent_path().filename().string(),
|
||||
artifactPath.stem().string().substr(0, 2));
|
||||
|
||||
database.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
@@ -1227,4 +1248,54 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(MaterialLoader, LoadMaterialArtifactFromContainerMainEntryPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_material_container_main_entry_test";
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(projectRoot);
|
||||
|
||||
ArtifactContainerWriter writer;
|
||||
|
||||
ArtifactContainerEntry mainEntry;
|
||||
mainEntry.name = "main";
|
||||
mainEntry.resourceType = ResourceType::Material;
|
||||
mainEntry.localID = kMainAssetLocalID;
|
||||
|
||||
MaterialArtifactFileHeader fileHeader;
|
||||
AppendBytes(mainEntry.payload, &fileHeader, sizeof(fileHeader));
|
||||
AppendArtifactString(mainEntry.payload, "ContainerMaterial");
|
||||
AppendArtifactString(mainEntry.payload, "Assets/container.material");
|
||||
AppendArtifactString(mainEntry.payload, "");
|
||||
|
||||
MaterialArtifactHeader header;
|
||||
header.renderQueue = static_cast<Core::int32>(MaterialRenderQueue::Geometry);
|
||||
AppendBytes(mainEntry.payload, &header, sizeof(header));
|
||||
writer.AddEntry(std::move(mainEntry));
|
||||
|
||||
const String containerPath =
|
||||
(projectRoot / "Library" / "Artifacts" / "ab" / "container.xcmat").string().c_str();
|
||||
String errorMessage;
|
||||
ASSERT_TRUE(writer.WriteToFile(containerPath, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
|
||||
MaterialLoader loader;
|
||||
const String virtualPath = BuildArtifactContainerEntryPath(containerPath, "main");
|
||||
EXPECT_TRUE(loader.CanLoad(virtualPath));
|
||||
|
||||
LoadResult result = loader.Load(virtualPath);
|
||||
ASSERT_TRUE(result) << result.errorMessage.CStr();
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* material = static_cast<Material*>(result.resource);
|
||||
ASSERT_NE(material, nullptr);
|
||||
EXPECT_TRUE(material->IsValid());
|
||||
EXPECT_EQ(material->GetPath(), "Assets/container.material");
|
||||
EXPECT_EQ(material->GetRenderQueue(), static_cast<Core::int32>(MaterialRenderQueue::Geometry));
|
||||
EXPECT_FALSE(material->HasRenderStateOverride());
|
||||
|
||||
delete material;
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -25,6 +25,7 @@ target_link_libraries(shader_tests
|
||||
|
||||
target_include_directories(shader_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/engine/src
|
||||
${CMAKE_SOURCE_DIR}/tests/Fixtures
|
||||
)
|
||||
|
||||
|
||||
@@ -142,6 +142,37 @@ TEST(Shader, ApplyShaderStageVariantCarriesMatchedBackendCompiledBinary) {
|
||||
EXPECT_EQ(compileDesc.compiledBinary[2], 0x09);
|
||||
}
|
||||
|
||||
TEST(Shader, ApplyShaderStageVariantTreatsOpenGLHlslPrecompiledBinaryAsVulkanSpirv) {
|
||||
ShaderPass pass = {};
|
||||
pass.name = "GaussianSplat";
|
||||
|
||||
ShaderStageVariant variant = {};
|
||||
variant.stage = ShaderType::Vertex;
|
||||
variant.language = ShaderLanguage::HLSL;
|
||||
variant.backend = ShaderBackend::Generic;
|
||||
variant.entryPoint = "MainVS";
|
||||
variant.profile = "vs_5_1";
|
||||
variant.sourceCode = "float4 MainVS() : SV_POSITION { return 0; }";
|
||||
|
||||
Array<XCEngine::Core::uint8> openglPayload;
|
||||
openglPayload.PushBack(0x0A);
|
||||
openglPayload.PushBack(0x0B);
|
||||
openglPayload.PushBack(0x0C);
|
||||
variant.SetCompiledBinaryForBackend(ShaderBackend::OpenGL, openglPayload);
|
||||
|
||||
XCEngine::RHI::ShaderCompileDesc compileDesc = {};
|
||||
::XCEngine::Rendering::Internal::ApplyShaderStageVariant(
|
||||
pass,
|
||||
ShaderBackend::OpenGL,
|
||||
variant,
|
||||
compileDesc);
|
||||
|
||||
EXPECT_EQ(compileDesc.compiledBinaryBackend, XCEngine::RHI::ShaderBinaryBackend::Vulkan);
|
||||
ASSERT_EQ(compileDesc.compiledBinary.size(), 3u);
|
||||
EXPECT_EQ(compileDesc.compiledBinary[0], 0x0A);
|
||||
EXPECT_EQ(compileDesc.compiledBinary[2], 0x0C);
|
||||
}
|
||||
|
||||
TEST(Shader, FindsBackendSpecificVariantAndFallsBackToGeneric) {
|
||||
Shader shader;
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Resources/BuiltinResources.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h>
|
||||
#include "Rendering/Internal/ShaderVariantUtils.h"
|
||||
|
||||
#include <chrono>
|
||||
@@ -18,6 +20,8 @@
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Containers;
|
||||
namespace Containers = XCEngine::Containers;
|
||||
namespace Core = XCEngine::Core;
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -29,6 +33,19 @@ void WriteTextFile(const std::filesystem::path& path, const std::string& content
|
||||
}
|
||||
|
||||
ShaderArtifactFileHeader ReadShaderArtifactFileHeader(const std::filesystem::path& path) {
|
||||
Containers::Array<Core::uint8> payload;
|
||||
if (ReadArtifactContainerMainEntryPayload(
|
||||
path.generic_string().c_str(),
|
||||
ResourceType::Shader,
|
||||
payload)) {
|
||||
EXPECT_GE(payload.Size(), sizeof(ShaderArtifactFileHeader));
|
||||
ShaderArtifactFileHeader header = {};
|
||||
if (payload.Size() >= sizeof(ShaderArtifactFileHeader)) {
|
||||
std::memcpy(&header, payload.Data(), sizeof(header));
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
ShaderArtifactFileHeader header = {};
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
EXPECT_TRUE(input.is_open());
|
||||
@@ -37,6 +54,33 @@ ShaderArtifactFileHeader ReadShaderArtifactFileHeader(const std::filesystem::pat
|
||||
return header;
|
||||
}
|
||||
|
||||
void WriteShaderArtifactFileHeader(const std::filesystem::path& path,
|
||||
const ShaderArtifactFileHeader& header) {
|
||||
Containers::Array<Core::uint8> payload;
|
||||
if (ReadArtifactContainerMainEntryPayload(
|
||||
path.generic_string().c_str(),
|
||||
ResourceType::Shader,
|
||||
payload)) {
|
||||
ASSERT_GE(payload.Size(), sizeof(ShaderArtifactFileHeader));
|
||||
std::memcpy(payload.Data(), &header, sizeof(header));
|
||||
|
||||
ArtifactContainerWriter writer;
|
||||
ArtifactContainerEntry entry;
|
||||
entry.name = "main";
|
||||
entry.resourceType = ResourceType::Shader;
|
||||
entry.localID = kMainAssetLocalID;
|
||||
entry.payload = payload;
|
||||
writer.AddEntry(std::move(entry));
|
||||
ASSERT_TRUE(writer.WriteToFile(path.generic_string().c_str()));
|
||||
return;
|
||||
}
|
||||
|
||||
std::fstream output(path, std::ios::binary | std::ios::in | std::ios::out);
|
||||
ASSERT_TRUE(output.is_open());
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
ASSERT_TRUE(static_cast<bool>(output));
|
||||
}
|
||||
|
||||
const ShaderPassTagEntry* FindPassTag(const ShaderPass* pass, const char* name) {
|
||||
if (pass == nullptr || name == nullptr) {
|
||||
return nullptr;
|
||||
@@ -699,6 +743,202 @@ TEST(ShaderLoader, LoadShaderAuthoringBuildsComputeOnlyPassConstantBufferBinding
|
||||
fs::remove_all(shaderRoot);
|
||||
}
|
||||
|
||||
TEST(ShaderLoader, LoadShaderAuthoringBuildsForwardLitExtraTextureBindings) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path shaderRoot = fs::temp_directory_path() / "xc_shader_authoring_forward_extra_textures";
|
||||
const fs::path shaderPath = shaderRoot / "forward_extra_textures.shader";
|
||||
|
||||
fs::remove_all(shaderRoot);
|
||||
fs::create_directories(shaderRoot);
|
||||
|
||||
WriteTextFile(
|
||||
shaderPath,
|
||||
R"(Shader "ForwardExtraTextures"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
_MainTex ("Main Tex", 2D) = "white" [Semantic(BaseColorTexture)]
|
||||
_LightMap ("Light Map", 2D) = "white"
|
||||
}
|
||||
SubShader
|
||||
{
|
||||
Pass
|
||||
{
|
||||
Name "ForwardLit"
|
||||
Tags { "LightMode" = "ForwardLit" }
|
||||
HLSLPROGRAM
|
||||
#pragma vertex MainVS
|
||||
#pragma fragment MainPS
|
||||
Texture2D BaseColorTexture;
|
||||
Texture2D _LightMap;
|
||||
SamplerState LinearClampSampler;
|
||||
float4 MainVS() : SV_POSITION
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
float4 MainPS() : SV_TARGET
|
||||
{
|
||||
return _LightMap.Sample(LinearClampSampler, float2(0.0f, 0.0f));
|
||||
}
|
||||
ENDHLSL
|
||||
}
|
||||
}
|
||||
}
|
||||
)");
|
||||
|
||||
ShaderLoader loader;
|
||||
LoadResult result = loader.Load(shaderPath.string().c_str());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
Shader* shader = static_cast<Shader*>(result.resource);
|
||||
ASSERT_NE(shader, nullptr);
|
||||
|
||||
const ShaderPass* pass = shader->FindPass("ForwardLit");
|
||||
ASSERT_NE(pass, nullptr);
|
||||
EXPECT_EQ(pass->resources.Size(), 9u);
|
||||
|
||||
const ShaderResourceBindingDesc* lightMapBinding =
|
||||
shader->FindPassResourceBinding("ForwardLit", "_LightMap");
|
||||
ASSERT_NE(lightMapBinding, nullptr);
|
||||
EXPECT_EQ(lightMapBinding->type, ShaderResourceType::Texture2D);
|
||||
EXPECT_EQ(lightMapBinding->set, 4u);
|
||||
EXPECT_EQ(lightMapBinding->binding, 1u);
|
||||
|
||||
delete shader;
|
||||
fs::remove_all(shaderRoot);
|
||||
}
|
||||
|
||||
TEST(ShaderLoader, LoadNahidaProjectCharacterToonShader) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path shaderPath =
|
||||
fs::path(__FILE__).parent_path().parent_path().parent_path().parent_path() /
|
||||
"project/Assets/Shaders/XCCharacterToon.shader";
|
||||
ASSERT_TRUE(fs::exists(shaderPath)) << shaderPath.string();
|
||||
|
||||
ShaderLoader loader;
|
||||
LoadResult result = loader.Load(shaderPath.string().c_str());
|
||||
ASSERT_TRUE(result) << result.errorMessage.CStr();
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
Shader* shader = static_cast<Shader*>(result.resource);
|
||||
ASSERT_NE(shader, nullptr);
|
||||
|
||||
const ShaderPass* pass = shader->FindPass("ForwardLit");
|
||||
ASSERT_NE(pass, nullptr);
|
||||
EXPECT_EQ(shader->GetProperties().Size(), 43u);
|
||||
EXPECT_EQ(pass->resources.Size(), 14u);
|
||||
EXPECT_NE(shader->FindPassResourceBinding("ForwardLit", "_LightMap"), nullptr);
|
||||
EXPECT_NE(shader->FindPassResourceBinding("ForwardLit", "_NormalMap"), nullptr);
|
||||
EXPECT_NE(shader->FindPassResourceBinding("ForwardLit", "_FaceLightMap"), nullptr);
|
||||
EXPECT_NE(shader->FindPassResourceBinding("ForwardLit", "_FaceShadow"), nullptr);
|
||||
EXPECT_NE(shader->FindPassResourceBinding("ForwardLit", "_ShadowRamp"), nullptr);
|
||||
EXPECT_NE(shader->FindPassResourceBinding("ForwardLit", "_MetalMap"), nullptr);
|
||||
EXPECT_NE(shader->FindPass("DepthOnly"), nullptr);
|
||||
EXPECT_NE(shader->FindPass("ShadowCaster"), nullptr);
|
||||
|
||||
delete shader;
|
||||
}
|
||||
|
||||
TEST(ShaderLoader, NahidaProjectCharacterToonShaderCompilesSurfaceAndFaceRuntimeVariants) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path shaderPath =
|
||||
fs::path(__FILE__).parent_path().parent_path().parent_path().parent_path() /
|
||||
"project/Assets/Shaders/XCCharacterToon.shader";
|
||||
ASSERT_TRUE(fs::exists(shaderPath)) << shaderPath.string();
|
||||
|
||||
ShaderLoader loader;
|
||||
LoadResult result = loader.Load(shaderPath.string().c_str());
|
||||
ASSERT_TRUE(result) << result.errorMessage.CStr();
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
Shader* shader = static_cast<Shader*>(result.resource);
|
||||
ASSERT_NE(shader, nullptr);
|
||||
|
||||
const ShaderPass* pass = shader->FindPass("ForwardLit");
|
||||
ASSERT_NE(pass, nullptr);
|
||||
|
||||
auto compileVariant =
|
||||
[&pass](
|
||||
const ShaderStageVariant& variant,
|
||||
ShaderBackend backend,
|
||||
std::string* outGlslSource = nullptr) {
|
||||
XCEngine::RHI::ShaderCompileDesc compileDesc = {};
|
||||
::XCEngine::Rendering::Internal::ApplyShaderStageVariant(
|
||||
*pass,
|
||||
backend,
|
||||
variant,
|
||||
compileDesc);
|
||||
|
||||
XCEngine::RHI::CompiledSpirvShader spirvShader = {};
|
||||
std::string errorMessage;
|
||||
EXPECT_TRUE(
|
||||
XCEngine::RHI::CompileSpirvShader(
|
||||
compileDesc,
|
||||
XCEngine::RHI::SpirvTargetEnvironment::Vulkan,
|
||||
spirvShader,
|
||||
&errorMessage))
|
||||
<< errorMessage;
|
||||
|
||||
if (outGlslSource != nullptr) {
|
||||
EXPECT_TRUE(XCEngine::RHI::TranspileSpirvToOpenGLGLSL(spirvShader, *outGlslSource, &errorMessage))
|
||||
<< errorMessage;
|
||||
}
|
||||
};
|
||||
|
||||
ShaderKeywordSet surfaceKeywords = {};
|
||||
surfaceKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST");
|
||||
surfaceKeywords.enabledKeywords.PushBack("_NORMAL_MAP");
|
||||
surfaceKeywords.enabledKeywords.PushBack("_RIM");
|
||||
surfaceKeywords.enabledKeywords.PushBack("_SPECULAR");
|
||||
|
||||
const ShaderStageVariant* surfaceVertex = shader->FindVariant(
|
||||
"ForwardLit",
|
||||
ShaderType::Vertex,
|
||||
ShaderBackend::OpenGL,
|
||||
surfaceKeywords);
|
||||
ASSERT_NE(surfaceVertex, nullptr);
|
||||
|
||||
const ShaderStageVariant* surfaceFragment = shader->FindVariant(
|
||||
"ForwardLit",
|
||||
ShaderType::Fragment,
|
||||
ShaderBackend::OpenGL,
|
||||
surfaceKeywords);
|
||||
ASSERT_NE(surfaceFragment, nullptr);
|
||||
|
||||
std::string surfaceVertexGlsl;
|
||||
compileVariant(*surfaceVertex, ShaderBackend::OpenGL, &surfaceVertexGlsl);
|
||||
EXPECT_NE(surfaceVertexGlsl.find("layout(location = 3) in vec3"), std::string::npos);
|
||||
EXPECT_NE(surfaceVertexGlsl.find("layout(location = 4) in vec3"), std::string::npos);
|
||||
|
||||
std::string surfaceFragmentGlsl;
|
||||
compileVariant(*surfaceFragment, ShaderBackend::OpenGL, &surfaceFragmentGlsl);
|
||||
EXPECT_NE(surfaceFragmentGlsl.find("sampler2D SPIRV_Cross_Combined_NormalMapLinearClampSampler"), std::string::npos);
|
||||
EXPECT_NE(surfaceFragmentGlsl.find("sampler2D SPIRV_Cross_Combined_MetalMapLinearClampSampler"), std::string::npos);
|
||||
|
||||
ShaderKeywordSet faceKeywords = {};
|
||||
faceKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST");
|
||||
faceKeywords.enabledKeywords.PushBack("_IS_FACE");
|
||||
faceKeywords.enabledKeywords.PushBack("_RIM");
|
||||
|
||||
const ShaderStageVariant* faceFragment = shader->FindVariant(
|
||||
"ForwardLit",
|
||||
ShaderType::Fragment,
|
||||
ShaderBackend::OpenGL,
|
||||
faceKeywords);
|
||||
ASSERT_NE(faceFragment, nullptr);
|
||||
|
||||
std::string faceFragmentGlsl;
|
||||
compileVariant(*faceFragment, ShaderBackend::OpenGL, &faceFragmentGlsl);
|
||||
EXPECT_NE(faceFragmentGlsl.find("sampler2D SPIRV_Cross_Combined_FaceLightMapLinearClampSampler"), std::string::npos);
|
||||
EXPECT_NE(faceFragmentGlsl.find("sampler2D SPIRV_Cross_Combined_FaceShadowLinearClampSampler"), std::string::npos);
|
||||
|
||||
delete shader;
|
||||
}
|
||||
|
||||
TEST(ShaderLoader, LoadShaderAuthoringRejectsPassMixingComputeAndGraphicsPragmas) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -2089,9 +2329,11 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringAndLoaderReads
|
||||
ASSERT_TRUE(resolvedAsset.artifactReady);
|
||||
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).extension().string(), ".xcshader");
|
||||
EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr()));
|
||||
EXPECT_FALSE(resolvedAsset.artifactMainEntryPath.Empty());
|
||||
EXPECT_NE(resolvedAsset.artifactMainEntryPath, resolvedAsset.artifactMainPath);
|
||||
|
||||
ShaderLoader loader;
|
||||
LoadResult result = loader.Load(resolvedAsset.artifactMainPath.CStr());
|
||||
LoadResult result = loader.Load(resolvedAsset.artifactMainEntryPath.CStr());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
@@ -2112,6 +2354,10 @@ TEST(ShaderLoader, AssetDatabaseCreatesShaderArtifactFromAuthoringAndLoaderReads
|
||||
shader->FindVariant("ForwardLit", ShaderType::Fragment, ShaderBackend::OpenGL);
|
||||
ASSERT_NE(fragmentVariant, nullptr);
|
||||
EXPECT_NE(std::string(fragmentVariant->sourceCode.CStr()).find("ARTIFACT_AUTHORING_PS"), std::string::npos);
|
||||
const Array<XCEngine::Core::uint8>* d3d12Binary =
|
||||
fragmentVariant->GetCompiledBinaryForBackend(ShaderBackend::D3D12);
|
||||
ASSERT_NE(d3d12Binary, nullptr);
|
||||
EXPECT_FALSE(d3d12Binary->Empty());
|
||||
|
||||
ShaderKeywordSet enabledKeywords = {};
|
||||
enabledKeywords.enabledKeywords.PushBack("XC_ALPHA_TEST");
|
||||
@@ -2181,12 +2427,7 @@ TEST(ShaderLoader, AssetDatabaseReimportsLegacyShaderArtifactHeaderBeforeLoad) {
|
||||
std::memcpy(legacyHeader.magic, "XCSHD04", 7);
|
||||
legacyHeader.magic[7] = '\0';
|
||||
legacyHeader.schemaVersion = 4u;
|
||||
{
|
||||
std::fstream output(firstResolve.artifactMainPath.CStr(), std::ios::binary | std::ios::in | std::ios::out);
|
||||
ASSERT_TRUE(output.is_open());
|
||||
output.write(reinterpret_cast<const char*>(&legacyHeader), sizeof(legacyHeader));
|
||||
ASSERT_TRUE(static_cast<bool>(output));
|
||||
}
|
||||
WriteShaderArtifactFileHeader(firstResolve.artifactMainPath.CStr(), legacyHeader);
|
||||
|
||||
AssetDatabase::ResolvedAsset secondResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/Shaders/lit.shader", ResourceType::Shader, secondResolve));
|
||||
@@ -2194,7 +2435,7 @@ TEST(ShaderLoader, AssetDatabaseReimportsLegacyShaderArtifactHeaderBeforeLoad) {
|
||||
ASSERT_TRUE(secondResolve.artifactReady);
|
||||
|
||||
const ShaderArtifactFileHeader currentHeader = ReadShaderArtifactFileHeader(secondResolve.artifactMainPath.CStr());
|
||||
EXPECT_EQ(std::string(currentHeader.magic, currentHeader.magic + 7), std::string("XCSHD05"));
|
||||
EXPECT_EQ(std::string(currentHeader.magic, currentHeader.magic + 7), std::string("XCSHD06"));
|
||||
EXPECT_EQ(currentHeader.schemaVersion, kShaderArtifactSchemaVersion);
|
||||
|
||||
ShaderLoader loader;
|
||||
@@ -3200,7 +3441,7 @@ TEST(ShaderLoader, LoadBuiltinObjectIdOutlineShaderBuildsAuthoringVariants) {
|
||||
ASSERT_NE(pass, nullptr);
|
||||
ASSERT_EQ(pass->variants.Size(), 2u);
|
||||
ASSERT_EQ(pass->tags.Size(), 1u);
|
||||
EXPECT_TRUE(pass->resources.Empty());
|
||||
EXPECT_FALSE(pass->resources.Empty());
|
||||
EXPECT_TRUE(pass->hasFixedFunctionState);
|
||||
EXPECT_EQ(pass->fixedFunctionState.cullMode, MaterialCullMode::None);
|
||||
EXPECT_FALSE(pass->fixedFunctionState.depthWriteEnable);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
@@ -8,12 +10,14 @@
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Containers;
|
||||
namespace Core = XCEngine::Core;
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -103,9 +107,14 @@ TEST(TextureLoader, AssetDatabaseCreatesTextureArtifactAndReusesItWithoutReimpor
|
||||
ASSERT_TRUE(firstResolve.exists);
|
||||
ASSERT_TRUE(firstResolve.artifactReady);
|
||||
EXPECT_TRUE(fs::exists(projectRoot / "Assets" / "checker.bmp.meta"));
|
||||
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "SourceAssetDB" / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "ArtifactDB" / "artifacts.db"));
|
||||
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "artifacts.db"));
|
||||
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
|
||||
const fs::path firstArtifactPath(firstResolve.artifactMainPath.CStr());
|
||||
ASSERT_TRUE(firstArtifactPath.has_filename());
|
||||
ASSERT_TRUE(firstArtifactPath.has_parent_path());
|
||||
EXPECT_EQ(firstArtifactPath.parent_path().filename().string(),
|
||||
firstArtifactPath.stem().string().substr(0, 2));
|
||||
|
||||
std::ifstream metaFile(projectRoot / "Assets" / "checker.bmp.meta");
|
||||
ASSERT_TRUE(metaFile.is_open());
|
||||
@@ -223,4 +232,64 @@ TEST(TextureLoader, ResourceManagerLoadsLibraryArtifactTextureWithoutReimporting
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(TextureLoader, LoadTextureArtifactFromContainerEntryPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_texture_container_entry_path_test";
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(projectRoot);
|
||||
|
||||
ArtifactContainerWriter writer;
|
||||
|
||||
TextureArtifactHeader header;
|
||||
header.textureType = static_cast<Core::uint32>(TextureType::Texture2D);
|
||||
header.textureFormat = static_cast<Core::uint32>(TextureFormat::RGBA8_UNORM);
|
||||
header.width = 2;
|
||||
header.height = 2;
|
||||
header.depth = 1;
|
||||
header.mipLevels = 1;
|
||||
header.arraySize = 1;
|
||||
header.pixelDataSize = 16;
|
||||
|
||||
ArtifactContainerEntry textureEntry;
|
||||
textureEntry.name = "texture_0.xctex";
|
||||
textureEntry.resourceType = ResourceType::Texture;
|
||||
textureEntry.payload.Resize(sizeof(TextureArtifactHeader) + static_cast<size_t>(header.pixelDataSize));
|
||||
std::memcpy(textureEntry.payload.Data(), &header, sizeof(TextureArtifactHeader));
|
||||
|
||||
Core::uint8* pixelBytes = textureEntry.payload.Data() + sizeof(TextureArtifactHeader);
|
||||
const Core::uint8 pixelData[16] = {
|
||||
255, 0, 0, 255,
|
||||
0, 255, 0, 255,
|
||||
0, 0, 255, 255,
|
||||
255, 255, 255, 255
|
||||
};
|
||||
std::memcpy(pixelBytes, pixelData, sizeof(pixelData));
|
||||
writer.AddEntry(std::move(textureEntry));
|
||||
|
||||
const String containerPath =
|
||||
(projectRoot / "Library" / "Artifacts" / "ab" / "artifact.xcmodel").string().c_str();
|
||||
String errorMessage;
|
||||
ASSERT_TRUE(writer.WriteToFile(containerPath, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
|
||||
TextureLoader loader;
|
||||
const String virtualPath =
|
||||
BuildArtifactContainerEntryPath(containerPath, "texture_0.xctex");
|
||||
LoadResult result = loader.Load(virtualPath);
|
||||
ASSERT_TRUE(result) << result.errorMessage.CStr();
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* texture = static_cast<Texture*>(result.resource);
|
||||
EXPECT_EQ(texture->GetWidth(), 2u);
|
||||
EXPECT_EQ(texture->GetHeight(), 2u);
|
||||
EXPECT_EQ(texture->GetTextureType(), TextureType::Texture2D);
|
||||
EXPECT_EQ(texture->GetFormat(), TextureFormat::RGBA8_UNORM);
|
||||
EXPECT_EQ(texture->GetPixelDataSize(), 16u);
|
||||
EXPECT_EQ(texture->GetPath(), virtualPath);
|
||||
|
||||
delete texture;
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/AssetImportService.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
@@ -147,9 +148,19 @@ TEST(UIDocumentLoader, AssetDatabaseImportsViewArtifactAndReimportsWhenDependenc
|
||||
ASSERT_TRUE(firstResolve.artifactReady);
|
||||
EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().string(), ".xcuiasset");
|
||||
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
|
||||
EXPECT_TRUE(IsArtifactContainerFile(firstResolve.artifactMainPath));
|
||||
EXPECT_FALSE(firstResolve.artifactMainEntryPath.Empty());
|
||||
EXPECT_NE(firstResolve.artifactMainEntryPath, firstResolve.artifactMainPath);
|
||||
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> artifactPayload;
|
||||
ASSERT_TRUE(ReadArtifactContainerMainEntryPayload(
|
||||
firstResolve.artifactMainPath,
|
||||
ResourceType::UIView,
|
||||
artifactPayload));
|
||||
EXPECT_FALSE(artifactPayload.Empty());
|
||||
|
||||
UIViewLoader loader;
|
||||
LoadResult firstLoad = loader.Load(firstResolve.artifactMainPath.CStr());
|
||||
LoadResult firstLoad = loader.Load(firstResolve.artifactMainEntryPath.CStr());
|
||||
ASSERT_TRUE(firstLoad);
|
||||
auto* firstView = static_cast<UIView*>(firstLoad.resource);
|
||||
ASSERT_NE(firstView, nullptr);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
|
||||
|
||||
@@ -103,6 +104,13 @@ TEST(UISchemaDocument, CompileAndArtifactLoadPopulateSchemaDefinition) {
|
||||
WriteUIDocumentArtifact(artifactPath.string().c_str(), compileResult, &artifactWriteError))
|
||||
<< artifactWriteError.CStr();
|
||||
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> artifactPayload;
|
||||
ASSERT_TRUE(ReadArtifactContainerMainEntryPayload(
|
||||
artifactPath.string().c_str(),
|
||||
ResourceType::UISchema,
|
||||
artifactPayload));
|
||||
EXPECT_FALSE(artifactPayload.Empty());
|
||||
|
||||
UIDocumentCompileResult artifactResult = {};
|
||||
ASSERT_TRUE(LoadUIDocumentArtifact(
|
||||
artifactPath.string().c_str(),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
@@ -159,40 +161,58 @@ TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReim
|
||||
fs::copy_file(fixturePath, volumePath, fs::copy_options::overwrite_existing);
|
||||
const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(fixturePath);
|
||||
|
||||
AssetDatabase database;
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
{
|
||||
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_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().generic_string(), ".xcvol");
|
||||
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<XCEngine::Core::uint8> 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<VolumeField*>(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;
|
||||
VolumeFieldLoader loader;
|
||||
LoadResult artifactLoad = loader.Load(firstResolve.artifactMainPath);
|
||||
ASSERT_TRUE(artifactLoad);
|
||||
ASSERT_NE(artifactLoad.resource, nullptr);
|
||||
auto* artifactVolume = static_cast<VolumeField*>(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;
|
||||
|
||||
const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr());
|
||||
std::this_thread::sleep_for(50ms);
|
||||
LoadResult entryLoad = loader.Load(firstResolve.artifactMainEntryPath);
|
||||
ASSERT_TRUE(entryLoad);
|
||||
ASSERT_NE(entryLoad.resource, nullptr);
|
||||
delete entryLoad.resource;
|
||||
|
||||
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()));
|
||||
const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr());
|
||||
std::this_thread::sleep_for(50ms);
|
||||
|
||||
database.Shutdown();
|
||||
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
|
||||
|
||||
@@ -15,7 +15,8 @@ Rules:
|
||||
- One scenario directory maps to one executable.
|
||||
- Do not accumulate unrelated checks into one monolithic app.
|
||||
- Shared infrastructure belongs in `shared/`, not duplicated per scenario.
|
||||
- Screenshots are stored per scenario inside that scenario's `captures/` folder.
|
||||
- Screenshots are written only under the active CMake build directory.
|
||||
- The output root is the executable directory: `.../Debug/captures/<scenario>/`.
|
||||
|
||||
Build:
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
add_subdirectory(drag_drop_basic)
|
||||
add_subdirectory(keyboard_focus)
|
||||
add_subdirectory(popup_menu_overlay)
|
||||
add_subdirectory(pointer_states)
|
||||
add_subdirectory(scroll_view)
|
||||
add_subdirectory(shortcut_scope)
|
||||
|
||||
add_custom_target(core_ui_drag_drop_contract_validation
|
||||
DEPENDS
|
||||
core_ui_input_drag_drop_basic_validation
|
||||
)
|
||||
|
||||
add_custom_target(core_ui_input_integration_tests
|
||||
DEPENDS
|
||||
core_ui_input_drag_drop_basic_validation
|
||||
core_ui_input_keyboard_focus_validation
|
||||
core_ui_input_popup_menu_overlay_validation
|
||||
core_ui_input_pointer_states_validation
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
set(CORE_UI_INPUT_DRAG_DROP_BASIC_RESOURCES
|
||||
View.xcui
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/integration/shared/themes/core_validation.xctheme
|
||||
)
|
||||
|
||||
add_executable(core_ui_input_drag_drop_basic_validation WIN32
|
||||
main.cpp
|
||||
${CORE_UI_INPUT_DRAG_DROP_BASIC_RESOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(core_ui_input_drag_drop_basic_validation PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/integration/shared/src
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
target_compile_definitions(core_ui_input_drag_drop_basic_validation PRIVATE
|
||||
UNICODE
|
||||
_UNICODE
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(core_ui_input_drag_drop_basic_validation PRIVATE /utf-8 /FS)
|
||||
set_property(TARGET core_ui_input_drag_drop_basic_validation PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
target_link_libraries(core_ui_input_drag_drop_basic_validation PRIVATE
|
||||
core_ui_integration_host
|
||||
)
|
||||
|
||||
set_target_properties(core_ui_input_drag_drop_basic_validation PROPERTIES
|
||||
OUTPUT_NAME "XCUICoreDragDropContractValidation"
|
||||
)
|
||||
|
||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES View.xcui)
|
||||
19
tests/UI/Core/integration/input/drag_drop_basic/View.xcui
Normal file
19
tests/UI/Core/integration/input/drag_drop_basic/View.xcui
Normal file
@@ -0,0 +1,19 @@
|
||||
<View
|
||||
name="CoreInputDragDropContract"
|
||||
theme="../../shared/themes/core_validation.xctheme">
|
||||
<Column padding="24" gap="16">
|
||||
<Card
|
||||
title="测试内容:Core Drag / Drop Contract"
|
||||
subtitle="只验证 Core 层拖拽原语本身:激活阈值、target accept/reject、release 完成、以及 Escape / focus lost 取消。"
|
||||
tone="accent"
|
||||
height="206">
|
||||
<Column gap="8">
|
||||
<Text text="1. 按住左侧任一 source 后不要立刻松开;只有拖过激活阈值后才会进入 active。" />
|
||||
<Text text="2. 将 Texture Asset 拖到 Project Browser,应显示 accept,预览操作应解析为 Copy。" />
|
||||
<Text text="3. 将 Texture Asset 拖到 Hierarchy Parent,应显示 reject;此时松开只会取消,不会完成 drop。" />
|
||||
<Text text="4. 将 Scene Entity 拖到 Hierarchy Parent,应显示 accept;松开后应完成 Move。" />
|
||||
<Text text="5. active 期间按 Esc,或切走窗口触发 focus lost,应立即取消当前 drag。" />
|
||||
</Column>
|
||||
</Card>
|
||||
</Column>
|
||||
</View>
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
8
tests/UI/Core/integration/input/drag_drop_basic/main.cpp
Normal file
8
tests/UI/Core/integration/input/drag_drop_basic/main.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "Application.h"
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||
return XCEngine::Tests::CoreUI::RunCoreUIValidationApp(
|
||||
hInstance,
|
||||
nCmdShow,
|
||||
"core.input.drag_drop_basic");
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_CORE_UI_TESTS_REPO_ROOT_PATH)
|
||||
file(TO_CMAKE_PATH "${CMAKE_BINARY_DIR}" XCENGINE_CORE_UI_TESTS_BUILD_ROOT_PATH)
|
||||
|
||||
add_library(core_ui_validation_registry STATIC
|
||||
src/CoreValidationScenario.cpp
|
||||
@@ -29,6 +30,7 @@ target_link_libraries(core_ui_validation_registry
|
||||
add_library(core_ui_integration_host STATIC
|
||||
src/AutoScreenshot.cpp
|
||||
src/Application.cpp
|
||||
src/DragDropValidationScene.cpp
|
||||
src/NativeRenderer.cpp
|
||||
src/PopupMenuOverlayValidationScene.cpp
|
||||
)
|
||||
@@ -44,6 +46,7 @@ target_compile_definitions(core_ui_integration_host
|
||||
UNICODE
|
||||
_UNICODE
|
||||
XCENGINE_CORE_UI_TESTS_REPO_ROOT="${XCENGINE_CORE_UI_TESTS_REPO_ROOT_PATH}"
|
||||
XCENGINE_CORE_UI_TESTS_BUILD_ROOT="${XCENGINE_CORE_UI_TESTS_BUILD_ROOT_PATH}"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
#define XCENGINE_CORE_UI_TESTS_REPO_ROOT "."
|
||||
#endif
|
||||
|
||||
#ifndef XCENGINE_CORE_UI_TESTS_BUILD_ROOT
|
||||
#define XCENGINE_CORE_UI_TESTS_BUILD_ROOT "."
|
||||
#endif
|
||||
|
||||
namespace XCEngine::Tests::CoreUI {
|
||||
|
||||
namespace {
|
||||
@@ -53,6 +57,47 @@ std::filesystem::path GetRepoRootPath() {
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path GetBuildRootPath() {
|
||||
std::string root = XCENGINE_CORE_UI_TESTS_BUILD_ROOT;
|
||||
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||
root = root.substr(1u, root.size() - 2u);
|
||||
}
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
bool TryMakeRepoRelativePath(
|
||||
const std::filesystem::path& absolutePath,
|
||||
std::filesystem::path& outRelativePath) {
|
||||
std::error_code errorCode = {};
|
||||
outRelativePath = std::filesystem::relative(
|
||||
absolutePath,
|
||||
GetRepoRootPath(),
|
||||
errorCode);
|
||||
if (errorCode || outRelativePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& part : outRelativePath) {
|
||||
if (part == "..") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveCaptureOutputRoot(
|
||||
const std::filesystem::path& sourceCaptureRoot) {
|
||||
const std::filesystem::path normalizedSourcePath =
|
||||
sourceCaptureRoot.lexically_normal();
|
||||
std::filesystem::path relativePath = {};
|
||||
if (TryMakeRepoRelativePath(normalizedSourcePath, relativePath)) {
|
||||
return (GetBuildRootPath() / relativePath).lexically_normal();
|
||||
}
|
||||
|
||||
return (GetBuildRootPath() / "ui_test_captures" / normalizedSourcePath.filename())
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
std::string TruncateText(const std::string& text, std::size_t maxLength) {
|
||||
if (text.size() <= maxLength) {
|
||||
return text;
|
||||
@@ -250,7 +295,8 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
if (initialScenario == nullptr) {
|
||||
initialScenario = &GetDefaultCoreValidationScenario();
|
||||
}
|
||||
m_autoScreenshot.Initialize(initialScenario->captureRootPath);
|
||||
m_autoScreenshot.Initialize(
|
||||
ResolveCaptureOutputRoot(initialScenario->captureRootPath));
|
||||
LoadStructuredScreen("startup");
|
||||
return true;
|
||||
}
|
||||
@@ -306,6 +352,10 @@ void Application::RenderFrame() {
|
||||
m_activeScenario->id == PopupMenuOverlayValidationScene::ScenarioId) {
|
||||
m_popupMenuOverlayScene.Update(frameEvents, viewportRect, windowFocused);
|
||||
}
|
||||
if (m_activeScenario != nullptr &&
|
||||
m_activeScenario->id == DragDropValidationScene::ScenarioId) {
|
||||
m_dragDropValidationScene.Update(frameEvents, viewportRect, windowFocused);
|
||||
}
|
||||
|
||||
if (m_useStructuredScreen && m_screenPlayer.IsLoaded()) {
|
||||
UIScreenFrameInput input = {};
|
||||
@@ -330,6 +380,10 @@ void Application::RenderFrame() {
|
||||
m_activeScenario->id == PopupMenuOverlayValidationScene::ScenarioId) {
|
||||
m_popupMenuOverlayScene.AppendDrawData(drawData, viewportRect);
|
||||
}
|
||||
if (m_activeScenario != nullptr &&
|
||||
m_activeScenario->id == DragDropValidationScene::ScenarioId) {
|
||||
m_dragDropValidationScene.AppendDrawData(drawData, viewportRect);
|
||||
}
|
||||
|
||||
if (drawData.Empty()) {
|
||||
m_runtimeStatus = "Core UI Validation | Load Error";
|
||||
@@ -447,6 +501,7 @@ bool Application::LoadStructuredScreen(const char* triggerReason) {
|
||||
: (scenarioLoadWarning.empty()
|
||||
? m_screenPlayer.GetLastError()
|
||||
: scenarioLoadWarning + " | " + m_screenPlayer.GetLastError());
|
||||
m_dragDropValidationScene.Reset();
|
||||
m_popupMenuOverlayScene.Reset();
|
||||
RebuildTrackedFileStates();
|
||||
return loaded;
|
||||
@@ -641,7 +696,11 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
|
||||
} else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) {
|
||||
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u));
|
||||
} else {
|
||||
detailLines.push_back("Screenshots: F12 -> current scenario captures/");
|
||||
detailLines.push_back(
|
||||
"Screenshots: F12 -> " +
|
||||
TruncateText(
|
||||
m_autoScreenshot.GetLatestCapturePath().parent_path().string(),
|
||||
60u));
|
||||
}
|
||||
|
||||
if (!m_runtimeError.empty()) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "AutoScreenshot.h"
|
||||
#include "CoreValidationScenario.h"
|
||||
#include "DragDropValidationScene.h"
|
||||
#include "InputModifierTracker.h"
|
||||
#include "NativeRenderer.h"
|
||||
#include "PopupMenuOverlayValidationScene.h"
|
||||
@@ -73,6 +74,7 @@ private:
|
||||
std::uint64_t m_frameIndex = 0;
|
||||
std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {};
|
||||
Host::InputModifierTracker m_inputModifierTracker = {};
|
||||
DragDropValidationScene m_dragDropValidationScene = {};
|
||||
PopupMenuOverlayValidationScene m_popupMenuOverlayScene = {};
|
||||
bool m_trackingMouseLeave = false;
|
||||
bool m_useStructuredScreen = false;
|
||||
|
||||
@@ -4,21 +4,80 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace XCEngine::Tests::CoreUI::Host {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsAutoCaptureOnStartupEnabled() {
|
||||
const char* value = std::getenv("XCUI_AUTO_CAPTURE_ON_STARTUP");
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string normalized = value;
|
||||
for (char& character : normalized) {
|
||||
character = static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
|
||||
}
|
||||
|
||||
return normalized != "0" &&
|
||||
normalized != "false" &&
|
||||
normalized != "off" &&
|
||||
normalized != "no";
|
||||
}
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
const DWORD copied = ::GetModuleFileNameW(
|
||||
nullptr,
|
||||
buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (copied == 0u) {
|
||||
return std::filesystem::current_path().lexically_normal();
|
||||
}
|
||||
|
||||
if (copied < buffer.size() - 1u) {
|
||||
return std::filesystem::path(std::wstring(buffer.data(), copied))
|
||||
.parent_path()
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
buffer.resize(buffer.size() * 2u);
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveBuildCaptureRoot(const std::filesystem::path& requestedCaptureRoot) {
|
||||
std::filesystem::path captureRoot = GetExecutableDirectory() / "captures";
|
||||
const std::filesystem::path scenarioPath = requestedCaptureRoot.parent_path().filename();
|
||||
if (!scenarioPath.empty() && scenarioPath != "captures") {
|
||||
captureRoot /= scenarioPath;
|
||||
}
|
||||
|
||||
return captureRoot.lexically_normal();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AutoScreenshotController::Initialize(const std::filesystem::path& captureRoot) {
|
||||
m_captureRoot = captureRoot.lexically_normal();
|
||||
m_captureRoot = ResolveBuildCaptureRoot(captureRoot);
|
||||
m_historyRoot = (m_captureRoot / "history").lexically_normal();
|
||||
m_latestCapturePath = (m_captureRoot / "latest.png").lexically_normal();
|
||||
m_captureCount = 0;
|
||||
m_capturePending = false;
|
||||
m_pendingReason.clear();
|
||||
m_lastCaptureSummary.clear();
|
||||
m_lastCaptureSummary = "Output: " + m_captureRoot.string();
|
||||
m_lastCaptureError.clear();
|
||||
if (IsAutoCaptureOnStartupEnabled()) {
|
||||
RequestCapture("startup");
|
||||
}
|
||||
}
|
||||
|
||||
void AutoScreenshotController::Shutdown() {
|
||||
|
||||
@@ -24,8 +24,17 @@ fs::path RepoRelative(const char* relativePath) {
|
||||
return (RepoRootPath() / relativePath).lexically_normal();
|
||||
}
|
||||
|
||||
const std::array<CoreValidationScenario, 10>& GetCoreValidationScenarios() {
|
||||
static const std::array<CoreValidationScenario, 10> scenarios = { {
|
||||
const std::array<CoreValidationScenario, 11>& GetCoreValidationScenarios() {
|
||||
static const std::array<CoreValidationScenario, 11> scenarios = { {
|
||||
{
|
||||
"core.input.drag_drop_basic",
|
||||
UIValidationDomain::Core,
|
||||
"input",
|
||||
"Core Input | Drag Drop Contract",
|
||||
RepoRelative("tests/UI/Core/integration/input/drag_drop_basic/View.xcui"),
|
||||
RepoRelative("tests/UI/Core/integration/shared/themes/core_validation.xctheme"),
|
||||
RepoRelative("tests/UI/Core/integration/input/drag_drop_basic/captures")
|
||||
},
|
||||
{
|
||||
"core.input.keyboard_focus",
|
||||
UIValidationDomain::Core,
|
||||
@@ -123,6 +132,12 @@ const std::array<CoreValidationScenario, 10>& GetCoreValidationScenarios() {
|
||||
} // namespace
|
||||
|
||||
const CoreValidationScenario& GetDefaultCoreValidationScenario() {
|
||||
for (const CoreValidationScenario& scenario : GetCoreValidationScenarios()) {
|
||||
if (scenario.id == "core.input.keyboard_focus") {
|
||||
return scenario;
|
||||
}
|
||||
}
|
||||
|
||||
return GetCoreValidationScenarios().front();
|
||||
}
|
||||
|
||||
|
||||
504
tests/UI/Core/integration/shared/src/DragDropValidationScene.cpp
Normal file
504
tests/UI/Core/integration/shared/src/DragDropValidationScene.cpp
Normal file
@@ -0,0 +1,504 @@
|
||||
#include "DragDropValidationScene.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::Tests::CoreUI {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Input::KeyCode;
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawData;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::Widgets::BeginUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::CancelUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::EndUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::HasResolvedUIDragDropTarget;
|
||||
using ::XCEngine::UI::Widgets::IsUIDragDropInProgress;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropOperation;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropPayload;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropResult;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropSourceDescriptor;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropTargetDescriptor;
|
||||
using ::XCEngine::UI::Widgets::UpdateUIDragDropPointer;
|
||||
using ::XCEngine::UI::Widgets::UpdateUIDragDropTarget;
|
||||
|
||||
constexpr UIColor kLabPanelBg(0.12f, 0.12f, 0.12f, 1.0f);
|
||||
constexpr UIColor kLabPanelBorder(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
constexpr UIColor kStatusBg(0.16f, 0.16f, 0.16f, 1.0f);
|
||||
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
constexpr UIColor kCardHover(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
constexpr UIColor kCardBorder(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
constexpr UIColor kAccept(0.36f, 0.46f, 0.36f, 1.0f);
|
||||
constexpr UIColor kAcceptBorder(0.56f, 0.72f, 0.56f, 1.0f);
|
||||
constexpr UIColor kReject(0.34f, 0.22f, 0.22f, 1.0f);
|
||||
constexpr UIColor kRejectBorder(0.72f, 0.38f, 0.38f, 1.0f);
|
||||
constexpr UIColor kGhostBg(0.28f, 0.28f, 0.28f, 0.95f);
|
||||
constexpr UIColor kGhostBorder(0.78f, 0.78f, 0.78f, 1.0f);
|
||||
constexpr UIColor kTextPrimary(0.93f, 0.93f, 0.93f, 1.0f);
|
||||
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||
constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f);
|
||||
constexpr UIColor kTextSuccess(0.62f, 0.82f, 0.62f, 1.0f);
|
||||
constexpr UIColor kTextDanger(0.84f, 0.48f, 0.48f, 1.0f);
|
||||
|
||||
constexpr std::uint64_t kTextureSourceOwnerId = 1001u;
|
||||
constexpr std::uint64_t kEntitySourceOwnerId = 1002u;
|
||||
constexpr std::uint64_t kProjectTargetOwnerId = 2001u;
|
||||
constexpr std::uint64_t kHierarchyTargetOwnerId = 2002u;
|
||||
|
||||
std::string DescribeOperation(UIDragDropOperation operation) {
|
||||
switch (operation) {
|
||||
case UIDragDropOperation::Copy:
|
||||
return "Copy";
|
||||
case UIDragDropOperation::Move:
|
||||
return "Move";
|
||||
case UIDragDropOperation::Link:
|
||||
return "Link";
|
||||
default:
|
||||
return "(none)";
|
||||
}
|
||||
}
|
||||
|
||||
void DrawPanel(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const UIColor& fillColor,
|
||||
const UIColor& borderColor,
|
||||
float rounding) {
|
||||
drawList.AddFilledRect(rect, fillColor, rounding);
|
||||
drawList.AddRectOutline(rect, borderColor, 1.0f, rounding);
|
||||
}
|
||||
|
||||
void DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
std::string_view title,
|
||||
std::string_view subtitle,
|
||||
const UIColor& fillColor,
|
||||
const UIColor& borderColor) {
|
||||
DrawPanel(drawList, rect, fillColor, borderColor, 10.0f);
|
||||
drawList.AddText(UIPoint(rect.x + 14.0f, rect.y + 12.0f), std::string(title), kTextPrimary, 15.0f);
|
||||
drawList.AddText(UIPoint(rect.x + 14.0f, rect.y + 34.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||
}
|
||||
|
||||
UIDragDropSourceDescriptor BuildTextureSource(const UIPoint& pointerPosition) {
|
||||
UIDragDropSourceDescriptor source = {};
|
||||
source.ownerId = kTextureSourceOwnerId;
|
||||
source.sourceId = "project.texture";
|
||||
source.pointerDownPosition = pointerPosition;
|
||||
source.payload = UIDragDropPayload{ "asset.texture", "tex-001", "Checker.png" };
|
||||
source.allowedOperations = UIDragDropOperation::Copy | UIDragDropOperation::Move;
|
||||
source.activationDistance = 6.0f;
|
||||
return source;
|
||||
}
|
||||
|
||||
UIDragDropSourceDescriptor BuildEntitySource(const UIPoint& pointerPosition) {
|
||||
UIDragDropSourceDescriptor source = {};
|
||||
source.ownerId = kEntitySourceOwnerId;
|
||||
source.sourceId = "hierarchy.entity";
|
||||
source.pointerDownPosition = pointerPosition;
|
||||
source.payload = UIDragDropPayload{ "scene.entity", "entity-hero", "HeroRoot" };
|
||||
source.allowedOperations = UIDragDropOperation::Move;
|
||||
source.activationDistance = 6.0f;
|
||||
return source;
|
||||
}
|
||||
|
||||
UIDragDropTargetDescriptor BuildProjectTarget() {
|
||||
static constexpr std::array<std::string_view, 2> kAcceptedTypes = {
|
||||
"asset.texture",
|
||||
"asset.material"
|
||||
};
|
||||
|
||||
UIDragDropTargetDescriptor target = {};
|
||||
target.ownerId = kProjectTargetOwnerId;
|
||||
target.targetId = "project.browser";
|
||||
target.acceptedPayloadTypes = kAcceptedTypes;
|
||||
target.acceptedOperations =
|
||||
UIDragDropOperation::Copy |
|
||||
UIDragDropOperation::Move;
|
||||
target.preferredOperation = UIDragDropOperation::Copy;
|
||||
return target;
|
||||
}
|
||||
|
||||
UIDragDropTargetDescriptor BuildHierarchyTarget() {
|
||||
static constexpr std::array<std::string_view, 1> kAcceptedTypes = {
|
||||
"scene.entity"
|
||||
};
|
||||
|
||||
UIDragDropTargetDescriptor target = {};
|
||||
target.ownerId = kHierarchyTargetOwnerId;
|
||||
target.targetId = "hierarchy.parent";
|
||||
target.acceptedPayloadTypes = kAcceptedTypes;
|
||||
target.acceptedOperations = UIDragDropOperation::Move;
|
||||
target.preferredOperation = UIDragDropOperation::Move;
|
||||
return target;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DragDropValidationScene::Reset() {
|
||||
m_dragState = {};
|
||||
m_pointerPosition = {};
|
||||
m_hasPointer = false;
|
||||
m_resultText = "Result: Ready";
|
||||
m_lastDropText = "(none)";
|
||||
}
|
||||
|
||||
void DragDropValidationScene::Update(
|
||||
const std::vector<UIInputEvent>& events,
|
||||
const UIRect& viewportRect,
|
||||
bool windowFocused) {
|
||||
const Geometry geometry = BuildGeometry(viewportRect);
|
||||
|
||||
if (!windowFocused &&
|
||||
IsUIDragDropInProgress(m_dragState)) {
|
||||
HandleCancel("Result: focus lost, drag cancelled");
|
||||
}
|
||||
|
||||
for (const UIInputEvent& event : events) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
m_pointerPosition = event.position;
|
||||
m_hasPointer = true;
|
||||
HandlePointerMove(geometry, event.position);
|
||||
break;
|
||||
case UIInputEventType::PointerLeave:
|
||||
m_hasPointer = false;
|
||||
break;
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
if (event.pointerButton == UIPointerButton::Left) {
|
||||
m_pointerPosition = event.position;
|
||||
m_hasPointer = true;
|
||||
HandlePointerDown(geometry, event.position);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
if (event.pointerButton == UIPointerButton::Left) {
|
||||
m_pointerPosition = event.position;
|
||||
m_hasPointer = true;
|
||||
HandlePointerUp(geometry, event.position);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::KeyDown:
|
||||
if (event.keyCode == static_cast<std::int32_t>(KeyCode::Escape)) {
|
||||
HandleCancel("Result: Escape cancelled current drag");
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::FocusLost:
|
||||
HandleCancel("Result: focus lost, drag cancelled");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DragDropValidationScene::AppendDrawData(
|
||||
UIDrawData& drawData,
|
||||
const UIRect& viewportRect) const {
|
||||
const Geometry geometry = BuildGeometry(viewportRect);
|
||||
const bool hoverTexture = m_hasPointer && RectContains(geometry.textureSourceRect, m_pointerPosition);
|
||||
const bool hoverEntity = m_hasPointer && RectContains(geometry.entitySourceRect, m_pointerPosition);
|
||||
const bool hoverProject = m_hasPointer && RectContains(geometry.projectTargetRect, m_pointerPosition);
|
||||
const bool hoverHierarchy = m_hasPointer && RectContains(geometry.hierarchyTargetRect, m_pointerPosition);
|
||||
const bool dragProject =
|
||||
m_dragState.active && m_dragState.targetOwnerId == kProjectTargetOwnerId;
|
||||
const bool dragHierarchy =
|
||||
m_dragState.active && m_dragState.targetOwnerId == kHierarchyTargetOwnerId;
|
||||
const bool rejectProject =
|
||||
m_dragState.active && hoverProject && !dragProject;
|
||||
const bool rejectHierarchy =
|
||||
m_dragState.active && hoverHierarchy && !dragHierarchy;
|
||||
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("Core Drag Drop Primitive Lab");
|
||||
DrawPanel(drawList, geometry.labRect, kLabPanelBg, kLabPanelBorder, 12.0f);
|
||||
DrawPanel(drawList, geometry.statusRect, kStatusBg, kCardBorder, 10.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 14.0f, geometry.statusRect.y + 12.0f),
|
||||
"测试内容:Core Drag / Drop Contract",
|
||||
kTextPrimary,
|
||||
14.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 14.0f, geometry.statusRect.y + 34.0f),
|
||||
m_resultText,
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 14.0f, geometry.statusRect.y + 54.0f),
|
||||
"Payload: " + (m_dragState.payload.label.empty() ? std::string("(none)") : m_dragState.payload.label),
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 280.0f, geometry.statusRect.y + 34.0f),
|
||||
std::string("Armed: ") + (m_dragState.armed ? "true" : "false"),
|
||||
m_dragState.armed ? kTextPrimary : kTextWeak,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 280.0f, geometry.statusRect.y + 54.0f),
|
||||
std::string("Active: ") + (m_dragState.active ? "true" : "false"),
|
||||
m_dragState.active ? kTextSuccess : kTextWeak,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 430.0f, geometry.statusRect.y + 34.0f),
|
||||
"Hover Target: " +
|
||||
(m_dragState.targetId.empty() ? std::string("(none)") : m_dragState.targetId),
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 430.0f, geometry.statusRect.y + 54.0f),
|
||||
"Preview Op: " + DescribeOperation(m_dragState.previewOperation),
|
||||
m_dragState.previewOperation == UIDragDropOperation::None ? kTextWeak : kTextSuccess,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 650.0f, geometry.statusRect.y + 34.0f),
|
||||
"Last Drop: " + m_lastDropText,
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.sourcesRect,
|
||||
"Sources",
|
||||
"按下 source 后先保持,越过阈值才会进入 active。",
|
||||
kCardBg,
|
||||
kCardBorder);
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.targetsRect,
|
||||
"Targets",
|
||||
"右侧同时展示 accept / reject 与预览操作。",
|
||||
kCardBg,
|
||||
kCardBorder);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.textureSourceRect,
|
||||
"Texture Asset",
|
||||
"type=asset.texture | Copy/Move",
|
||||
hoverTexture ? kCardHover : kCardBg,
|
||||
kCardBorder);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.textureSourceRect.x + 14.0f, geometry.textureSourceRect.y + 58.0f),
|
||||
"Checker.png",
|
||||
kTextPrimary,
|
||||
13.0f);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.entitySourceRect,
|
||||
"Scene Entity",
|
||||
"type=scene.entity | Move only",
|
||||
hoverEntity ? kCardHover : kCardBg,
|
||||
kCardBorder);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.entitySourceRect.x + 14.0f, geometry.entitySourceRect.y + 58.0f),
|
||||
"HeroRoot",
|
||||
kTextPrimary,
|
||||
13.0f);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.projectTargetRect,
|
||||
"Project Browser",
|
||||
"accepts asset.texture / asset.material | preferred Copy",
|
||||
dragProject ? kAccept : (rejectProject ? kReject : (hoverProject ? kCardHover : kCardBg)),
|
||||
dragProject ? kAcceptBorder : (rejectProject ? kRejectBorder : kCardBorder));
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.projectTargetRect.x + 14.0f, geometry.projectTargetRect.y + 58.0f),
|
||||
dragProject ? "Accepting current payload" : "拖入 texture 时应显示 Copy",
|
||||
dragProject ? kTextSuccess : (rejectProject ? kTextDanger : kTextMuted),
|
||||
12.0f);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.hierarchyTargetRect,
|
||||
"Hierarchy Parent",
|
||||
"accepts scene.entity | preferred Move",
|
||||
dragHierarchy ? kAccept : (rejectHierarchy ? kReject : (hoverHierarchy ? kCardHover : kCardBg)),
|
||||
dragHierarchy ? kAcceptBorder : (rejectHierarchy ? kRejectBorder : kCardBorder));
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.hierarchyTargetRect.x + 14.0f, geometry.hierarchyTargetRect.y + 58.0f),
|
||||
dragHierarchy ? "Accepting current payload" : "拖入 entity 时应显示 Move",
|
||||
dragHierarchy ? kTextSuccess : (rejectHierarchy ? kTextDanger : kTextMuted),
|
||||
12.0f);
|
||||
|
||||
if (m_dragState.active) {
|
||||
const UIRect ghostRect(
|
||||
m_pointerPosition.x + 16.0f,
|
||||
m_pointerPosition.y + 12.0f,
|
||||
188.0f,
|
||||
64.0f);
|
||||
DrawPanel(drawList, ghostRect, kGhostBg, kGhostBorder, 8.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(ghostRect.x + 12.0f, ghostRect.y + 12.0f),
|
||||
m_dragState.payload.label.empty() ? std::string("(payload)") : m_dragState.payload.label,
|
||||
kTextPrimary,
|
||||
14.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(ghostRect.x + 12.0f, ghostRect.y + 34.0f),
|
||||
"type=" + m_dragState.payload.typeId + " | op=" + DescribeOperation(m_dragState.previewOperation),
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
}
|
||||
}
|
||||
|
||||
DragDropValidationScene::Geometry DragDropValidationScene::BuildGeometry(
|
||||
const UIRect& viewportRect) const {
|
||||
Geometry geometry = {};
|
||||
const float availableWidth = (std::max)(720.0f, viewportRect.width - 48.0f);
|
||||
const float availableHeight = (std::max)(360.0f, viewportRect.height - 256.0f);
|
||||
geometry.labRect = UIRect(
|
||||
24.0f,
|
||||
220.0f,
|
||||
(std::min)(980.0f, availableWidth),
|
||||
(std::min)(460.0f, availableHeight));
|
||||
geometry.statusRect = UIRect(
|
||||
geometry.labRect.x + 20.0f,
|
||||
geometry.labRect.y + 18.0f,
|
||||
geometry.labRect.width - 40.0f,
|
||||
84.0f);
|
||||
geometry.sourcesRect = UIRect(
|
||||
geometry.labRect.x + 20.0f,
|
||||
geometry.statusRect.y + geometry.statusRect.height + 18.0f,
|
||||
280.0f,
|
||||
geometry.labRect.height - 140.0f);
|
||||
geometry.targetsRect = UIRect(
|
||||
geometry.sourcesRect.x + geometry.sourcesRect.width + 18.0f,
|
||||
geometry.sourcesRect.y,
|
||||
geometry.labRect.width - 338.0f,
|
||||
geometry.sourcesRect.height);
|
||||
geometry.textureSourceRect = UIRect(
|
||||
geometry.sourcesRect.x + 14.0f,
|
||||
geometry.sourcesRect.y + 54.0f,
|
||||
geometry.sourcesRect.width - 28.0f,
|
||||
96.0f);
|
||||
geometry.entitySourceRect = UIRect(
|
||||
geometry.textureSourceRect.x,
|
||||
geometry.textureSourceRect.y + geometry.textureSourceRect.height + 16.0f,
|
||||
geometry.textureSourceRect.width,
|
||||
96.0f);
|
||||
geometry.projectTargetRect = UIRect(
|
||||
geometry.targetsRect.x + 14.0f,
|
||||
geometry.targetsRect.y + 54.0f,
|
||||
geometry.targetsRect.width - 28.0f,
|
||||
112.0f);
|
||||
geometry.hierarchyTargetRect = UIRect(
|
||||
geometry.projectTargetRect.x,
|
||||
geometry.projectTargetRect.y + geometry.projectTargetRect.height + 18.0f,
|
||||
geometry.projectTargetRect.width,
|
||||
112.0f);
|
||||
return geometry;
|
||||
}
|
||||
|
||||
void DragDropValidationScene::HandlePointerDown(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
if (RectContains(geometry.textureSourceRect, position)) {
|
||||
BeginUIDragDrop(BuildTextureSource(position), m_dragState);
|
||||
SetResult("Result: armed Texture Asset, move beyond threshold to activate");
|
||||
return;
|
||||
}
|
||||
if (RectContains(geometry.entitySourceRect, position)) {
|
||||
BeginUIDragDrop(BuildEntitySource(position), m_dragState);
|
||||
SetResult("Result: armed Scene Entity, move beyond threshold to activate");
|
||||
}
|
||||
}
|
||||
|
||||
void DragDropValidationScene::HandlePointerMove(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
if (!IsUIDragDropInProgress(m_dragState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIDragDropResult result = {};
|
||||
UpdateUIDragDropPointer(m_dragState, position, &result);
|
||||
if (result.activated) {
|
||||
SetResult("Result: drag became active after crossing activation distance");
|
||||
}
|
||||
UpdateHoveredTarget(geometry, position);
|
||||
}
|
||||
|
||||
void DragDropValidationScene::HandlePointerUp(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
if (!IsUIDragDropInProgress(m_dragState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateHoveredTarget(geometry, position);
|
||||
UIDragDropResult result = {};
|
||||
if (!EndUIDragDrop(m_dragState, result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.completed) {
|
||||
m_lastDropText =
|
||||
result.payloadItemId + " -> " + result.targetId + " (" + DescribeOperation(result.operation) + ")";
|
||||
SetResult("Result: drop completed on " + result.targetId + " with " + DescribeOperation(result.operation));
|
||||
return;
|
||||
}
|
||||
|
||||
SetResult("Result: pointer released without accepted target, drag cancelled");
|
||||
}
|
||||
|
||||
void DragDropValidationScene::HandleCancel(std::string reason) {
|
||||
if (!IsUIDragDropInProgress(m_dragState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIDragDropResult result = {};
|
||||
CancelUIDragDrop(m_dragState, &result);
|
||||
SetResult(std::move(reason));
|
||||
}
|
||||
|
||||
void DragDropValidationScene::UpdateHoveredTarget(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
if (!m_dragState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIDragDropResult result = {};
|
||||
if (RectContains(geometry.projectTargetRect, position)) {
|
||||
const UIDragDropTargetDescriptor projectTarget = BuildProjectTarget();
|
||||
UpdateUIDragDropTarget(m_dragState, &projectTarget, &result);
|
||||
} else if (RectContains(geometry.hierarchyTargetRect, position)) {
|
||||
const UIDragDropTargetDescriptor hierarchyTarget = BuildHierarchyTarget();
|
||||
UpdateUIDragDropTarget(m_dragState, &hierarchyTarget, &result);
|
||||
} else {
|
||||
UpdateUIDragDropTarget(m_dragState, nullptr, &result);
|
||||
}
|
||||
|
||||
if (result.targetChanged) {
|
||||
if (!HasResolvedUIDragDropTarget(m_dragState)) {
|
||||
SetResult("Result: current hover target rejects payload type or operation");
|
||||
} else {
|
||||
SetResult("Result: hover target accepts payload with " + DescribeOperation(m_dragState.previewOperation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DragDropValidationScene::SetResult(std::string text) {
|
||||
m_resultText = std::move(text);
|
||||
}
|
||||
|
||||
bool DragDropValidationScene::RectContains(
|
||||
const UIRect& rect,
|
||||
const UIPoint& position) {
|
||||
return position.x >= rect.x &&
|
||||
position.x <= rect.x + rect.width &&
|
||||
position.y >= rect.y &&
|
||||
position.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::Tests::CoreUI
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UIDragDropInteraction.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::Tests::CoreUI {
|
||||
|
||||
class DragDropValidationScene {
|
||||
public:
|
||||
static constexpr const char* ScenarioId = "core.input.drag_drop_basic";
|
||||
|
||||
void Reset();
|
||||
void Update(
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& events,
|
||||
const ::XCEngine::UI::UIRect& viewportRect,
|
||||
bool windowFocused);
|
||||
void AppendDrawData(
|
||||
::XCEngine::UI::UIDrawData& drawData,
|
||||
const ::XCEngine::UI::UIRect& viewportRect) const;
|
||||
|
||||
private:
|
||||
struct Geometry {
|
||||
::XCEngine::UI::UIRect labRect = {};
|
||||
::XCEngine::UI::UIRect statusRect = {};
|
||||
::XCEngine::UI::UIRect sourcesRect = {};
|
||||
::XCEngine::UI::UIRect targetsRect = {};
|
||||
::XCEngine::UI::UIRect textureSourceRect = {};
|
||||
::XCEngine::UI::UIRect entitySourceRect = {};
|
||||
::XCEngine::UI::UIRect projectTargetRect = {};
|
||||
::XCEngine::UI::UIRect hierarchyTargetRect = {};
|
||||
};
|
||||
|
||||
Geometry BuildGeometry(const ::XCEngine::UI::UIRect& viewportRect) const;
|
||||
void HandlePointerDown(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void HandlePointerMove(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void HandlePointerUp(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void HandleCancel(std::string reason);
|
||||
void UpdateHoveredTarget(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void SetResult(std::string text);
|
||||
static bool RectContains(
|
||||
const ::XCEngine::UI::UIRect& rect,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
|
||||
::XCEngine::UI::Widgets::UIDragDropState m_dragState = {};
|
||||
::XCEngine::UI::UIPoint m_pointerPosition = {};
|
||||
bool m_hasPointer = false;
|
||||
std::string m_resultText = "Result: Ready";
|
||||
std::string m_lastDropText = "(none)";
|
||||
};
|
||||
|
||||
} // namespace XCEngine::Tests::CoreUI
|
||||
@@ -4,6 +4,7 @@ set(CORE_UI_TEST_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_layout_engine.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_core.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_draw_data.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_drag_drop_interaction.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_expansion_model.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_flat_hierarchy_helpers.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_input_dispatcher.cpp
|
||||
|
||||
164
tests/UI/Core/unit/test_ui_drag_drop_interaction.cpp
Normal file
164
tests/UI/Core/unit/test_ui_drag_drop_interaction.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIDragDropInteraction.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::Widgets::BeginUIDragDrop;
|
||||
using XCEngine::UI::Widgets::CancelUIDragDrop;
|
||||
using XCEngine::UI::Widgets::DoesUIDragDropPayloadTypeMatch;
|
||||
using XCEngine::UI::Widgets::EndUIDragDrop;
|
||||
using XCEngine::UI::Widgets::HasResolvedUIDragDropTarget;
|
||||
using XCEngine::UI::Widgets::IsUIDragDropInProgress;
|
||||
using XCEngine::UI::Widgets::ResolveUIDragDropOperation;
|
||||
using XCEngine::UI::Widgets::UIDragDropOperation;
|
||||
using XCEngine::UI::Widgets::UIDragDropPayload;
|
||||
using XCEngine::UI::Widgets::UIDragDropResult;
|
||||
using XCEngine::UI::Widgets::UIDragDropSourceDescriptor;
|
||||
using XCEngine::UI::Widgets::UIDragDropState;
|
||||
using XCEngine::UI::Widgets::UIDragDropTargetDescriptor;
|
||||
using XCEngine::UI::Widgets::UpdateUIDragDropPointer;
|
||||
using XCEngine::UI::Widgets::UpdateUIDragDropTarget;
|
||||
|
||||
UIDragDropSourceDescriptor BuildSource() {
|
||||
UIDragDropSourceDescriptor descriptor = {};
|
||||
descriptor.ownerId = 101u;
|
||||
descriptor.sourceId = "project.asset.texture";
|
||||
descriptor.pointerDownPosition = UIPoint(24.0f, 32.0f);
|
||||
descriptor.payload = UIDragDropPayload{ "asset.texture", "tex-001", "Checker" };
|
||||
descriptor.allowedOperations =
|
||||
UIDragDropOperation::Copy |
|
||||
UIDragDropOperation::Move;
|
||||
descriptor.activationDistance = 5.0f;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
UIDragDropTargetDescriptor BuildTarget() {
|
||||
static constexpr std::array<std::string_view, 1> kAcceptedTypes = {
|
||||
"asset.texture"
|
||||
};
|
||||
|
||||
UIDragDropTargetDescriptor target = {};
|
||||
target.ownerId = 202u;
|
||||
target.targetId = "project.browser";
|
||||
target.acceptedPayloadTypes = kAcceptedTypes;
|
||||
target.acceptedOperations =
|
||||
UIDragDropOperation::Copy |
|
||||
UIDragDropOperation::Link;
|
||||
target.preferredOperation =
|
||||
UIDragDropOperation::Copy |
|
||||
UIDragDropOperation::Link;
|
||||
return target;
|
||||
}
|
||||
|
||||
void ActivateDrag(UIDragDropState& state) {
|
||||
ASSERT_TRUE(BeginUIDragDrop(BuildSource(), state));
|
||||
ASSERT_TRUE(UpdateUIDragDropPointer(state, UIPoint(40.0f, 48.0f)));
|
||||
ASSERT_TRUE(state.active);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIDragDropInteractionTest, PayloadTypeMatchAndOperationResolutionStayWithinSupportedSingleOperation) {
|
||||
constexpr std::array<std::string_view, 2> acceptedTypes = {
|
||||
"asset.texture",
|
||||
"asset.material"
|
||||
};
|
||||
|
||||
EXPECT_TRUE(DoesUIDragDropPayloadTypeMatch("asset.texture", acceptedTypes));
|
||||
EXPECT_FALSE(DoesUIDragDropPayloadTypeMatch("scene.entity", acceptedTypes));
|
||||
EXPECT_TRUE(DoesUIDragDropPayloadTypeMatch("anything", {}));
|
||||
|
||||
const UIDragDropTargetDescriptor target = BuildTarget();
|
||||
EXPECT_EQ(
|
||||
ResolveUIDragDropOperation(UIDragDropOperation::Copy, target),
|
||||
UIDragDropOperation::Copy);
|
||||
}
|
||||
|
||||
TEST(UIDragDropInteractionTest, PointerMustCrossActivationDistanceBeforeDragBecomesActive) {
|
||||
UIDragDropState state = {};
|
||||
ASSERT_TRUE(BeginUIDragDrop(BuildSource(), state));
|
||||
EXPECT_TRUE(IsUIDragDropInProgress(state));
|
||||
EXPECT_TRUE(state.armed);
|
||||
EXPECT_FALSE(state.active);
|
||||
|
||||
UIDragDropResult result = {};
|
||||
EXPECT_TRUE(UpdateUIDragDropPointer(state, UIPoint(27.0f, 35.0f), &result));
|
||||
EXPECT_FALSE(result.activated);
|
||||
EXPECT_FALSE(state.active);
|
||||
|
||||
EXPECT_TRUE(UpdateUIDragDropPointer(state, UIPoint(31.0f, 39.0f), &result));
|
||||
EXPECT_TRUE(result.activated);
|
||||
EXPECT_TRUE(state.active);
|
||||
}
|
||||
|
||||
TEST(UIDragDropInteractionTest, TargetUpdatesReturnConsistentAcceptedAndRejectedSnapshots) {
|
||||
UIDragDropState state = {};
|
||||
ActivateDrag(state);
|
||||
|
||||
const UIDragDropTargetDescriptor acceptedTarget = BuildTarget();
|
||||
UIDragDropResult result = {};
|
||||
EXPECT_TRUE(UpdateUIDragDropTarget(state, &acceptedTarget, &result));
|
||||
EXPECT_TRUE(result.targetChanged);
|
||||
EXPECT_EQ(result.targetId, "project.browser");
|
||||
EXPECT_EQ(result.operation, UIDragDropOperation::Copy);
|
||||
EXPECT_TRUE(HasResolvedUIDragDropTarget(state));
|
||||
|
||||
EXPECT_TRUE(UpdateUIDragDropTarget(state, &acceptedTarget, &result));
|
||||
EXPECT_FALSE(result.targetChanged);
|
||||
EXPECT_EQ(result.targetId, "project.browser");
|
||||
EXPECT_EQ(result.operation, UIDragDropOperation::Copy);
|
||||
|
||||
static constexpr std::array<std::string_view, 1> kRejectedTypes = {
|
||||
"scene.entity"
|
||||
};
|
||||
UIDragDropTargetDescriptor rejectedTarget = acceptedTarget;
|
||||
rejectedTarget.acceptedPayloadTypes = kRejectedTypes;
|
||||
|
||||
EXPECT_FALSE(UpdateUIDragDropTarget(state, &rejectedTarget, &result));
|
||||
EXPECT_TRUE(result.targetChanged);
|
||||
EXPECT_EQ(result.targetOwnerId, 0u);
|
||||
EXPECT_TRUE(result.targetId.empty());
|
||||
EXPECT_EQ(result.operation, UIDragDropOperation::None);
|
||||
EXPECT_FALSE(HasResolvedUIDragDropTarget(state));
|
||||
}
|
||||
|
||||
TEST(UIDragDropInteractionTest, ReleaseOverResolvedTargetCompletesAndResetsState) {
|
||||
UIDragDropState state = {};
|
||||
ActivateDrag(state);
|
||||
|
||||
const UIDragDropTargetDescriptor target = BuildTarget();
|
||||
ASSERT_TRUE(UpdateUIDragDropTarget(state, &target));
|
||||
|
||||
UIDragDropResult result = {};
|
||||
ASSERT_TRUE(EndUIDragDrop(state, result));
|
||||
EXPECT_TRUE(result.completed);
|
||||
EXPECT_FALSE(result.cancelled);
|
||||
EXPECT_EQ(result.sourceId, "project.asset.texture");
|
||||
EXPECT_EQ(result.targetId, "project.browser");
|
||||
EXPECT_EQ(result.payloadTypeId, "asset.texture");
|
||||
EXPECT_EQ(result.payloadItemId, "tex-001");
|
||||
EXPECT_EQ(result.operation, UIDragDropOperation::Copy);
|
||||
EXPECT_FALSE(IsUIDragDropInProgress(state));
|
||||
}
|
||||
|
||||
TEST(UIDragDropInteractionTest, CancelOrReleaseWithoutResolvedTargetCancelsAndResetsState) {
|
||||
UIDragDropState state = {};
|
||||
ActivateDrag(state);
|
||||
|
||||
UIDragDropResult result = {};
|
||||
ASSERT_TRUE(EndUIDragDrop(state, result));
|
||||
EXPECT_FALSE(result.completed);
|
||||
EXPECT_TRUE(result.cancelled);
|
||||
EXPECT_EQ(result.sourceId, "project.asset.texture");
|
||||
EXPECT_FALSE(IsUIDragDropInProgress(state));
|
||||
|
||||
ASSERT_TRUE(BeginUIDragDrop(BuildSource(), state));
|
||||
ASSERT_TRUE(CancelUIDragDrop(state, &result));
|
||||
EXPECT_TRUE(result.cancelled);
|
||||
EXPECT_EQ(result.sourceId, "project.asset.texture");
|
||||
EXPECT_FALSE(IsUIDragDropInProgress(state));
|
||||
}
|
||||
@@ -59,6 +59,23 @@ TEST(UISelectionModelTest, MultiSelectionTracksMembershipAndPrimarySelection) {
|
||||
EXPECT_FALSE(selection.SetPrimarySelection("missing"));
|
||||
}
|
||||
|
||||
TEST(UISelectionModelTest, ToggleSelectionMembershipAddsAndRemovesWithoutDroppingOthers) {
|
||||
UISelectionModel selection = {};
|
||||
|
||||
EXPECT_TRUE(selection.SetSelections({ "camera", "lights" }, "lights"));
|
||||
EXPECT_TRUE(selection.ToggleSelectionMembership("scene"));
|
||||
EXPECT_TRUE(selection.IsSelected("camera"));
|
||||
EXPECT_TRUE(selection.IsSelected("lights"));
|
||||
EXPECT_TRUE(selection.IsSelected("scene"));
|
||||
EXPECT_EQ(selection.GetSelectedId(), "scene");
|
||||
|
||||
EXPECT_TRUE(selection.ToggleSelectionMembership("lights"));
|
||||
EXPECT_TRUE(selection.IsSelected("camera"));
|
||||
EXPECT_FALSE(selection.IsSelected("lights"));
|
||||
EXPECT_TRUE(selection.IsSelected("scene"));
|
||||
EXPECT_EQ(selection.GetSelectedId(), "scene");
|
||||
}
|
||||
|
||||
TEST(UISelectionModelTest, SetSelectionsNormalizesDuplicatesAndKeepsRequestedPrimary) {
|
||||
UISelectionModel selection = {};
|
||||
|
||||
|
||||
@@ -89,7 +89,11 @@ TEST(UIEditorMenuPopupTest, HitTestIgnoresSeparatorsAndFallsBackToPopupSurface)
|
||||
EXPECT_EQ(itemHit.kind, UIEditorMenuPopupHitTargetKind::Item);
|
||||
EXPECT_EQ(itemHit.index, 0u);
|
||||
|
||||
const auto separatorHit = HitTestUIEditorMenuPopup(layout, items, UIPoint(130.0f, 88.0f));
|
||||
const UIRect separatorRect = layout.itemRects[1];
|
||||
const UIPoint separatorCenter(
|
||||
separatorRect.x + separatorRect.width * 0.5f,
|
||||
separatorRect.y + separatorRect.height * 0.5f);
|
||||
const auto separatorHit = HitTestUIEditorMenuPopup(layout, items, separatorCenter);
|
||||
EXPECT_EQ(separatorHit.kind, UIEditorMenuPopupHitTargetKind::PopupSurface);
|
||||
EXPECT_EQ(separatorHit.index, UIEditorMenuPopupInvalidIndex);
|
||||
|
||||
@@ -134,3 +138,34 @@ TEST(UIEditorMenuPopupTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
EXPECT_EQ(foregroundCommands[12].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(foregroundCommands[12].text, "Ctrl+W");
|
||||
}
|
||||
|
||||
TEST(UIEditorMenuPopupTest, SubmenuIndicatorStaysInsideReservedRightEdgeSlot) {
|
||||
const auto items = BuildItems();
|
||||
XCEngine::UI::Editor::Widgets::UIEditorMenuPopupMetrics metrics = {};
|
||||
const auto layout = BuildUIEditorMenuPopupLayout(
|
||||
UIRect(100.0f, 50.0f, 220.0f, 118.0f),
|
||||
items,
|
||||
metrics);
|
||||
|
||||
UIDrawList foreground("MenuPopupForeground");
|
||||
AppendUIEditorMenuPopupForeground(foreground, layout, items, {}, {}, metrics);
|
||||
|
||||
const auto& commands = foreground.GetCommands();
|
||||
auto submenuIt = std::find_if(
|
||||
commands.begin(),
|
||||
commands.end(),
|
||||
[](const auto& command) {
|
||||
return command.type == UIDrawCommandType::Text && command.text == ">";
|
||||
});
|
||||
ASSERT_NE(submenuIt, commands.end());
|
||||
|
||||
const UIRect& submenuRect = layout.itemRects[2];
|
||||
const float expectedLeft =
|
||||
submenuRect.x + submenuRect.width -
|
||||
metrics.shortcutInsetRight -
|
||||
metrics.submenuIndicatorWidth;
|
||||
EXPECT_FLOAT_EQ(submenuIt->position.x, expectedLeft);
|
||||
EXPECT_LE(
|
||||
submenuIt->position.x + metrics.estimatedGlyphWidth,
|
||||
submenuRect.x + submenuRect.width - metrics.shortcutInsetRight + 0.001f);
|
||||
}
|
||||
|
||||
@@ -149,15 +149,16 @@ TEST(UIEditorTabStripTest, BackgroundAndForegroundEmitStableChromeCommands) {
|
||||
|
||||
UIDrawList background("TabStripBackground");
|
||||
AppendUIEditorTabStripBackground(background, layout, state);
|
||||
ASSERT_EQ(background.GetCommandCount(), 9u);
|
||||
ASSERT_EQ(background.GetCommandCount(), 8u);
|
||||
const auto& backgroundCommands = background.GetCommands();
|
||||
EXPECT_EQ(backgroundCommands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[3].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[4].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[5].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[1].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[2].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[3].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[4].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[5].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[6].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[7].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(backgroundCommands[8].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(backgroundCommands[7].type, UIDrawCommandType::RectOutline);
|
||||
|
||||
UIDrawList foreground("TabStripForeground");
|
||||
AppendUIEditorTabStripForeground(foreground, layout, items, state);
|
||||
@@ -198,13 +199,12 @@ TEST(UIEditorTabStripTest, ForegroundCentersTabLabelsWithinHeaderContentArea) {
|
||||
ASSERT_EQ(commands[3].type, UIDrawCommandType::Text);
|
||||
ASSERT_EQ(commands[6].type, UIDrawCommandType::Text);
|
||||
|
||||
const float padding = 8.0f;
|
||||
const float firstExpectedX =
|
||||
layout.tabHeaderRects[0].x + padding +
|
||||
((layout.tabHeaderRects[0].width - padding * 2.0f) - items[0].desiredHeaderLabelWidth) * 0.5f;
|
||||
layout.tabHeaderRects[0].x +
|
||||
std::floor((layout.tabHeaderRects[0].width - items[0].desiredHeaderLabelWidth) * 0.5f);
|
||||
const float secondExpectedX =
|
||||
layout.tabHeaderRects[1].x + padding +
|
||||
((layout.tabHeaderRects[1].width - padding * 2.0f) - items[1].desiredHeaderLabelWidth) * 0.5f;
|
||||
layout.tabHeaderRects[1].x +
|
||||
std::floor((layout.tabHeaderRects[1].width - items[1].desiredHeaderLabelWidth) * 0.5f);
|
||||
|
||||
EXPECT_FLOAT_EQ(commands[3].position.x, firstExpectedX);
|
||||
EXPECT_FLOAT_EQ(commands[6].position.x, secondExpectedX);
|
||||
|
||||
@@ -39,6 +39,16 @@ bool ContainsTextCommand(const UIDrawList& drawList, std::string_view text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ContainsCommandType(const UIDrawList& drawList, UIDrawCommandType type) {
|
||||
for (const auto& command : drawList.GetCommands()) {
|
||||
if (command.type == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<UIEditorTreeViewItem> BuildTreeItems() {
|
||||
return {
|
||||
{ "scene", "Scene", 0u, false, 0.0f },
|
||||
@@ -178,10 +188,38 @@ TEST(UIEditorTreeViewTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
AppendUIEditorTreeViewForeground(foreground, layout, items);
|
||||
ASSERT_EQ(foreground.GetCommandCount(), 20u);
|
||||
EXPECT_EQ(foreground.GetCommands()[0].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "v"));
|
||||
EXPECT_TRUE(ContainsCommandType(foreground, UIDrawCommandType::FilledTriangle));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Scene"));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Camera"));
|
||||
EXPECT_EQ(foreground.GetCommands()[19].type, UIDrawCommandType::PopClipRect);
|
||||
}
|
||||
|
||||
TEST(UIEditorTreeViewTest, LeadingIconAddsImageCommandAndReservesIconRect) {
|
||||
std::vector<UIEditorTreeViewItem> items = {
|
||||
{ "folder", "Assets", 0u, true, 0.0f, XCEngine::UI::UITextureHandle { 1u, 18u, 18u } }
|
||||
};
|
||||
UIExpansionModel expansionModel = {};
|
||||
|
||||
UIEditorTreeViewMetrics metrics = {};
|
||||
metrics.rowHeight = 20.0f;
|
||||
metrics.horizontalPadding = 6.0f;
|
||||
metrics.disclosureExtent = 18.0f;
|
||||
metrics.disclosureLabelGap = 2.0f;
|
||||
metrics.iconExtent = 18.0f;
|
||||
metrics.iconLabelGap = 2.0f;
|
||||
|
||||
const UIEditorTreeViewLayout layout =
|
||||
BuildUIEditorTreeViewLayout(UIRect(10.0f, 12.0f, 240.0f, 80.0f), items, expansionModel, metrics);
|
||||
|
||||
ASSERT_EQ(layout.iconRects.size(), 1u);
|
||||
EXPECT_FLOAT_EQ(layout.iconRects[0].x, 36.0f);
|
||||
EXPECT_FLOAT_EQ(layout.iconRects[0].y, 13.0f);
|
||||
EXPECT_FLOAT_EQ(layout.labelRects[0].x, 56.0f);
|
||||
|
||||
UIDrawList foreground("TreeViewForegroundWithIcon");
|
||||
AppendUIEditorTreeViewForeground(foreground, layout, items);
|
||||
EXPECT_TRUE(ContainsCommandType(foreground, UIDrawCommandType::Image));
|
||||
EXPECT_TRUE(ContainsTextCommand(foreground, "Assets"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# XCUI TEST_SPEC
|
||||
|
||||
日期: `2026-04-06`
|
||||
日期: `2026-04-09`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
@@ -44,8 +44,14 @@ tests/UI/
|
||||
- `Editor`
|
||||
- 面向编辑器 UI 的测试。
|
||||
- 例如 editor host、editor shell、editor-only widget、editor domain 集成。
|
||||
- 当前阶段采用固定代码样式:Editor 默认样式、palette、metrics 与视觉语义由代码层固定维护,不再为 Editor 主题解析单独扩测试主线。
|
||||
|
||||
当前阶段的资源化方向约束:
|
||||
- `Core` / `Runtime` 可以继续推进资源化、热重载与资源驱动验证。
|
||||
- `Editor` 当前不以主题资源解析作为主线,验证重点放在 editor-only 结构、状态机、交互和固定代码样式。
|
||||
|
||||
禁止把 `Runtime` 和 `Editor` 混在同一个测试目录里。
|
||||
当前正式验证入口固定为 `tests/UI`,不得把 `new_editor` 当作当前验证树的替代入口。
|
||||
|
||||
## 3. Unit 规范
|
||||
|
||||
@@ -72,6 +78,7 @@ tests/UI/
|
||||
- 每次只暴露当前批次需要检查的操作区域,不做大杂烩面板。
|
||||
- 界面中的操作提示默认使用中文,必要时可混用 `hover`、`focus`、`active`、`capture` 等术语。
|
||||
- 测哪一层,就把场景放到哪一层的 `integration/` 目录下。
|
||||
- `Editor` 集成验证当前只覆盖 editor-only 基础层;在 `Core / Editor` 基础层未收口前,不在 `new_editor` 中提前做业务面板验证。
|
||||
|
||||
`integration` 测试不负责:
|
||||
|
||||
@@ -165,12 +172,12 @@ Editor 集成测试只承载 editor-only 场景,不再承载共享 Core primit
|
||||
|
||||
- `F12` 手动截图。
|
||||
- 截图只允许截当前 exe 自己的渲染结果。
|
||||
- 截图输出到当前 scenario 自己的 `captures/` 目录。
|
||||
- 截图只允许输出到当前构建目录下、当前 exe 自己的 `Debug/captures/<scenario>/` 目录。
|
||||
|
||||
输出格式:
|
||||
|
||||
- `captures/latest.png`
|
||||
- `captures/history/<timestamp>_<index>_<reason>.png`
|
||||
- `build/.../Debug/captures/<scenario>/latest.png`
|
||||
- `build/.../Debug/captures/<scenario>/history/<timestamp>_<index>_<reason>.png`
|
||||
|
||||
原则:
|
||||
|
||||
@@ -185,15 +192,19 @@ XCUI 必须坚持自底向上的建设顺序:
|
||||
2. 先补对应 `unit`。
|
||||
3. 再补一个聚焦的 `integration` exe。
|
||||
4. 人工检查通过后再继续向上推进。
|
||||
5. 基础层稳定后,才允许把能力接入 `new_editor` 宿主做装配冒烟;不反向把 `new_editor` 当作验证入口。
|
||||
|
||||
禁止事项:
|
||||
|
||||
- 先堆 editor 具体面板,再回头补底层。
|
||||
- 把 `new_editor` 当作 XCUI 主测试入口。
|
||||
- 在基础层未完成前,把业务面板直接塞进 `new_editor` 作为主推进路径。
|
||||
- 把一个验证 exe 做成综合试验场。
|
||||
- 为了赶进度写跨层耦合的临时代码。
|
||||
|
||||
## 9. 当前入口约定
|
||||
|
||||
当前 XCUI 的正式验证入口是 `tests/UI`。
|
||||
`new_editor` 不是后续 XCUI 测试体系的主入口,也不应继续承载新的测试场景扩展。
|
||||
当前 XCUI 的正式验证入口是 `tests/UI`,其下的 `Core / Runtime / Editor` 三层测试树是唯一有效的当前验证体系。
|
||||
`new_editor` 只作为未来重建 editor 的产品宿主,不是当前测试入口,也不继续承载新的测试场景扩展。
|
||||
`tests/UI/TEST_SPEC.md` 负责顶层测试分层、职责边界和执行规则。
|
||||
`tests/UI/Editor/integration/README.md` 负责 Editor 集成验证的当前入口说明、场景清单、构建运行和截图约定。
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <XCEngine/Core/Asset/ProjectAssetIndex.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -350,12 +351,12 @@ TEST(AssetImportService_Test, RebuildLibraryCacheKeepsStableAssetRefs) {
|
||||
ASSERT_TRUE(firstAssetRef.IsValid());
|
||||
|
||||
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "ArtifactDB" / "artifacts.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "artifacts.db"));
|
||||
|
||||
EXPECT_TRUE(importService.RebuildLibraryCache());
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "ArtifactDB" / "artifacts.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "artifacts.db"));
|
||||
|
||||
AssetRef secondAssetRef;
|
||||
ASSERT_TRUE(importService.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, secondAssetRef));
|
||||
@@ -368,6 +369,61 @@ TEST(AssetImportService_Test, RebuildLibraryCacheKeepsStableAssetRefs) {
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(AssetImportService_Test, EnsureArtifactExposesContainerEntryRuntimeLoadPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
AssetImportService importService;
|
||||
importService.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_entry_runtime_path_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());
|
||||
|
||||
AssetImportService::ImportedAsset importedAsset;
|
||||
ASSERT_TRUE(importService.EnsureArtifact("Assets/runtime.material", ResourceType::Material, importedAsset));
|
||||
ASSERT_TRUE(importedAsset.exists);
|
||||
ASSERT_TRUE(importedAsset.artifactReady);
|
||||
EXPECT_EQ(importedAsset.artifactStorageKind, ArtifactStorageKind::SingleFileContainer);
|
||||
EXPECT_EQ(importedAsset.mainEntryName, "main");
|
||||
EXPECT_FALSE(importedAsset.artifactMainPath.Empty());
|
||||
EXPECT_FALSE(importedAsset.artifactMainEntryPath.Empty());
|
||||
EXPECT_EQ(importedAsset.runtimeLoadPath, importedAsset.artifactMainEntryPath);
|
||||
EXPECT_NE(importedAsset.artifactMainEntryPath, importedAsset.artifactMainPath);
|
||||
EXPECT_NE(std::string(importedAsset.runtimeLoadPath.CStr()).find("@entry=main"), std::string::npos);
|
||||
|
||||
{
|
||||
const fs::path artifactDbPath = fs::path(importService.GetLibraryRoot().CStr()) / "artifacts.db";
|
||||
std::ifstream artifactDbInput(artifactDbPath, std::ios::binary);
|
||||
ASSERT_TRUE(artifactDbInput.is_open());
|
||||
std::stringstream artifactDbBuffer;
|
||||
artifactDbBuffer << artifactDbInput.rdbuf();
|
||||
const std::string artifactDbText = artifactDbBuffer.str();
|
||||
EXPECT_NE(artifactDbText.find("# schema=2"), std::string::npos);
|
||||
EXPECT_NE(artifactDbText.find("storageKind\tmainEntryName"), std::string::npos);
|
||||
}
|
||||
|
||||
MaterialLoader loader;
|
||||
LoadResult loadResult = loader.Load(importedAsset.runtimeLoadPath);
|
||||
ASSERT_TRUE(loadResult);
|
||||
ASSERT_NE(loadResult.resource, nullptr);
|
||||
delete loadResult.resource;
|
||||
|
||||
importService.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ResourceManager_Test, RebuildProjectAssetCacheRefreshesLookupState) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -395,10 +451,10 @@ TEST(ResourceManager_Test, RebuildProjectAssetCacheRefreshesLookupState) {
|
||||
ASSERT_TRUE(firstAssetRef.IsValid());
|
||||
|
||||
const fs::path libraryRoot(manager.GetProjectLibraryRoot().CStr());
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
||||
|
||||
EXPECT_TRUE(manager.RebuildProjectAssetCache());
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "SourceAssetDB" / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
||||
|
||||
AssetRef secondAssetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, secondAssetRef));
|
||||
@@ -410,6 +466,45 @@ TEST(ResourceManager_Test, RebuildProjectAssetCacheRefreshesLookupState) {
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ResourceManager_Test, AssetImportServiceMigratesLegacyLibraryDatabasePathsToLibraryRoot) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
AssetImportService importService;
|
||||
importService.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_library_db_path_migration_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path legacySourceDbPath = projectRoot / "Library" / "SourceAssetDB" / "assets.db";
|
||||
const fs::path legacyArtifactDbPath = projectRoot / "Library" / "ArtifactDB" / "artifacts.db";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
fs::create_directories(legacySourceDbPath.parent_path());
|
||||
fs::create_directories(legacyArtifactDbPath.parent_path());
|
||||
|
||||
{
|
||||
std::ofstream sourceDbFile(legacySourceDbPath);
|
||||
ASSERT_TRUE(sourceDbFile.is_open());
|
||||
sourceDbFile << "# legacy source database\n";
|
||||
}
|
||||
{
|
||||
std::ofstream artifactDbFile(legacyArtifactDbPath);
|
||||
ASSERT_TRUE(artifactDbFile.is_open());
|
||||
artifactDbFile << "# legacy artifact database\n";
|
||||
}
|
||||
|
||||
importService.SetProjectRoot(projectRoot.string().c_str());
|
||||
|
||||
const fs::path libraryRoot(importService.GetLibraryRoot().CStr());
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(libraryRoot / "artifacts.db"));
|
||||
EXPECT_FALSE(fs::exists(legacySourceDbPath));
|
||||
EXPECT_FALSE(fs::exists(legacyArtifactDbPath));
|
||||
|
||||
importService.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ResourceManager_Test, SetResourceRootBootstrapsProjectAssetCache) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
|
||||
@@ -77,11 +77,19 @@ endif()
|
||||
|
||||
add_executable(editor_tests ${EDITOR_TEST_SOURCES})
|
||||
|
||||
add_executable(nahida_preview_regenerator
|
||||
nahida_preview_regenerator.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(editor_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
target_compile_options(editor_tests PRIVATE /FS /utf-8)
|
||||
target_compile_options(nahida_preview_regenerator PRIVATE /FS /utf-8)
|
||||
endif()
|
||||
|
||||
target_link_libraries(editor_tests PRIVATE
|
||||
@@ -92,6 +100,10 @@ target_link_libraries(editor_tests PRIVATE
|
||||
comdlg32
|
||||
)
|
||||
|
||||
target_link_libraries(nahida_preview_regenerator PRIVATE
|
||||
XCEngine
|
||||
)
|
||||
|
||||
target_include_directories(editor_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/editor/src
|
||||
@@ -100,6 +112,13 @@ target_include_directories(editor_tests PRIVATE
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
|
||||
)
|
||||
|
||||
target_include_directories(nahida_preview_regenerator PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/editor/src
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
|
||||
)
|
||||
|
||||
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_EDITOR_TEST_REPO_ROOT_CMAKE)
|
||||
file(TO_CMAKE_PATH "${XCENGINE_MONO_ROOT_DIR}" XCENGINE_EDITOR_TEST_MONO_ROOT_CMAKE)
|
||||
|
||||
@@ -108,6 +127,22 @@ target_compile_definitions(editor_tests PRIVATE
|
||||
XCENGINE_EDITOR_MONO_ROOT_DIR="${XCENGINE_EDITOR_TEST_MONO_ROOT_CMAKE}"
|
||||
)
|
||||
|
||||
target_compile_definitions(nahida_preview_regenerator PRIVATE
|
||||
XCENGINE_EDITOR_REPO_ROOT="${XCENGINE_EDITOR_TEST_REPO_ROOT_CMAKE}"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET editor_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
|
||||
$<TARGET_FILE_DIR:editor_tests>/assimp-vc143-mt.dll
|
||||
)
|
||||
|
||||
add_custom_command(TARGET nahida_preview_regenerator POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
|
||||
$<TARGET_FILE_DIR:nahida_preview_regenerator>/assimp-vc143-mt.dll
|
||||
)
|
||||
|
||||
if(XCENGINE_ENABLE_MONO_SCRIPTING AND TARGET xcengine_managed_assemblies)
|
||||
add_dependencies(editor_tests xcengine_managed_assemblies)
|
||||
|
||||
|
||||
193
tests/editor/nahida_preview_regenerator.cpp
Normal file
193
tests/editor/nahida_preview_regenerator.cpp
Normal file
@@ -0,0 +1,193 @@
|
||||
#include "Commands/ProjectCommands.h"
|
||||
#include "Core/EditorContext.h"
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Rect.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kDefaultModelAssetPath = "Assets/Models/nahida/Avatar_Loli_Catalyst_Nahida.fbx";
|
||||
constexpr const char* kDefaultSceneAssetPath = "Assets/Scenes/NahidaPreview.xc";
|
||||
constexpr const char* kDefaultSceneName = "Nahida Preview";
|
||||
|
||||
std::shared_ptr<XCEngine::Editor::AssetItem> MakeModelAssetItem(
|
||||
const fs::path& projectRoot,
|
||||
const std::string& modelAssetPath) {
|
||||
auto item = std::make_shared<XCEngine::Editor::AssetItem>();
|
||||
item->name = fs::path(modelAssetPath).filename().string();
|
||||
item->type = "Model";
|
||||
item->isFolder = false;
|
||||
item->fullPath = (projectRoot / fs::path(modelAssetPath)).string();
|
||||
return item;
|
||||
}
|
||||
|
||||
void ConfigurePreviewCamera(XCEngine::Components::GameObject& gameObject) {
|
||||
using namespace XCEngine;
|
||||
|
||||
gameObject.GetTransform()->SetLocalPosition(Math::Vector3(0.0f, 1.2f, -4.25f));
|
||||
gameObject.GetTransform()->SetLocalRotation(Math::Quaternion(0.104528f, 0.0f, 0.0f, 0.994522f));
|
||||
|
||||
auto* camera = gameObject.AddComponent<Components::CameraComponent>();
|
||||
camera->SetProjectionType(Components::CameraProjectionType::Perspective);
|
||||
camera->SetFieldOfView(35.0f);
|
||||
camera->SetNearClipPlane(0.01f);
|
||||
camera->SetFarClipPlane(100.0f);
|
||||
camera->SetDepth(0.0f);
|
||||
camera->SetPrimary(true);
|
||||
camera->SetClearMode(Components::CameraClearMode::Auto);
|
||||
camera->SetStackType(Components::CameraStackType::Base);
|
||||
camera->SetCullingMask(0xFFFFFFFFu);
|
||||
camera->SetViewportRect(Math::Rect(0.0f, 0.0f, 1.0f, 1.0f));
|
||||
camera->SetClearColor(Math::Color(0.04f, 0.05f, 0.07f, 1.0f));
|
||||
camera->SetSkyboxEnabled(false);
|
||||
camera->SetSkyboxTopColor(Math::Color(0.18f, 0.36f, 0.74f, 1.0f));
|
||||
camera->SetSkyboxHorizonColor(Math::Color(0.78f, 0.84f, 0.92f, 1.0f));
|
||||
camera->SetSkyboxBottomColor(Math::Color(0.92f, 0.93f, 0.95f, 1.0f));
|
||||
}
|
||||
|
||||
void ConfigureKeyLight(XCEngine::Components::GameObject& gameObject) {
|
||||
using namespace XCEngine;
|
||||
|
||||
gameObject.GetTransform()->SetLocalPosition(Math::Vector3(2.5f, 3.0f, -2.0f));
|
||||
gameObject.GetTransform()->SetLocalRotation(Math::Quaternion(0.21644f, -0.39404f, 0.09198f, 0.88755f));
|
||||
|
||||
auto* light = gameObject.AddComponent<Components::LightComponent>();
|
||||
light->SetLightType(Components::LightType::Directional);
|
||||
light->SetColor(Math::Color(1.0f, 0.976f, 0.94f, 1.0f));
|
||||
light->SetIntensity(1.4f);
|
||||
light->SetRange(10.0f);
|
||||
light->SetSpotAngle(30.0f);
|
||||
light->SetCastsShadows(true);
|
||||
}
|
||||
|
||||
void ConfigureFillLight(XCEngine::Components::GameObject& gameObject) {
|
||||
using namespace XCEngine;
|
||||
|
||||
gameObject.GetTransform()->SetLocalPosition(Math::Vector3(-1.75f, 1.5f, -1.25f));
|
||||
gameObject.GetTransform()->SetLocalRotation(Math::Quaternion::Identity());
|
||||
|
||||
auto* light = gameObject.AddComponent<Components::LightComponent>();
|
||||
light->SetLightType(Components::LightType::Point);
|
||||
light->SetColor(Math::Color(0.24f, 0.32f, 0.5f, 1.0f));
|
||||
light->SetIntensity(0.35f);
|
||||
light->SetRange(10.0f);
|
||||
light->SetSpotAngle(30.0f);
|
||||
light->SetCastsShadows(false);
|
||||
}
|
||||
|
||||
void ConfigureGround(XCEngine::Components::GameObject& gameObject) {
|
||||
using namespace XCEngine;
|
||||
|
||||
gameObject.GetTransform()->SetLocalPosition(Math::Vector3::Zero());
|
||||
gameObject.GetTransform()->SetLocalRotation(Math::Quaternion::Identity());
|
||||
gameObject.GetTransform()->SetLocalScale(Math::Vector3(8.0f, 1.0f, 8.0f));
|
||||
|
||||
auto* meshFilter = gameObject.AddComponent<Components::MeshFilterComponent>();
|
||||
meshFilter->SetMeshPath("builtin://meshes/plane");
|
||||
|
||||
auto* meshRenderer = gameObject.AddComponent<Components::MeshRendererComponent>();
|
||||
meshRenderer->SetMaterialPath(0, "builtin://materials/default-primitive");
|
||||
meshRenderer->SetCastShadows(true);
|
||||
meshRenderer->SetReceiveShadows(true);
|
||||
meshRenderer->SetRenderLayer(0);
|
||||
}
|
||||
|
||||
std::string ParseArgValue(int argc, char** argv, const char* name, const char* fallback) {
|
||||
const std::string optionPrefix = std::string(name) + "=";
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string argument = argv[index];
|
||||
if (argument.rfind(optionPrefix, 0) == 0) {
|
||||
return argument.substr(optionPrefix.size());
|
||||
}
|
||||
}
|
||||
|
||||
return fallback != nullptr ? std::string(fallback) : std::string();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
using namespace XCEngine;
|
||||
|
||||
const fs::path repoRoot(XCENGINE_EDITOR_REPO_ROOT);
|
||||
const std::string projectArg = ParseArgValue(argc, argv, "--project-root", nullptr);
|
||||
const fs::path projectRoot = projectArg.empty() ? (repoRoot / "project") : fs::path(projectArg);
|
||||
const std::string modelAssetPath =
|
||||
ParseArgValue(argc, argv, "--model-asset", kDefaultModelAssetPath);
|
||||
const std::string sceneAssetPath =
|
||||
ParseArgValue(argc, argv, "--scene-asset", kDefaultSceneAssetPath);
|
||||
const fs::path sceneFilePath = projectRoot / fs::path(sceneAssetPath);
|
||||
|
||||
if (!fs::exists(projectRoot) || !fs::is_directory(projectRoot)) {
|
||||
std::cerr << "Project root does not exist: " << projectRoot << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
Editor::EditorContext context;
|
||||
context.SetProjectPath(projectRoot.string());
|
||||
context.GetProjectManager().Initialize(projectRoot.string());
|
||||
context.GetSceneManager().NewScene(kDefaultSceneName);
|
||||
|
||||
auto& resourceManager = Resources::ResourceManager::Get();
|
||||
resourceManager.Initialize();
|
||||
resourceManager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
auto* camera = context.GetSceneManager().CreateEntity("Preview Camera");
|
||||
auto* keyLight = context.GetSceneManager().CreateEntity("Key Light");
|
||||
auto* fillLight = context.GetSceneManager().CreateEntity("Fill Light");
|
||||
auto* ground = context.GetSceneManager().CreateEntity("Ground");
|
||||
auto* avatarRoot = context.GetSceneManager().CreateEntity("AvatarRoot");
|
||||
if (camera == nullptr || keyLight == nullptr || fillLight == nullptr || ground == nullptr || avatarRoot == nullptr) {
|
||||
std::cerr << "Failed to create preview scene entities." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
ConfigurePreviewCamera(*camera);
|
||||
ConfigureKeyLight(*keyLight);
|
||||
ConfigureFillLight(*fillLight);
|
||||
ConfigureGround(*ground);
|
||||
avatarRoot->GetTransform()->SetLocalPosition(Math::Vector3::Zero());
|
||||
avatarRoot->GetTransform()->SetLocalRotation(Math::Quaternion::Identity());
|
||||
avatarRoot->GetTransform()->SetLocalScale(Math::Vector3::One());
|
||||
|
||||
const auto modelItem = MakeModelAssetItem(projectRoot, modelAssetPath);
|
||||
auto* createdRoot = Editor::Commands::InstantiateModelAsset(
|
||||
context,
|
||||
modelItem,
|
||||
avatarRoot,
|
||||
"Instantiate Nahida Preview Model");
|
||||
if (createdRoot == nullptr) {
|
||||
std::cerr << "Failed to instantiate model asset: " << modelAssetPath << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
createdRoot->SetName("NahidaUnityModel");
|
||||
|
||||
if (!context.GetSceneManager().SaveSceneAs(sceneFilePath.string())) {
|
||||
std::cerr << "Failed to save scene: " << sceneFilePath << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
resourceManager.UnloadAll();
|
||||
resourceManager.SetResourceRoot("");
|
||||
resourceManager.Shutdown();
|
||||
|
||||
std::cout << "Generated scene: " << sceneFilePath << std::endl;
|
||||
std::cout << "Model asset: " << modelAssetPath << std::endl;
|
||||
return 0;
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "Actions/MainMenuActionRouter.h"
|
||||
#include "Actions/ProjectActionRouter.h"
|
||||
#include "Commands/EntityCommands.h"
|
||||
#include "Commands/ProjectCommands.h"
|
||||
#include "Commands/SceneCommands.h"
|
||||
#include "Core/EditorContext.h"
|
||||
#include "Core/PlaySessionController.h"
|
||||
@@ -15,6 +16,7 @@
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Resources/BuiltinResources.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
@@ -22,11 +24,41 @@
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace XCEngine::Editor {
|
||||
namespace {
|
||||
|
||||
fs::path GetRepositoryRoot() {
|
||||
return fs::path(XCENGINE_EDITOR_REPO_ROOT);
|
||||
}
|
||||
|
||||
std::string GetMeshFixturePath(const char* fileName) {
|
||||
return (GetRepositoryRoot() / "tests" / "Fixtures" / "Resources" / "Mesh" / fileName).string();
|
||||
}
|
||||
|
||||
void CopyTexturedTriangleFixture(const fs::path& assetsDir) {
|
||||
fs::copy_file(
|
||||
GetMeshFixturePath("textured_triangle.obj"),
|
||||
assetsDir / "textured_triangle.obj",
|
||||
fs::copy_options::overwrite_existing);
|
||||
fs::copy_file(
|
||||
GetMeshFixturePath("textured_triangle.mtl"),
|
||||
assetsDir / "textured_triangle.mtl",
|
||||
fs::copy_options::overwrite_existing);
|
||||
fs::copy_file(
|
||||
GetMeshFixturePath("checker.bmp"),
|
||||
assetsDir / "checker.bmp",
|
||||
fs::copy_options::overwrite_existing);
|
||||
}
|
||||
|
||||
bool DirectoryHasEntries(const fs::path& directoryPath) {
|
||||
std::error_code ec;
|
||||
if (!fs::exists(directoryPath, ec) || !fs::is_directory(directoryPath, ec)) {
|
||||
@@ -36,6 +68,18 @@ bool DirectoryHasEntries(const fs::path& directoryPath) {
|
||||
return fs::directory_iterator(directoryPath) != fs::directory_iterator();
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
struct AssimpDllGuard {
|
||||
HMODULE module = nullptr;
|
||||
|
||||
~AssimpDllGuard() {
|
||||
if (module != nullptr) {
|
||||
FreeLibrary(module);
|
||||
}
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
class EditorActionRoutingTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
@@ -98,6 +142,26 @@ protected:
|
||||
return total;
|
||||
}
|
||||
|
||||
template <typename ComponentType>
|
||||
static ::XCEngine::Components::GameObject* FindFirstEntityWithComponent(
|
||||
::XCEngine::Components::GameObject* gameObject) {
|
||||
if (gameObject == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (gameObject->GetComponent<ComponentType>() != nullptr) {
|
||||
return gameObject;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < gameObject->GetChildCount(); ++i) {
|
||||
if (auto* found = FindFirstEntityWithComponent<ComponentType>(gameObject->GetChild(i))) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EditorContext m_context;
|
||||
fs::path m_projectRoot;
|
||||
};
|
||||
@@ -223,6 +287,138 @@ TEST_F(EditorActionRoutingTest, ProjectRouteExecutesOpenBackAndDelete) {
|
||||
EXPECT_FALSE(fs::exists(filePath));
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, ProjectCommandsInstantiateModelAssetBuildsHierarchyAndSupportsUndoRedo) {
|
||||
using ::XCEngine::Resources::ResourceManager;
|
||||
|
||||
const fs::path assetsDir = m_projectRoot / "Assets";
|
||||
CopyTexturedTriangleFixture(assetsDir);
|
||||
m_context.GetProjectManager().RefreshCurrentFolder();
|
||||
|
||||
const AssetItemPtr modelItem = FindCurrentItemByName("textured_triangle.obj");
|
||||
ASSERT_NE(modelItem, nullptr);
|
||||
EXPECT_EQ(modelItem->type, "Model");
|
||||
EXPECT_TRUE(Commands::CanInstantiateModelAsset(m_context, modelItem));
|
||||
|
||||
#ifdef _WIN32
|
||||
AssimpDllGuard dllGuard;
|
||||
const fs::path assimpDllPath =
|
||||
GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
||||
ASSERT_TRUE(fs::exists(assimpDllPath));
|
||||
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
||||
ASSERT_NE(dllGuard.module, nullptr);
|
||||
#endif
|
||||
|
||||
ResourceManager& resourceManager = ResourceManager::Get();
|
||||
resourceManager.Initialize();
|
||||
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
|
||||
|
||||
const size_t entityCountBeforeInstantiate = CountHierarchyEntities(m_context.GetSceneManager());
|
||||
auto* createdRoot = Commands::InstantiateModelAsset(m_context, modelItem);
|
||||
ASSERT_NE(createdRoot, nullptr);
|
||||
const uint64_t createdRootId = createdRoot->GetID();
|
||||
|
||||
const size_t entityCountAfterInstantiate = CountHierarchyEntities(m_context.GetSceneManager());
|
||||
EXPECT_GT(entityCountAfterInstantiate, entityCountBeforeInstantiate);
|
||||
EXPECT_EQ(m_context.GetSelectionManager().GetSelectedEntity(), createdRootId);
|
||||
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
|
||||
|
||||
auto* meshObject = FindFirstEntityWithComponent<::XCEngine::Components::MeshFilterComponent>(createdRoot);
|
||||
ASSERT_NE(meshObject, nullptr);
|
||||
auto* meshFilter = meshObject->GetComponent<::XCEngine::Components::MeshFilterComponent>();
|
||||
auto* meshRenderer = meshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
||||
ASSERT_NE(meshFilter, nullptr);
|
||||
ASSERT_NE(meshRenderer, nullptr);
|
||||
EXPECT_TRUE(meshFilter->GetMeshAssetRef().IsValid());
|
||||
ASSERT_FALSE(meshRenderer->GetMaterialAssetRefs().empty());
|
||||
EXPECT_TRUE(meshRenderer->GetMaterialAssetRefs()[0].IsValid());
|
||||
|
||||
m_context.GetUndoManager().Undo();
|
||||
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeInstantiate);
|
||||
EXPECT_EQ(m_context.GetSceneManager().GetEntity(createdRootId), nullptr);
|
||||
EXPECT_FALSE(m_context.GetSelectionManager().HasSelection());
|
||||
|
||||
m_context.GetUndoManager().Redo();
|
||||
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountAfterInstantiate);
|
||||
EXPECT_EQ(m_context.GetSelectionManager().GetSelectedEntity(), createdRootId);
|
||||
|
||||
auto* restoredRoot = m_context.GetSceneManager().GetEntity(createdRootId);
|
||||
ASSERT_NE(restoredRoot, nullptr);
|
||||
auto* restoredMeshObject =
|
||||
FindFirstEntityWithComponent<::XCEngine::Components::MeshFilterComponent>(restoredRoot);
|
||||
ASSERT_NE(restoredMeshObject, nullptr);
|
||||
auto* restoredMeshFilter =
|
||||
restoredMeshObject->GetComponent<::XCEngine::Components::MeshFilterComponent>();
|
||||
auto* restoredMeshRenderer =
|
||||
restoredMeshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
||||
ASSERT_NE(restoredMeshFilter, nullptr);
|
||||
ASSERT_NE(restoredMeshRenderer, nullptr);
|
||||
EXPECT_TRUE(restoredMeshFilter->GetMeshAssetRef().IsValid());
|
||||
ASSERT_FALSE(restoredMeshRenderer->GetMaterialAssetRefs().empty());
|
||||
EXPECT_TRUE(restoredMeshRenderer->GetMaterialAssetRefs()[0].IsValid());
|
||||
|
||||
resourceManager.UnloadAll();
|
||||
resourceManager.SetResourceRoot("");
|
||||
resourceManager.Shutdown();
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, ProjectCommandsInstantiateModelAssetAppliesSidecarMaterialOverrides) {
|
||||
using ::XCEngine::Resources::Model;
|
||||
using ::XCEngine::Resources::ResourceManager;
|
||||
using ::XCEngine::Resources::ResourceType;
|
||||
|
||||
const fs::path assetsDir = m_projectRoot / "Assets";
|
||||
CopyTexturedTriangleFixture(assetsDir);
|
||||
std::ofstream(assetsDir / "OverrideMaterial.mat")
|
||||
<< "{\n"
|
||||
" \"renderQueue\": \"geometry\"\n"
|
||||
"}\n";
|
||||
|
||||
#ifdef _WIN32
|
||||
AssimpDllGuard dllGuard;
|
||||
const fs::path assimpDllPath =
|
||||
GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
||||
ASSERT_TRUE(fs::exists(assimpDllPath));
|
||||
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
||||
ASSERT_NE(dllGuard.module, nullptr);
|
||||
#endif
|
||||
|
||||
ResourceManager& resourceManager = ResourceManager::Get();
|
||||
resourceManager.Initialize();
|
||||
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
|
||||
|
||||
const auto modelHandle = resourceManager.Load<Model>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(modelHandle.IsValid());
|
||||
ASSERT_FALSE(modelHandle->GetMeshBindings().Empty());
|
||||
|
||||
std::ofstream(assetsDir / "textured_triangle.obj.materialmap")
|
||||
<< modelHandle->GetMeshBindings()[0].meshLocalID
|
||||
<< "=Assets/OverrideMaterial.mat\n";
|
||||
|
||||
m_context.GetProjectManager().RefreshCurrentFolder();
|
||||
const AssetItemPtr modelItem = FindCurrentItemByName("textured_triangle.obj");
|
||||
ASSERT_NE(modelItem, nullptr);
|
||||
|
||||
auto* createdRoot = Commands::InstantiateModelAsset(m_context, modelItem);
|
||||
ASSERT_NE(createdRoot, nullptr);
|
||||
|
||||
auto* meshObject = FindFirstEntityWithComponent<::XCEngine::Components::MeshFilterComponent>(createdRoot);
|
||||
ASSERT_NE(meshObject, nullptr);
|
||||
auto* meshRenderer = meshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
||||
ASSERT_NE(meshRenderer, nullptr);
|
||||
|
||||
::XCEngine::Resources::AssetRef overrideMaterialRef;
|
||||
ASSERT_TRUE(resourceManager.TryGetAssetRef("Assets/OverrideMaterial.mat", ResourceType::Material, overrideMaterialRef));
|
||||
ASSERT_FALSE(meshRenderer->GetMaterialAssetRefs().empty());
|
||||
EXPECT_EQ(meshRenderer->GetMaterialAssetRefs()[0].assetGuid, overrideMaterialRef.assetGuid);
|
||||
EXPECT_EQ(meshRenderer->GetMaterialAssetRefs()[0].localID, overrideMaterialRef.localID);
|
||||
EXPECT_EQ(meshRenderer->GetMaterialAssetRefs()[0].resourceType, ResourceType::Material);
|
||||
EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Assets/OverrideMaterial.mat");
|
||||
|
||||
resourceManager.UnloadAll();
|
||||
resourceManager.SetResourceRoot("");
|
||||
resourceManager.Shutdown();
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, LoadSceneResetsSelectionAndUndoAfterFallbackSave) {
|
||||
auto* savedEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Saved", "SavedEntity");
|
||||
ASSERT_NE(savedEntity, nullptr);
|
||||
|
||||
Reference in New Issue
Block a user