tests: remove legacy test tree

This commit is contained in:
2026-04-22 00:22:32 +08:00
parent 8bfca5e8f2
commit bc47e6e5ac
754 changed files with 0 additions and 3517894 deletions

View File

@@ -1,100 +1 @@
cmake_minimum_required(VERSION 3.15)
project(XCEngineTests)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ============================================================
# Test Configuration
# ============================================================
option(ENABLE_COVERAGE "Enable code coverage" OFF)
option(ENABLE_BENCHMARK "Enable benchmark tests" OFF)
# ============================================================
# Dependencies
# ============================================================
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://gitee.com/mirrors/googletest.git
GIT_TAG v1.14.0
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
if(TARGET gtest AND NOT TARGET GTest::gtest)
add_library(GTest::gtest ALIAS gtest)
endif()
if(TARGET gtest_main AND NOT TARGET GTest::gtest_main)
add_library(GTest::gtest_main ALIAS gtest_main)
endif()
if(TARGET gmock AND NOT TARGET GTest::gmock)
add_library(GTest::gmock ALIAS gmock)
endif()
if(TARGET gmock_main AND NOT TARGET GTest::gmock_main)
add_library(GTest::gmock_main ALIAS gmock_main)
endif()
enable_testing()
# ============================================================
# Test Subdirectories
# ============================================================
add_subdirectory(Core)
add_subdirectory(UI)
add_subdirectory(Memory)
add_subdirectory(Threading)
add_subdirectory(Debug)
add_subdirectory(Components)
add_subdirectory(Physics)
add_subdirectory(Scene)
add_subdirectory(Scripting)
add_subdirectory(Rendering)
add_subdirectory(RHI)
add_subdirectory(Resources)
add_subdirectory(Input)
add_subdirectory(Editor)
if(WIN32)
find_program(XCENGINE_POWERSHELL_EXECUTABLE NAMES powershell pwsh REQUIRED)
add_custom_target(rendering_phase_regression_build
DEPENDS
rendering_all_tests
editor_tests
XCEditor
)
add_custom_target(rendering_phase_regression
DEPENDS
rendering_phase_regression_build
COMMAND "${XCENGINE_POWERSHELL_EXECUTABLE}"
-NoProfile
-ExecutionPolicy Bypass
-File "${CMAKE_SOURCE_DIR}/scripts/Run-RendererPhaseRegression.ps1"
-RepoRoot "${CMAKE_SOURCE_DIR}"
-BuildDir "${CMAKE_BINARY_DIR}"
-Config $<CONFIG>
-SkipBuild
USES_TERMINAL
COMMENT "Run renderer phase regression suite"
)
endif()
# ============================================================
# Test Summary
# ============================================================
add_custom_target(print_tests
COMMAND ${CMAKE_COMMAND} -E echo "===== XCEngine Test Suite ====="
COMMAND ${CMAKE_CTEST_COMMAND} -N
COMMENT "Available tests:"
)

View File

@@ -1,40 +0,0 @@
cmake_minimum_required(VERSION 3.15)
project(XCEngine_ComponentsTests)
set(COMPONENTS_TEST_SOURCES
test_component.cpp
test_audio_mixer.cpp
test_audio_system.cpp
test_windows_audio_backend.cpp
test_component_factory_registry.cpp
test_audio_source_component.cpp
test_transform_component.cpp
test_game_object.cpp
test_camera_light_component.cpp
test_physics_components.cpp
test_gaussian_splat_renderer_component.cpp
test_mesh_render_components.cpp
test_volume_renderer_component.cpp
)
add_executable(components_tests ${COMPONENTS_TEST_SOURCES})
if(MSVC)
set_target_properties(components_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
endif()
target_link_libraries(components_tests PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(components_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
)
include(GoogleTest)
gtest_discover_tests(components_tests)

View File

@@ -1,195 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Audio/AudioMixer.h>
#include <XCEngine/Audio/Equalizer.h>
#include <XCEngine/Audio/FFTFilter.h>
#include <XCEngine/Audio/IAudioEffect.h>
#include <XCEngine/Audio/Reverbation.h>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace XCEngine::Audio;
namespace {
class OffsetEffect final : public IAudioEffect {
public:
explicit OffsetEffect(float offset) : m_offset(offset) {}
void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels) override {
++callCount;
const size_t totalSamples = static_cast<size_t>(frameCount) * channels;
for (size_t i = 0; i < totalSamples; ++i) {
buffer[i] += m_offset;
}
}
int callCount = 0;
private:
float m_offset = 0.0f;
};
TEST(AudioMixer, AppliesMasterAndChannelVolume) {
AudioMixer mixer;
mixer.SetVolume(0.5f);
mixer.SetChannelVolume(AudioChannel::FrontLeft, 0.25f);
mixer.SetChannelVolume(AudioChannel::FrontRight, 1.0f);
float buffer[] = {1.0f, 1.0f, 1.0f, 1.0f};
mixer.ProcessAudio(buffer, 2, 2);
EXPECT_FLOAT_EQ(buffer[0], 0.125f);
EXPECT_FLOAT_EQ(buffer[1], 0.5f);
EXPECT_FLOAT_EQ(buffer[2], 0.125f);
EXPECT_FLOAT_EQ(buffer[3], 0.5f);
}
TEST(AudioMixer, MuteSilencesBuffer) {
AudioMixer mixer;
mixer.SetMute(true);
float buffer[] = {0.5f, -0.5f};
mixer.ProcessAudio(buffer, 1, 2);
EXPECT_FLOAT_EQ(buffer[0], 0.0f);
EXPECT_FLOAT_EQ(buffer[1], 0.0f);
}
TEST(AudioMixer, EffectsRunAfterGain) {
AudioMixer mixer;
mixer.SetVolume(0.5f);
OffsetEffect effect(0.25f);
mixer.AddEffect(&effect);
float buffer[] = {1.0f, -1.0f};
mixer.ProcessAudio(buffer, 1, 2);
EXPECT_EQ(effect.callCount, 1);
EXPECT_FLOAT_EQ(buffer[0], 0.75f);
EXPECT_FLOAT_EQ(buffer[1], -0.25f);
}
TEST(AudioMixer, EqualizerWetMixControlsProcessedSignal) {
AudioMixer mixer;
Equalizer equalizer;
equalizer.SetBandCount(1);
equalizer.SetBandFrequency(0, 2000.0f);
equalizer.SetBandGain(0, 12.0f);
equalizer.SetBandQ(0, 0.7f);
mixer.AddEffect(&equalizer);
std::vector<float> dryBuffer(64, 0.0f);
dryBuffer[0] = 1.0f;
equalizer.SetWetMix(0.0f);
mixer.ProcessAudio(dryBuffer.data(), 64, 1, 48000);
EXPECT_FLOAT_EQ(dryBuffer[0], 1.0f);
EXPECT_TRUE(std::all_of(
dryBuffer.begin() + 1,
dryBuffer.end(),
[](float sample) { return sample == 0.0f; }));
std::vector<float> wetBuffer(64, 0.0f);
wetBuffer[0] = 1.0f;
equalizer.ResetState();
equalizer.SetWetMix(1.0f);
mixer.ProcessAudio(wetBuffer.data(), 64, 1, 48000);
const bool changed = std::any_of(
wetBuffer.begin(),
wetBuffer.end(),
[](float sample) { return std::abs(sample) > 1e-4f && sample != 1.0f; }) ||
std::any_of(
wetBuffer.begin() + 1,
wetBuffer.end(),
[](float sample) { return std::abs(sample) > 1e-5f; });
EXPECT_TRUE(changed);
}
TEST(AudioMixer, ReverbationProducesAudibleTail) {
AudioMixer mixer;
Reverbation reverb;
reverb.SetWetMix(1.0f);
reverb.SetDryMix(0.0f);
reverb.SetRoomSize(1.0f);
reverb.SetDamping(0.0f);
mixer.AddEffect(&reverb);
std::vector<float> buffer(2000, 0.0f);
buffer[0] = 1.0f;
mixer.ProcessAudio(buffer.data(), 2000, 1, 48000);
EXPECT_FLOAT_EQ(buffer[0], 0.0f);
const bool hasTail = std::any_of(
buffer.begin() + 1000,
buffer.end(),
[](float sample) { return std::abs(sample) > 1e-6f; });
EXPECT_TRUE(hasTail);
}
TEST(AudioMixer, ReverbationWidthControlsStereoCrossfeed) {
AudioMixer narrowMixer;
Reverbation narrowReverb;
narrowReverb.SetWetMix(1.0f);
narrowReverb.SetDryMix(0.0f);
narrowReverb.SetRoomSize(1.0f);
narrowReverb.SetDamping(0.0f);
narrowReverb.SetWidth(0.0f);
narrowMixer.AddEffect(&narrowReverb);
std::vector<float> narrowBuffer(2000 * 2, 0.0f);
narrowBuffer[0] = 1.0f;
narrowMixer.ProcessAudio(narrowBuffer.data(), 2000, 2, 48000);
float narrowMaxDiff = 0.0f;
for (size_t i = 1100; i < 1150; ++i) {
narrowMaxDiff = std::max(
narrowMaxDiff,
std::abs(narrowBuffer[i * 2] - narrowBuffer[i * 2 + 1]));
}
EXPECT_LT(narrowMaxDiff, 1e-6f);
AudioMixer wideMixer;
Reverbation wideReverb;
wideReverb.SetWetMix(1.0f);
wideReverb.SetDryMix(0.0f);
wideReverb.SetRoomSize(1.0f);
wideReverb.SetDamping(0.0f);
wideReverb.SetWidth(1.0f);
wideMixer.AddEffect(&wideReverb);
std::vector<float> wideBuffer(2000 * 2, 0.0f);
wideBuffer[0] = 1.0f;
wideMixer.ProcessAudio(wideBuffer.data(), 2000, 2, 48000);
float wideMaxDiff = 0.0f;
for (size_t i = 1100; i < 1150; ++i) {
wideMaxDiff = std::max(
wideMaxDiff,
std::abs(wideBuffer[i * 2] - wideBuffer[i * 2 + 1]));
}
EXPECT_GT(wideMaxDiff, 1e-6f);
}
TEST(AudioMixer, FFTFilterAnalyzesWithoutMutatingBuffer) {
AudioMixer mixer;
FFTFilter fft(8);
fft.SetSmoothingFactor(0.0f);
mixer.AddEffect(&fft);
std::vector<float> buffer = {1.0f, 0.5f, 0.0f, -0.5f};
const std::vector<float> original = buffer;
mixer.ProcessAudio(buffer.data(), 4, 1, 48000);
EXPECT_EQ(buffer, original);
ASSERT_EQ(fft.GetSpectrumSize(), 4u);
const bool hasSpectrum = std::any_of(
fft.GetSpectrumData(),
fft.GetSpectrumData() + fft.GetSpectrumSize(),
[](float value) { return value > 0.0f; });
EXPECT_TRUE(hasSpectrum);
}
} // namespace

View File

@@ -1,512 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/AudioSourceComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <sstream>
using namespace XCEngine::Components;
using namespace XCEngine::Resources;
namespace {
AudioClip CreateMono16Clip(std::initializer_list<int16_t> samples, XCEngine::Core::uint32 sampleRate = 4) {
AudioClip clip;
XCEngine::Containers::Array<XCEngine::Core::uint8> pcmData;
pcmData.ResizeUninitialized(samples.size() * sizeof(int16_t));
size_t byteOffset = 0;
for (const int16_t sample : samples) {
const uint16_t encoded = static_cast<uint16_t>(sample);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>(encoded & 0xFFu);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>((encoded >> 8) & 0xFFu);
}
clip.SetSampleRate(sampleRate);
clip.SetChannels(1);
clip.SetBitsPerSample(16);
clip.SetAudioFormat(AudioFormat::WAV);
clip.SetPCMData(pcmData);
clip.m_isValid = true;
return clip;
}
AudioClip CreateStereo16Clip(std::initializer_list<int16_t> samples, XCEngine::Core::uint32 sampleRate = 4) {
AudioClip clip;
XCEngine::Containers::Array<XCEngine::Core::uint8> pcmData;
pcmData.ResizeUninitialized(samples.size() * sizeof(int16_t));
size_t byteOffset = 0;
for (const int16_t sample : samples) {
const uint16_t encoded = static_cast<uint16_t>(sample);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>(encoded & 0xFFu);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>((encoded >> 8) & 0xFFu);
}
clip.SetSampleRate(sampleRate);
clip.SetChannels(2);
clip.SetBitsPerSample(16);
clip.SetAudioFormat(AudioFormat::WAV);
clip.SetPCMData(pcmData);
clip.m_isValid = true;
return clip;
}
TEST(AudioSourceComponent, MonoClipMapsToStereoOutput) {
AudioClip clip = CreateMono16Clip({32767, -32768}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetClip(&clip);
source.Play();
float buffer[4] = {};
source.ProcessAudio(
buffer,
2,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(buffer[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(buffer[1], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(buffer[2], -1.0f, 1e-5f);
EXPECT_NEAR(buffer[3], -1.0f, 1e-5f);
EXPECT_FALSE(source.IsPlaying());
EXPECT_FLOAT_EQ(source.GetTime(), 0.0f);
}
TEST(AudioSourceComponent, PauseSilencesUntilResumed) {
AudioClip clip = CreateMono16Clip({32767, 16384}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetClip(&clip);
source.Play();
source.Pause();
float pausedBuffer[1] = {};
source.ProcessAudio(
pausedBuffer,
1,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_FLOAT_EQ(pausedBuffer[0], 0.0f);
source.Play();
float resumedBuffer[1] = {};
source.ProcessAudio(
resumedBuffer,
1,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(resumedBuffer[0], 32767.0f / 32768.0f, 1e-5f);
}
TEST(AudioSourceComponent, LoopingWrapsAtClipEnd) {
AudioClip clip = CreateMono16Clip({32767, -32768}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetLooping(true);
source.SetClip(&clip);
source.Play();
float buffer[4] = {};
source.ProcessAudio(
buffer,
4,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(buffer[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(buffer[1], -1.0f, 1e-5f);
EXPECT_NEAR(buffer[2], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(buffer[3], -1.0f, 1e-5f);
EXPECT_TRUE(source.IsPlaying());
}
TEST(AudioSourceComponent, PitchSupportsFractionalFramePlayback) {
AudioClip clip = CreateMono16Clip({0, 32767, 0}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetPitch(0.5f);
source.SetClip(&clip);
source.Play();
float buffer[3] = {};
source.ProcessAudio(
buffer,
3,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
const float peak = 32767.0f / 32768.0f;
EXPECT_NEAR(buffer[0], 0.0f, 1e-5f);
EXPECT_NEAR(buffer[1], peak * 0.5f, 1e-4f);
EXPECT_NEAR(buffer[2], peak, 1e-5f);
EXPECT_TRUE(source.IsPlaying());
EXPECT_NEAR(source.GetTime(), 0.375f, 1e-5f);
}
TEST(AudioSourceComponent, PanControlsStereoBalanceForMonoClip) {
AudioClip clip = CreateMono16Clip({32767}, 4);
AudioSourceComponent leftSource;
leftSource.SetSpatialize(false);
leftSource.SetPan(-1.0f);
leftSource.SetClip(&clip);
leftSource.Play();
float leftBuffer[2] = {};
leftSource.ProcessAudio(
leftBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(leftBuffer[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(leftBuffer[1], 0.0f, 1e-5f);
AudioSourceComponent rightSource;
rightSource.SetSpatialize(false);
rightSource.SetPan(1.0f);
rightSource.SetClip(&clip);
rightSource.Play();
float rightBuffer[2] = {};
rightSource.ProcessAudio(
rightBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(rightBuffer[0], 0.0f, 1e-5f);
EXPECT_NEAR(rightBuffer[1], 32767.0f / 32768.0f, 1e-5f);
}
TEST(AudioSourceComponent, DopplerAdjustsPlaybackRateFromListenerVelocity) {
AudioClip clip = CreateMono16Clip({0, 32767, 0, 0, 0}, 4);
GameObject sourceObject("AudioSourceObject");
sourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* source = sourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams params;
params.minDistance = 100.0f;
params.maxDistance = 100000.0f;
source->Set3DParams(params);
source->SetClip(&clip);
source->Play();
float buffer[2] = {};
source->ProcessAudio(
buffer,
2,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity(),
XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f),
1.0f,
20.0f,
4);
const float peak = 32767.0f / 32768.0f;
EXPECT_NEAR(buffer[0], 0.0f, 1e-5f);
EXPECT_NEAR(buffer[1], peak * 0.5f, 1e-3f);
EXPECT_TRUE(source->IsPlaying());
}
TEST(AudioSourceComponent, SourceVelocityAdjustsDopplerWhenMovingAway) {
AudioClip clip = CreateMono16Clip({0, 32767, 0}, 4);
GameObject sourceObject("MovingAudioSource");
sourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* source = sourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams params;
params.minDistance = 100.0f;
params.maxDistance = 100000.0f;
source->Set3DParams(params);
source->SetClip(&clip);
source->Play();
source->Update(0.0f);
sourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(20.0f, 0.0f, 0.0f));
source->Update(1.0f);
float buffer[2] = {};
source->ProcessAudio(
buffer,
2,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity(),
XCEngine::Math::Vector3::Zero(),
1.0f,
20.0f,
4);
const float peak = 32767.0f / 32768.0f;
EXPECT_NEAR(buffer[0], 0.0f, 1e-5f);
EXPECT_NEAR(buffer[1], peak * (20.0f / 30.0f), 1e-3f);
}
TEST(AudioSourceComponent, SpatialPanUsesListenerRotationAndPanLevel) {
AudioClip clip = CreateMono16Clip({32767}, 4);
GameObject rightSourceObject("RightSource");
rightSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* rightSource = rightSourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams params;
params.minDistance = 0.0f;
params.maxDistance = 100000.0f;
params.panLevel = 1.0f;
rightSource->Set3DParams(params);
rightSource->SetClip(&clip);
rightSource->Play();
float identityBuffer[2] = {};
rightSource->ProcessAudio(
identityBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_GT(identityBuffer[1], identityBuffer[0]);
GameObject rotatedSourceObject("RotatedRightSource");
rotatedSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* rotatedSource = rotatedSourceObject.AddComponent<AudioSourceComponent>();
rotatedSource->Set3DParams(params);
rotatedSource->SetClip(&clip);
rotatedSource->Play();
float rotatedBuffer[2] = {};
rotatedSource->ProcessAudio(
rotatedBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::FromEulerAngles(0.0f, XCEngine::Math::PI, 0.0f));
EXPECT_GT(rotatedBuffer[0], rotatedBuffer[1]);
GameObject neutralSourceObject("NeutralPanSource");
neutralSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* neutralSource = neutralSourceObject.AddComponent<AudioSourceComponent>();
params.panLevel = 0.0f;
neutralSource->Set3DParams(params);
neutralSource->SetClip(&clip);
neutralSource->Play();
float neutralBuffer[2] = {};
neutralSource->ProcessAudio(
neutralBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(neutralBuffer[0], neutralBuffer[1], 1e-5f);
}
TEST(AudioSourceComponent, SpreadControlsStereoWidthWhenSpatialized) {
AudioClip clip = CreateStereo16Clip({32767, 0}, 4);
GameObject narrowSourceObject("NarrowStereoSource");
narrowSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3::Zero());
auto* narrowSource = narrowSourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams narrowParams;
narrowParams.minDistance = 0.0f;
narrowParams.maxDistance = 100000.0f;
narrowParams.spread = 0.0f;
narrowSource->Set3DParams(narrowParams);
narrowSource->SetClip(&clip);
narrowSource->Play();
float narrowBuffer[2] = {};
narrowSource->ProcessAudio(
narrowBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
const float peak = 32767.0f / 32768.0f;
EXPECT_NEAR(narrowBuffer[0], peak * 0.5f, 1e-5f);
EXPECT_NEAR(narrowBuffer[1], peak * 0.5f, 1e-5f);
GameObject wideSourceObject("WideStereoSource");
wideSourceObject.GetTransform()->SetPosition(XCEngine::Math::Vector3::Zero());
auto* wideSource = wideSourceObject.AddComponent<AudioSourceComponent>();
XCEngine::Audio::Audio3DParams wideParams = narrowParams;
wideParams.spread = 1.0f;
wideSource->Set3DParams(wideParams);
wideSource->SetClip(&clip);
wideSource->Play();
float wideBuffer[2] = {};
wideSource->ProcessAudio(
wideBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(wideBuffer[0], peak, 1e-5f);
EXPECT_NEAR(wideBuffer[1], 0.0f, 1e-5f);
}
TEST(AudioSourceComponent, HRTFSpatializesMonoSourceOnStereoOutput) {
AudioClip clip = CreateMono16Clip({32767}, 4);
XCEngine::Audio::Audio3DParams params;
params.minDistance = 100.0f;
params.maxDistance = 100000.0f;
params.panLevel = 1.0f;
GameObject baselineObject("BaselineSpatialSource");
baselineObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* baselineSource = baselineObject.AddComponent<AudioSourceComponent>();
baselineSource->Set3DParams(params);
baselineSource->SetClip(&clip);
baselineSource->Play();
float baselineBuffer[2] = {};
baselineSource->ProcessAudio(
baselineBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_NEAR(baselineBuffer[0], 0.0f, 1e-5f);
EXPECT_GT(baselineBuffer[1], 0.0f);
GameObject hrtfObject("HRTFSpatialSource");
hrtfObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
auto* hrtfSource = hrtfObject.AddComponent<AudioSourceComponent>();
hrtfSource->Set3DParams(params);
hrtfSource->SetHRTFEnabled(true);
hrtfSource->SetHRTFCrossFeed(0.25f);
hrtfSource->SetClip(&clip);
hrtfSource->Play();
float hrtfBuffer[2] = {};
hrtfSource->ProcessAudio(
hrtfBuffer,
1,
2,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_GT(hrtfBuffer[0], baselineBuffer[0]);
EXPECT_GT(hrtfBuffer[0], 0.0f);
EXPECT_GT(hrtfBuffer[1], hrtfBuffer[0]);
}
TEST(AudioSourceComponent, MultipleSourcesCanReuseSameClipDecodedCache) {
AudioClip clip = CreateMono16Clip({32767, -32768}, 4);
const float* decodedBuffer = clip.GetDecodedPCMData().data();
AudioSourceComponent sourceA;
sourceA.SetSpatialize(false);
sourceA.SetClip(&clip);
sourceA.Play();
AudioSourceComponent sourceB;
sourceB.SetSpatialize(false);
sourceB.SetClip(&clip);
sourceB.Play();
float bufferA[2] = {};
float bufferB[2] = {};
sourceA.ProcessAudio(
bufferA,
2,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
sourceB.ProcessAudio(
bufferB,
2,
1,
XCEngine::Math::Vector3::Zero(),
XCEngine::Math::Quaternion::Identity());
EXPECT_EQ(decodedBuffer, clip.GetDecodedPCMData().data());
EXPECT_NEAR(bufferA[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(bufferA[1], -1.0f, 1e-5f);
EXPECT_NEAR(bufferB[0], 32767.0f / 32768.0f, 1e-5f);
EXPECT_NEAR(bufferB[1], -1.0f, 1e-5f);
}
TEST(AudioSourceComponent, SerializeRoundTripPreservesClipPathAndSpatialSettings) {
AudioClip clip = CreateMono16Clip({32767}, 4);
clip.m_path = "test://audio/runtime.wav";
clip.m_name = "runtime.wav";
AudioSourceComponent source;
source.SetClip(&clip);
source.SetVolume(0.75f);
source.SetPitch(1.5f);
source.SetPan(-0.25f);
source.SetLooping(true);
source.SetSpatialize(true);
source.SetHRTFEnabled(true);
source.SetHRTFCrossFeed(0.4f);
source.SetHRTFQuality(3);
XCEngine::Audio::Audio3DParams params;
params.dopplerLevel = 2.0f;
params.speedOfSound = 280.0f;
params.minDistance = 2.0f;
params.maxDistance = 64.0f;
params.panLevel = 0.6f;
params.spread = 0.3f;
params.reverbZoneMix = 0.2f;
source.Set3DParams(params);
std::stringstream stream;
source.Serialize(stream);
AudioSourceComponent target;
target.Deserialize(stream);
EXPECT_EQ(target.GetClip(), nullptr);
EXPECT_EQ(target.GetClipPath(), "test://audio/runtime.wav");
EXPECT_FLOAT_EQ(target.GetVolume(), 0.75f);
EXPECT_FLOAT_EQ(target.GetPitch(), 1.5f);
EXPECT_FLOAT_EQ(target.GetPan(), -0.25f);
EXPECT_TRUE(target.IsLooping());
EXPECT_TRUE(target.IsSpatialize());
EXPECT_TRUE(target.IsHRTFEnabled());
EXPECT_FLOAT_EQ(target.GetHRTFCrossFeed(), 0.4f);
EXPECT_EQ(target.GetHRTFQuality(), 3u);
EXPECT_FLOAT_EQ(target.GetDopplerLevel(), 2.0f);
const XCEngine::Audio::Audio3DParams restoredParams = target.Get3DParams();
EXPECT_FLOAT_EQ(restoredParams.speedOfSound, 280.0f);
EXPECT_FLOAT_EQ(restoredParams.minDistance, 2.0f);
EXPECT_FLOAT_EQ(restoredParams.maxDistance, 64.0f);
EXPECT_FLOAT_EQ(restoredParams.panLevel, 0.6f);
EXPECT_FLOAT_EQ(target.GetSpread(), 0.3f);
EXPECT_FLOAT_EQ(target.GetReverbZoneMix(), 0.2f);
}
} // namespace

View File

@@ -1,459 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Audio/AudioSystem.h>
#include <XCEngine/Audio/IAudioBackend.h>
#include <XCEngine/Audio/AudioMixer.h>
#include <XCEngine/Components/AudioListenerComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/AudioSourceComponent.h>
#include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Resources/AudioClip/AudioClip.h>
#include <sstream>
using namespace XCEngine::Audio;
using namespace XCEngine::Components;
using namespace XCEngine::Resources;
namespace {
AudioClip CreateMono16Clip(std::initializer_list<int16_t> samples, XCEngine::Core::uint32 sampleRate = 4) {
AudioClip clip;
XCEngine::Containers::Array<XCEngine::Core::uint8> pcmData;
pcmData.ResizeUninitialized(samples.size() * sizeof(int16_t));
size_t byteOffset = 0;
for (const int16_t sample : samples) {
const uint16_t encoded = static_cast<uint16_t>(sample);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>(encoded & 0xFFu);
pcmData[byteOffset++] = static_cast<XCEngine::Core::uint8>((encoded >> 8) & 0xFFu);
}
clip.SetSampleRate(sampleRate);
clip.SetChannels(1);
clip.SetBitsPerSample(16);
clip.SetAudioFormat(XCEngine::Resources::AudioFormat::WAV);
clip.SetPCMData(pcmData);
clip.m_isValid = true;
return clip;
}
class CaptureBackend final : public IAudioBackend {
public:
explicit CaptureBackend(const AudioConfig& inConfig) : config(inConfig) {}
bool Initialize(const AudioConfig& inConfig) override {
config = inConfig;
return true;
}
void Shutdown() override {}
std::string GetDeviceName() const override { return "CaptureBackend"; }
void GetAvailableDevices(std::vector<std::string>& devices) override {
devices = {"CaptureBackend"};
}
bool SetDevice(const std::string& deviceName) override { return deviceName == "CaptureBackend"; }
void Start() override { running = true; }
void Stop() override { running = false; }
void Suspend() override {}
void Resume() override {}
void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate) override {
lastFrameCount = frameCount;
lastChannels = channels;
lastSampleRate = sampleRate;
captured.assign(buffer, buffer + static_cast<size_t>(frameCount) * channels);
}
bool IsRunning() const override { return running; }
AudioConfig GetConfig() const override { return config; }
AudioConfig config{};
std::vector<float> captured;
bool running = true;
uint32 lastFrameCount = 0;
uint32 lastChannels = 0;
uint32 lastSampleRate = 0;
};
class PullCaptureBackend final : public IAudioBackend {
public:
explicit PullCaptureBackend(const AudioConfig& inConfig) : config(inConfig) {}
bool Initialize(const AudioConfig& inConfig) override {
config = inConfig;
return true;
}
void Shutdown() override {}
std::string GetDeviceName() const override { return "PullCaptureBackend"; }
void GetAvailableDevices(std::vector<std::string>& devices) override {
devices = {"PullCaptureBackend"};
}
bool SetDevice(const std::string& deviceName) override { return deviceName == "PullCaptureBackend"; }
void Start() override { running = true; }
void Stop() override { running = false; }
void Suspend() override {}
void Resume() override {}
void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate) override {
(void)buffer;
(void)frameCount;
(void)channels;
(void)sampleRate;
submissionCount++;
}
bool UsesPullModel() const override { return true; }
void SetRenderCallback(RenderCallback callback) override { renderCallback = callback; }
bool IsRunning() const override { return running; }
AudioConfig GetConfig() const override { return config; }
bool RenderOnce() {
if (!renderCallback) {
return false;
}
const size_t sampleCount = static_cast<size_t>(config.bufferSize) * config.channels;
captured.assign(sampleCount, 0.0f);
renderCallback(captured.data(), config.bufferSize, config.channels, config.sampleRate);
return true;
}
AudioConfig config{};
std::vector<float> captured;
RenderCallback renderCallback;
bool running = true;
uint32 submissionCount = 0;
};
TEST(AudioSystem, MasterMixerProcessesDirectSources) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
AudioConfig config;
config.sampleRate = 4;
config.channels = 1;
config.bufferSize = 1;
auto backend = std::make_unique<CaptureBackend>(config);
CaptureBackend* backendPtr = backend.get();
system.SetBackend(std::move(backend));
system.GetMasterMixer().SetVolume(0.5f);
system.GetMasterMixer().SetMute(false);
system.GetMasterMixer().ClearEffects();
AudioClip clip = CreateMono16Clip({16384}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetClip(&clip);
source.Play();
system.Update(0.0f);
ASSERT_EQ(backendPtr->captured.size(), 1u);
EXPECT_NEAR(backendPtr->captured[0], 0.25f, 1e-5f);
source.Stop();
system.GetMasterMixer().SetVolume(1.0f);
system.Shutdown();
}
TEST(AudioSystem, SourceOutputMixerRoutesIntoBackend) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
AudioConfig config;
config.sampleRate = 4;
config.channels = 1;
config.bufferSize = 1;
auto backend = std::make_unique<CaptureBackend>(config);
CaptureBackend* backendPtr = backend.get();
system.SetBackend(std::move(backend));
system.GetMasterMixer().SetVolume(1.0f);
system.GetMasterMixer().SetMute(false);
system.GetMasterMixer().ClearEffects();
AudioMixer mixer;
mixer.SetVolume(0.5f);
AudioClip clip = CreateMono16Clip({16384}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetOutputMixer(&mixer);
source.SetClip(&clip);
source.Play();
system.Update(0.0f);
ASSERT_EQ(backendPtr->captured.size(), 1u);
EXPECT_NEAR(backendPtr->captured[0], 0.25f, 1e-5f);
source.Stop();
system.Shutdown();
}
TEST(AudioSystem, SetMasterVolumeControlsMasterMixerOutput) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
AudioConfig config;
config.sampleRate = 4;
config.channels = 1;
config.bufferSize = 1;
auto backend = std::make_unique<CaptureBackend>(config);
CaptureBackend* backendPtr = backend.get();
system.SetBackend(std::move(backend));
system.SetMasterVolume(0.25f);
system.SetMuted(false);
system.GetMasterMixer().ClearEffects();
AudioClip clip = CreateMono16Clip({16384}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetClip(&clip);
source.Play();
system.Update(0.0f);
ASSERT_EQ(backendPtr->captured.size(), 1u);
EXPECT_NEAR(backendPtr->captured[0], 0.125f, 1e-5f);
EXPECT_FLOAT_EQ(system.GetMasterVolume(), 0.25f);
EXPECT_FALSE(system.IsMuted());
source.Stop();
system.SetMasterVolume(1.0f);
system.Shutdown();
}
TEST(AudioSystem, ListenerReverbMixerReceivesSend) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
AudioConfig config;
config.sampleRate = 4;
config.channels = 1;
config.bufferSize = 1;
auto backend = std::make_unique<CaptureBackend>(config);
CaptureBackend* backendPtr = backend.get();
system.SetBackend(std::move(backend));
system.SetMasterVolume(1.0f);
system.SetMuted(false);
system.GetMasterMixer().ClearEffects();
AudioMixer reverbMixer;
reverbMixer.SetVolume(1.0f);
system.SetListenerReverbMixer(&reverbMixer);
system.SetListenerReverbLevel(0.5f);
AudioClip clip = CreateMono16Clip({16384}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetReverbZoneMix(0.5f);
source.SetClip(&clip);
source.Play();
system.Update(0.0f);
ASSERT_EQ(backendPtr->captured.size(), 1u);
EXPECT_NEAR(backendPtr->captured[0], 0.625f, 1e-5f);
source.Stop();
system.SetListenerReverbMixer(nullptr);
system.SetListenerReverbLevel(1.0f);
system.Shutdown();
}
TEST(AudioSystem, DestroyingMixerClearsDependentRoutes) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
AudioSourceComponent source;
AudioListenerComponent listener;
auto parentMixer = std::make_unique<AudioMixer>();
auto childMixer = std::make_unique<AudioMixer>();
source.SetOutputMixer(parentMixer.get());
listener.SetReverb(parentMixer.get());
childMixer->SetOutputMixer(parentMixer.get());
parentMixer.reset();
EXPECT_EQ(source.GetOutputMixer(), nullptr);
EXPECT_EQ(listener.GetReverb(), nullptr);
EXPECT_EQ(system.GetListenerReverbMixer(), nullptr);
EXPECT_EQ(childMixer->GetOutputMixer(), nullptr);
system.Shutdown();
}
TEST(AudioSystem, ListenerComponentPublishesVelocityAndDopplerSettings) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
GameObject listenerObject("Listener");
auto* listener = listenerObject.AddComponent<AudioListenerComponent>();
listener->SetDopplerLevel(2.0f);
listener->SetSpeedOfSound(200.0f);
listenerObject.GetTransform()->SetPosition(XCEngine::Math::Vector3::Zero());
listener->Update(0.0f);
listenerObject.GetTransform()->SetPosition(XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
listener->Update(0.5f);
const XCEngine::Math::Vector3 listenerPosition = system.GetListenerPosition();
const XCEngine::Math::Vector3 listenerVelocity = system.GetListenerVelocity();
const XCEngine::Math::Quaternion listenerRotation = system.GetListenerRotation();
EXPECT_FLOAT_EQ(system.GetListenerDopplerLevel(), 2.0f);
EXPECT_FLOAT_EQ(system.GetSpeedOfSound(), 200.0f);
EXPECT_EQ(listenerPosition, XCEngine::Math::Vector3(10.0f, 0.0f, 0.0f));
EXPECT_EQ(listenerVelocity, XCEngine::Math::Vector3(20.0f, 0.0f, 0.0f));
EXPECT_FLOAT_EQ(listenerRotation.x, 0.0f);
EXPECT_FLOAT_EQ(listenerRotation.y, 0.0f);
EXPECT_FLOAT_EQ(listenerRotation.z, 0.0f);
EXPECT_FLOAT_EQ(listenerRotation.w, 1.0f);
system.Shutdown();
}
TEST(AudioSystem, ProcessAudioUsesBackendConfigSampleRateForDirectSubmission) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
AudioConfig config;
config.sampleRate = 22050;
config.channels = 1;
config.bufferSize = 1;
auto backend = std::make_unique<CaptureBackend>(config);
CaptureBackend* backendPtr = backend.get();
system.SetBackend(std::move(backend));
float buffer[1] = {0.5f};
system.ProcessAudio(buffer, 1, 1);
ASSERT_EQ(backendPtr->captured.size(), 1u);
EXPECT_FLOAT_EQ(backendPtr->captured[0], 0.5f);
EXPECT_EQ(backendPtr->lastSampleRate, 22050u);
system.Shutdown();
}
TEST(AudioSystem, RenderAudioProducesMixedBlockWithoutBackendSubmission) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
system.SetMasterVolume(0.5f);
system.SetMuted(false);
system.GetMasterMixer().ClearEffects();
AudioClip clip = CreateMono16Clip({16384}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetClip(&clip);
source.Play();
float buffer[1] = {};
system.RenderAudio(buffer, 1, 1, 4);
const AudioSystem::Stats stats = system.GetStats();
EXPECT_NEAR(buffer[0], 0.25f, 1e-5f);
EXPECT_EQ(stats.activeSources, 1u);
EXPECT_EQ(stats.totalSources, 1u);
source.Stop();
system.SetMasterVolume(1.0f);
system.Shutdown();
}
TEST(AudioSystem, PullBackendRendersViaCallbackWithoutPushSubmission) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
AudioConfig config;
config.sampleRate = 4;
config.channels = 1;
config.bufferSize = 1;
auto backend = std::make_unique<PullCaptureBackend>(config);
PullCaptureBackend* backendPtr = backend.get();
system.SetBackend(std::move(backend));
system.SetMasterVolume(0.5f);
system.SetMuted(false);
system.GetMasterMixer().ClearEffects();
AudioClip clip = CreateMono16Clip({16384}, 4);
AudioSourceComponent source;
source.SetSpatialize(false);
source.SetClip(&clip);
source.Play();
system.Update(0.0f);
EXPECT_EQ(backendPtr->submissionCount, 0u);
ASSERT_TRUE(backendPtr->RenderOnce());
ASSERT_EQ(backendPtr->captured.size(), 1u);
EXPECT_NEAR(backendPtr->captured[0], 0.25f, 1e-5f);
source.Stop();
system.SetMasterVolume(1.0f);
system.Shutdown();
}
TEST(AudioSystem, AudioListenerComponentSerializeRoundTripPreservesSettings) {
AudioSystem& system = AudioSystem::Get();
system.Shutdown();
AudioListenerComponent source;
source.SetMasterVolume(0.6f);
source.SetMute(true);
source.SetDopplerLevel(2.5f);
source.SetSpeedOfSound(250.0f);
source.SetReverbLevel(0.35f);
std::stringstream stream;
source.Serialize(stream);
AudioListenerComponent target;
target.Deserialize(stream);
EXPECT_FLOAT_EQ(target.GetMasterVolume(), 0.6f);
EXPECT_TRUE(target.IsMute());
EXPECT_FLOAT_EQ(target.GetDopplerLevel(), 2.5f);
EXPECT_FLOAT_EQ(target.GetSpeedOfSound(), 250.0f);
EXPECT_FLOAT_EQ(target.GetReverbLevel(), 0.35f);
EXPECT_FLOAT_EQ(system.GetMasterVolume(), 0.6f);
EXPECT_TRUE(system.IsMuted());
EXPECT_FLOAT_EQ(system.GetListenerDopplerLevel(), 2.5f);
EXPECT_FLOAT_EQ(system.GetSpeedOfSound(), 250.0f);
EXPECT_FLOAT_EQ(system.GetListenerReverbLevel(), 0.35f);
system.SetMasterVolume(1.0f);
system.SetMuted(false);
system.SetListenerDopplerLevel(1.0f);
system.SetSpeedOfSound(343.0f);
system.SetListenerReverbLevel(1.0f);
system.Shutdown();
}
TEST(AudioSystem, AudioListenerFrequencySnapshotDefaultsToEmpty) {
AudioListenerComponent listener;
const std::vector<float> frequencyData = listener.GetFrequencyData();
EXPECT_TRUE(frequencyData.empty());
EXPECT_EQ(listener.GetFrequencyDataSize(), 0u);
}
} // namespace

View File

@@ -1,254 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h>
#include <sstream>
using namespace XCEngine::Components;
namespace {
TEST(CameraComponent_Test, DefaultValues) {
CameraComponent camera;
EXPECT_EQ(camera.GetProjectionType(), CameraProjectionType::Perspective);
EXPECT_FLOAT_EQ(camera.GetFieldOfView(), 60.0f);
EXPECT_FLOAT_EQ(camera.GetOrthographicSize(), 5.0f);
EXPECT_FLOAT_EQ(camera.GetNearClipPlane(), 0.1f);
EXPECT_FLOAT_EQ(camera.GetFarClipPlane(), 1000.0f);
EXPECT_TRUE(camera.IsPrimary());
EXPECT_EQ(camera.GetClearMode(), CameraClearMode::Auto);
EXPECT_EQ(camera.GetStackType(), CameraStackType::Base);
EXPECT_EQ(camera.GetCullingMask(), 0xFFFFFFFFu);
EXPECT_FLOAT_EQ(camera.GetViewportRect().x, 0.0f);
EXPECT_FLOAT_EQ(camera.GetViewportRect().y, 0.0f);
EXPECT_FLOAT_EQ(camera.GetViewportRect().width, 1.0f);
EXPECT_FLOAT_EQ(camera.GetViewportRect().height, 1.0f);
EXPECT_FALSE(camera.IsSkyboxEnabled());
EXPECT_TRUE(camera.GetSkyboxMaterialPath().empty());
EXPECT_FALSE(camera.GetSkyboxMaterialAssetRef().IsValid());
EXPECT_FLOAT_EQ(camera.GetSkyboxTopColor().r, 0.18f);
EXPECT_FLOAT_EQ(camera.GetSkyboxHorizonColor().g, 0.84f);
EXPECT_FLOAT_EQ(camera.GetSkyboxBottomColor().b, 0.95f);
EXPECT_TRUE(camera.GetPostProcessPasses().empty());
EXPECT_FALSE(camera.GetFinalColorOverrides().HasOverrides());
EXPECT_FALSE(camera.IsColorScalePostProcessEnabled());
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessScale().x, 1.0f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessScale().y, 1.0f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessScale().z, 1.0f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessScale().w, 1.0f);
EXPECT_TRUE(camera.GetColorScalePostProcessPasses().empty());
}
TEST(CameraComponent_Test, SetterClamping) {
CameraComponent camera;
camera.SetFieldOfView(500.0f);
camera.SetOrthographicSize(-1.0f);
camera.SetNearClipPlane(-10.0f);
camera.SetFarClipPlane(0.0f);
EXPECT_FLOAT_EQ(camera.GetFieldOfView(), 179.0f);
EXPECT_FLOAT_EQ(camera.GetOrthographicSize(), 0.001f);
EXPECT_FLOAT_EQ(camera.GetNearClipPlane(), 0.001f);
EXPECT_GT(camera.GetFarClipPlane(), camera.GetNearClipPlane());
}
TEST(CameraComponent_Test, ViewportRectIsClampedToNormalizedSurfaceRange) {
CameraComponent camera;
camera.SetViewportRect(XCEngine::Math::Rect(-0.25f, 0.2f, 1.5f, 1.1f));
EXPECT_FLOAT_EQ(camera.GetViewportRect().x, 0.0f);
EXPECT_FLOAT_EQ(camera.GetViewportRect().y, 0.2f);
EXPECT_FLOAT_EQ(camera.GetViewportRect().width, 1.0f);
EXPECT_FLOAT_EQ(camera.GetViewportRect().height, 0.8f);
}
TEST(CameraComponent_Test, SerializeRoundTripPreservesViewportAndClearState) {
CameraComponent source;
source.SetClearMode(CameraClearMode::DepthOnly);
source.SetStackType(CameraStackType::Overlay);
source.SetCullingMask(0x0000000Fu);
source.SetViewportRect(XCEngine::Math::Rect(0.25f, 0.125f, 0.5f, 0.625f));
source.SetSkyboxEnabled(true);
source.SetSkyboxTopColor(XCEngine::Math::Color(0.12f, 0.21f, 0.64f, 1.0f));
source.SetSkyboxHorizonColor(XCEngine::Math::Color(0.71f, 0.76f, 0.88f, 1.0f));
source.SetSkyboxBottomColor(XCEngine::Math::Color(0.92f, 0.82f, 0.58f, 1.0f));
XCEngine::Rendering::FinalColorOverrideSettings finalColorOverrides = {};
finalColorOverrides.overrideOutputTransferMode = true;
finalColorOverrides.outputTransferMode =
XCEngine::Rendering::FinalColorOutputTransferMode::LinearToSRGB;
finalColorOverrides.overrideExposureMode = true;
finalColorOverrides.exposureMode = XCEngine::Rendering::FinalColorExposureMode::Fixed;
finalColorOverrides.overrideExposureValue = true;
finalColorOverrides.exposureValue = 1.35f;
finalColorOverrides.overrideFinalColorScale = true;
finalColorOverrides.finalColorScale = XCEngine::Math::Vector4(1.0f, 0.9f, 0.8f, 1.0f);
source.SetFinalColorOverrides(finalColorOverrides);
source.SetPostProcessPasses({
XCEngine::Rendering::FullscreenPassDesc::MakeColorScale(
XCEngine::Math::Vector4(0.55f, 0.8f, 1.2f, 1.0f)),
XCEngine::Rendering::FullscreenPassDesc::MakeColorScale(
XCEngine::Math::Vector4(1.0f, 0.95f, 0.75f, 1.0f))
});
std::stringstream stream;
source.Serialize(stream);
CameraComponent target;
target.Deserialize(stream);
EXPECT_EQ(target.GetClearMode(), CameraClearMode::DepthOnly);
EXPECT_EQ(target.GetStackType(), CameraStackType::Overlay);
EXPECT_EQ(target.GetCullingMask(), 0x0000000Fu);
EXPECT_FLOAT_EQ(target.GetViewportRect().x, 0.25f);
EXPECT_FLOAT_EQ(target.GetViewportRect().y, 0.125f);
EXPECT_FLOAT_EQ(target.GetViewportRect().width, 0.5f);
EXPECT_FLOAT_EQ(target.GetViewportRect().height, 0.625f);
EXPECT_TRUE(target.IsSkyboxEnabled());
EXPECT_TRUE(target.GetSkyboxMaterialPath().empty());
EXPECT_FLOAT_EQ(target.GetSkyboxTopColor().b, 0.64f);
EXPECT_FLOAT_EQ(target.GetSkyboxHorizonColor().g, 0.76f);
EXPECT_FLOAT_EQ(target.GetSkyboxBottomColor().r, 0.92f);
EXPECT_TRUE(target.GetFinalColorOverrides().overrideOutputTransferMode);
EXPECT_EQ(
target.GetFinalColorOverrides().outputTransferMode,
XCEngine::Rendering::FinalColorOutputTransferMode::LinearToSRGB);
EXPECT_TRUE(target.GetFinalColorOverrides().overrideExposureMode);
EXPECT_EQ(
target.GetFinalColorOverrides().exposureMode,
XCEngine::Rendering::FinalColorExposureMode::Fixed);
EXPECT_TRUE(target.GetFinalColorOverrides().overrideExposureValue);
EXPECT_FLOAT_EQ(target.GetFinalColorOverrides().exposureValue, 1.35f);
EXPECT_TRUE(target.GetFinalColorOverrides().overrideFinalColorScale);
EXPECT_FLOAT_EQ(target.GetFinalColorOverrides().finalColorScale.y, 0.9f);
ASSERT_EQ(target.GetPostProcessPasses().size(), 2u);
EXPECT_EQ(
target.GetPostProcessPasses()[0].type,
XCEngine::Rendering::FullscreenPassType::ColorScale);
EXPECT_EQ(
target.GetPostProcessPasses()[1].type,
XCEngine::Rendering::FullscreenPassType::ColorScale);
EXPECT_TRUE(target.IsColorScalePostProcessEnabled());
EXPECT_FLOAT_EQ(target.GetColorScalePostProcessScale().x, 0.55f);
EXPECT_FLOAT_EQ(target.GetColorScalePostProcessScale().y, 0.8f);
EXPECT_FLOAT_EQ(target.GetColorScalePostProcessScale().z, 1.2f);
EXPECT_FLOAT_EQ(target.GetColorScalePostProcessScale().w, 1.0f);
ASSERT_EQ(target.GetColorScalePostProcessPasses().size(), 2u);
EXPECT_FLOAT_EQ(target.GetColorScalePostProcessPasses()[1].x, 1.0f);
EXPECT_FLOAT_EQ(target.GetColorScalePostProcessPasses()[1].y, 0.95f);
EXPECT_FLOAT_EQ(target.GetColorScalePostProcessPasses()[1].z, 0.75f);
EXPECT_FLOAT_EQ(target.GetColorScalePostProcessPasses()[1].w, 1.0f);
}
TEST(CameraComponent_Test, DeserializeLegacyColorScalePostProcessFields) {
std::stringstream stream;
stream
<< "colorScalePostProcessEnabled=1;"
<< "colorScalePostProcessScale=0.55,0.8,1.2,1;"
<< "colorScalePostProcessPassCount=2;"
<< "colorScalePostProcessPass0=0.55,0.8,1.2,1;"
<< "colorScalePostProcessPass1=1,0.95,0.75,1;";
CameraComponent camera;
camera.Deserialize(stream);
ASSERT_EQ(camera.GetPostProcessPasses().size(), 2u);
EXPECT_EQ(
camera.GetPostProcessPasses()[0].type,
XCEngine::Rendering::FullscreenPassType::ColorScale);
EXPECT_TRUE(camera.IsColorScalePostProcessEnabled());
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].x, 0.55f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[1].y, 0.95f);
}
TEST(CameraComponent_Test, LegacyColorScaleAccessorsDriveFirstPass) {
CameraComponent camera;
camera.SetColorScalePostProcessScale(XCEngine::Math::Vector4(0.55f, 0.8f, 1.2f, 1.0f));
EXPECT_FALSE(camera.IsColorScalePostProcessEnabled());
camera.SetColorScalePostProcessEnabled(true);
ASSERT_EQ(camera.GetColorScalePostProcessPasses().size(), 1u);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].x, 0.55f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].y, 0.8f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].z, 1.2f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].w, 1.0f);
camera.AddColorScalePostProcessPass(XCEngine::Math::Vector4(1.0f, 0.95f, 0.75f, 1.0f));
camera.SetColorScalePostProcessScale(XCEngine::Math::Vector4(0.6f, 0.7f, 1.1f, 1.0f));
ASSERT_EQ(camera.GetColorScalePostProcessPasses().size(), 2u);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].x, 0.6f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].y, 0.7f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].z, 1.1f);
EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[1].y, 0.95f);
}
TEST(LightComponent_Test, DefaultValues) {
LightComponent light;
EXPECT_EQ(light.GetLightType(), LightType::Directional);
EXPECT_FLOAT_EQ(light.GetIntensity(), 1.0f);
EXPECT_FLOAT_EQ(light.GetRange(), 10.0f);
EXPECT_FLOAT_EQ(light.GetSpotAngle(), 30.0f);
EXPECT_FALSE(light.GetCastsShadows());
EXPECT_FALSE(light.GetOverridesDirectionalShadowSettings());
EXPECT_FLOAT_EQ(light.GetDirectionalShadowReceiverDepthBias(), 0.0010f);
EXPECT_FLOAT_EQ(light.GetDirectionalShadowNormalBiasScale(), 2.0f);
EXPECT_FLOAT_EQ(light.GetDirectionalShadowStrength(), 0.85f);
EXPECT_FLOAT_EQ(light.GetDirectionalShadowDepthBiasFactor(), 2.5f);
EXPECT_EQ(light.GetDirectionalShadowDepthBiasUnits(), 4);
}
TEST(LightComponent_Test, SetterClamping) {
LightComponent light;
light.SetIntensity(-3.0f);
light.SetRange(-1.0f);
light.SetSpotAngle(500.0f);
EXPECT_FLOAT_EQ(light.GetIntensity(), 0.0f);
EXPECT_FLOAT_EQ(light.GetRange(), 0.001f);
EXPECT_FLOAT_EQ(light.GetSpotAngle(), 179.0f);
light.SetDirectionalShadowReceiverDepthBias(-1.0f);
light.SetDirectionalShadowNormalBiasScale(-2.0f);
light.SetDirectionalShadowStrength(5.0f);
light.SetDirectionalShadowDepthBiasFactor(-3.0f);
light.SetDirectionalShadowDepthBiasUnits(-4);
EXPECT_FLOAT_EQ(light.GetDirectionalShadowReceiverDepthBias(), 0.0010f);
EXPECT_FLOAT_EQ(light.GetDirectionalShadowNormalBiasScale(), 2.0f);
EXPECT_FLOAT_EQ(light.GetDirectionalShadowStrength(), 1.0f);
EXPECT_FLOAT_EQ(light.GetDirectionalShadowDepthBiasFactor(), 2.5f);
EXPECT_EQ(light.GetDirectionalShadowDepthBiasUnits(), 0);
}
TEST(LightComponent_Test, SerializeRoundTripPreservesDirectionalShadowOverrides) {
LightComponent source;
source.SetLightType(LightType::Directional);
source.SetCastsShadows(true);
source.SetOverridesDirectionalShadowSettings(true);
source.SetDirectionalShadowReceiverDepthBias(0.0025f);
source.SetDirectionalShadowNormalBiasScale(1.75f);
source.SetDirectionalShadowStrength(0.72f);
source.SetDirectionalShadowDepthBiasFactor(3.5f);
source.SetDirectionalShadowDepthBiasUnits(6);
std::stringstream stream;
source.Serialize(stream);
LightComponent target;
target.Deserialize(stream);
EXPECT_TRUE(target.GetCastsShadows());
EXPECT_TRUE(target.GetOverridesDirectionalShadowSettings());
EXPECT_FLOAT_EQ(target.GetDirectionalShadowReceiverDepthBias(), 0.0025f);
EXPECT_FLOAT_EQ(target.GetDirectionalShadowNormalBiasScale(), 1.75f);
EXPECT_FLOAT_EQ(target.GetDirectionalShadowStrength(), 0.72f);
EXPECT_FLOAT_EQ(target.GetDirectionalShadowDepthBiasFactor(), 3.5f);
EXPECT_EQ(target.GetDirectionalShadowDepthBiasUnits(), 6);
}
} // namespace

View File

@@ -1,132 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/Component.h>
#include <XCEngine/Components/GameObject.h>
using namespace XCEngine::Components;
namespace {
class TestComponent : public Component {
public:
TestComponent() = default;
explicit TestComponent(const std::string& name) : m_customName(name) {}
std::string GetName() const override {
return m_customName.empty() ? "TestComponent" : m_customName;
}
bool m_awakeCalled = false;
bool m_startCalled = false;
bool m_updateCalled = false;
bool m_onEnableCalled = false;
bool m_onDisableCalled = false;
bool m_onDestroyCalled = false;
int m_onEnableCount = 0;
int m_onDisableCount = 0;
void Awake() override { m_awakeCalled = true; }
void Start() override { m_startCalled = true; }
void Update(float deltaTime) override { m_updateCalled = true; }
void OnEnable() override { m_onEnableCalled = true; ++m_onEnableCount; }
void OnDisable() override { m_onDisableCalled = true; ++m_onDisableCount; }
void OnDestroy() override { m_onDestroyCalled = true; }
private:
std::string m_customName;
};
TEST(Component_Test, DefaultConstructor) {
TestComponent comp;
EXPECT_EQ(comp.GetName(), "TestComponent");
}
TEST(Component_Test, GetGameObject_ReturnsNullptr_WhenNotAttached) {
TestComponent comp;
EXPECT_EQ(comp.GetGameObject(), nullptr);
}
TEST(Component_Test, IsEnabled_DefaultTrue) {
TestComponent comp;
EXPECT_TRUE(comp.IsEnabled());
}
TEST(Component_Test, SetEnabled_TrueToFalse) {
TestComponent comp;
EXPECT_TRUE(comp.IsEnabled());
comp.SetEnabled(false);
EXPECT_FALSE(comp.IsEnabled());
}
TEST(Component_Test, SetEnabled_FalseToTrue) {
TestComponent comp;
comp.SetEnabled(false);
EXPECT_FALSE(comp.IsEnabled());
comp.SetEnabled(true);
EXPECT_TRUE(comp.IsEnabled());
}
TEST(Component_Test, Lifecycle_AwakeCalled) {
TestComponent comp;
comp.Awake();
EXPECT_TRUE(comp.m_awakeCalled);
}
TEST(Component_Test, Lifecycle_StartCalled) {
TestComponent comp;
comp.Start();
EXPECT_TRUE(comp.m_startCalled);
}
TEST(Component_Test, Lifecycle_UpdateCalled) {
TestComponent comp;
comp.Update(0.016f);
EXPECT_TRUE(comp.m_updateCalled);
}
TEST(Component_Test, Lifecycle_OnEnableCalled_WhenEnabling) {
TestComponent comp;
comp.SetEnabled(false);
comp.SetEnabled(true);
EXPECT_TRUE(comp.m_onEnableCalled);
}
TEST(Component_Test, Lifecycle_OnDisableCalled_WhenDisabling) {
TestComponent comp;
comp.SetEnabled(false);
EXPECT_TRUE(comp.m_onDisableCalled);
}
TEST(Component_Test, Lifecycle_OnDestroyCalled) {
TestComponent comp;
comp.OnDestroy();
EXPECT_TRUE(comp.m_onDestroyCalled);
}
TEST(Component_Test, SetEnabled_NoCallback_WhenStateUnchanged) {
TestComponent comp;
comp.SetEnabled(true);
EXPECT_FALSE(comp.m_onEnableCalled);
EXPECT_FALSE(comp.m_onDisableCalled);
}
TEST(Component_Test, SetEnabled_AttachedInactiveGameObject_DelaysOnEnableUntilActivation) {
GameObject go;
TestComponent* comp = go.AddComponent<TestComponent>();
go.SetActive(false);
comp->m_onEnableCalled = false;
comp->m_onDisableCalled = false;
comp->m_onEnableCount = 0;
comp->m_onDisableCount = 0;
comp->SetEnabled(false);
EXPECT_EQ(comp->m_onDisableCount, 0);
comp->SetEnabled(true);
EXPECT_EQ(comp->m_onEnableCount, 0);
go.SetActive(true);
EXPECT_EQ(comp->m_onEnableCount, 1);
}
} // namespace

View File

@@ -1,60 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/AudioListenerComponent.h>
#include <XCEngine/Components/AudioSourceComponent.h>
#include <XCEngine/Components/BoxColliderComponent.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/CapsuleColliderComponent.h>
#include <XCEngine/Components/ComponentFactoryRegistry.h>
#include <XCEngine/Components/GaussianSplatRendererComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Components/RigidbodyComponent.h>
#include <XCEngine/Components/SphereColliderComponent.h>
#include <XCEngine/Components/VolumeRendererComponent.h>
using namespace XCEngine::Components;
namespace {
TEST(ComponentFactoryRegistry_Test, BuiltInTypesAreRegistered) {
auto& registry = ComponentFactoryRegistry::Get();
EXPECT_TRUE(registry.IsRegistered("Camera"));
EXPECT_TRUE(registry.IsRegistered("Light"));
EXPECT_TRUE(registry.IsRegistered("AudioSource"));
EXPECT_TRUE(registry.IsRegistered("AudioListener"));
EXPECT_TRUE(registry.IsRegistered("Rigidbody"));
EXPECT_TRUE(registry.IsRegistered("BoxCollider"));
EXPECT_TRUE(registry.IsRegistered("SphereCollider"));
EXPECT_TRUE(registry.IsRegistered("CapsuleCollider"));
EXPECT_TRUE(registry.IsRegistered("MeshFilter"));
EXPECT_TRUE(registry.IsRegistered("MeshRenderer"));
EXPECT_TRUE(registry.IsRegistered("GaussianSplatRenderer"));
EXPECT_TRUE(registry.IsRegistered("VolumeRenderer"));
EXPECT_FALSE(registry.IsRegistered("Transform"));
EXPECT_FALSE(registry.IsRegistered("MissingComponent"));
}
TEST(ComponentFactoryRegistry_Test, CreateBuiltInComponentsByTypeName) {
GameObject gameObject("FactoryTarget");
auto& registry = ComponentFactoryRegistry::Get();
EXPECT_NE(dynamic_cast<CameraComponent*>(registry.CreateComponent(&gameObject, "Camera")), nullptr);
EXPECT_NE(dynamic_cast<LightComponent*>(registry.CreateComponent(&gameObject, "Light")), nullptr);
EXPECT_NE(dynamic_cast<AudioSourceComponent*>(registry.CreateComponent(&gameObject, "AudioSource")), nullptr);
EXPECT_NE(dynamic_cast<AudioListenerComponent*>(registry.CreateComponent(&gameObject, "AudioListener")), nullptr);
EXPECT_NE(dynamic_cast<RigidbodyComponent*>(registry.CreateComponent(&gameObject, "Rigidbody")), nullptr);
EXPECT_NE(dynamic_cast<BoxColliderComponent*>(registry.CreateComponent(&gameObject, "BoxCollider")), nullptr);
EXPECT_NE(dynamic_cast<SphereColliderComponent*>(registry.CreateComponent(&gameObject, "SphereCollider")), nullptr);
EXPECT_NE(dynamic_cast<CapsuleColliderComponent*>(registry.CreateComponent(&gameObject, "CapsuleCollider")), nullptr);
EXPECT_NE(dynamic_cast<MeshFilterComponent*>(registry.CreateComponent(&gameObject, "MeshFilter")), nullptr);
EXPECT_NE(dynamic_cast<MeshRendererComponent*>(registry.CreateComponent(&gameObject, "MeshRenderer")), nullptr);
EXPECT_NE(dynamic_cast<GaussianSplatRendererComponent*>(registry.CreateComponent(&gameObject, "GaussianSplatRenderer")), nullptr);
EXPECT_NE(dynamic_cast<VolumeRendererComponent*>(registry.CreateComponent(&gameObject, "VolumeRenderer")), nullptr);
EXPECT_EQ(registry.CreateComponent(&gameObject, "MissingComponent"), nullptr);
}
} // namespace

View File

@@ -1,359 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/TransformComponent.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <sstream>
using namespace XCEngine::Components;
using namespace XCEngine::Math;
namespace {
class TestComponent : public Component {
public:
TestComponent() = default;
explicit TestComponent(const std::string& name) : m_customName(name) {}
std::string GetName() const override {
return m_customName.empty() ? "TestComponent" : m_customName;
}
bool m_awakeCalled = false;
bool m_startCalled = false;
bool m_updateCalled = false;
bool m_onEnableCalled = false;
bool m_onDisableCalled = false;
bool m_onDestroyCalled = false;
int m_startCount = 0;
int m_updateCount = 0;
int m_onEnableCount = 0;
int m_onDisableCount = 0;
int m_onDestroyCount = 0;
void Awake() override { m_awakeCalled = true; }
void Start() override { m_startCalled = true; ++m_startCount; }
void Update(float deltaTime) override { m_updateCalled = true; ++m_updateCount; }
void OnEnable() override { m_onEnableCalled = true; ++m_onEnableCount; }
void OnDisable() override { m_onDisableCalled = true; ++m_onDisableCount; }
void OnDestroy() override { m_onDestroyCalled = true; ++m_onDestroyCount; }
private:
std::string m_customName;
};
class GameObjectTest : public ::testing::Test {
protected:
void SetUp() override {
testScene = std::make_unique<Scene>("TestScene");
}
std::unique_ptr<Scene> testScene;
};
TEST(GameObject_Test, DefaultConstructor_DefaultValues) {
GameObject go;
EXPECT_EQ(go.GetName(), "GameObject");
EXPECT_EQ(go.GetTag(), "Untagged");
EXPECT_TRUE(go.CompareTag("Untagged"));
EXPECT_TRUE(go.IsActive());
EXPECT_NE(go.GetID(), GameObject::INVALID_ID);
}
TEST(GameObject_Test, NamedConstructor) {
GameObject go("TestObject");
EXPECT_EQ(go.GetName(), "TestObject");
}
TEST(GameObject_Test, Layer_GetSetAndSerializeRoundTrip) {
GameObject source("LayeredObject");
source.SetTag("Player");
source.SetLayer(7);
std::stringstream stream;
source.Serialize(stream);
GameObject target;
target.Deserialize(stream);
EXPECT_EQ(source.GetTag(), "Player");
EXPECT_EQ(source.GetLayer(), 7u);
EXPECT_EQ(target.GetTag(), "Player");
EXPECT_EQ(target.GetLayer(), 7u);
}
TEST(GameObject_Test, AddComponent_Single) {
GameObject go;
TestComponent* comp = go.AddComponent<TestComponent>();
EXPECT_NE(comp, nullptr);
EXPECT_EQ(go.GetComponent<TestComponent>(), comp);
}
TEST(GameObject_Test, AddComponent_Multiple) {
GameObject go;
TestComponent* comp1 = go.AddComponent<TestComponent>();
TestComponent* comp2 = go.AddComponent<TestComponent>();
EXPECT_NE(comp1, nullptr);
EXPECT_NE(comp2, nullptr);
EXPECT_NE(comp1, comp2);
}
TEST(GameObject_Test, AddComponent_TransformComponent) {
GameObject go;
TransformComponent* tc = go.GetTransform();
EXPECT_NE(tc, nullptr);
EXPECT_EQ(tc->GetName(), "Transform");
}
TEST(GameObject_Test, GetComponent_Exists) {
GameObject go;
TestComponent* comp = go.AddComponent<TestComponent>();
TestComponent* found = go.GetComponent<TestComponent>();
EXPECT_EQ(found, comp);
}
TEST(GameObject_Test, GetComponent_NotExists) {
GameObject go;
TestComponent* found = go.GetComponent<TestComponent>();
EXPECT_EQ(found, nullptr);
}
TEST(GameObject_Test, GetComponents_Multiple) {
GameObject go;
go.AddComponent<TestComponent>();
go.AddComponent<TestComponent>();
go.AddComponent<TestComponent>();
std::vector<TestComponent*> comps = go.GetComponents<TestComponent>();
EXPECT_EQ(comps.size(), 3u);
}
TEST(GameObject_Test, GetTransform_ReturnsTransform) {
GameObject go;
TransformComponent* tc = go.GetTransform();
EXPECT_NE(tc, nullptr);
}
TEST(GameObject_Test, SetParent_WithWorldPosition) {
GameObject parent("Parent");
GameObject child("Child");
parent.GetTransform()->SetLocalPosition(Vector3(1.0f, 0.0f, 0.0f));
child.GetTransform()->SetLocalPosition(Vector3(2.0f, 0.0f, 0.0f));
child.SetParent(&parent, true);
Vector3 childWorldPos = child.GetTransform()->GetPosition();
EXPECT_NEAR(childWorldPos.x, 2.0f, 0.001f);
}
TEST(GameObject_Test, SetParent_WithoutWorldPosition) {
GameObject parent("Parent");
GameObject child("Child");
parent.GetTransform()->SetLocalPosition(Vector3(1.0f, 0.0f, 0.0f));
child.GetTransform()->SetLocalPosition(Vector3(2.0f, 0.0f, 0.0f));
child.SetParent(&parent, false);
Vector3 childWorldPos = child.GetTransform()->GetPosition();
EXPECT_NEAR(childWorldPos.x, 3.0f, 0.001f);
}
TEST(GameObject_Test, GetChild_ValidIndex) {
GameObject parent("Parent");
GameObject child("Child");
child.SetParent(&parent);
GameObject* found = parent.GetChild(0);
EXPECT_EQ(found, &child);
}
TEST(GameObject_Test, GetChild_InvalidIndex) {
GameObject parent("Parent");
GameObject* found = parent.GetChild(0);
EXPECT_EQ(found, nullptr);
}
TEST(GameObject_Test, GetChildren_ReturnsAllChildren) {
GameObject parent("Parent");
GameObject child1("Child1");
GameObject child2("Child2");
child1.SetParent(&parent);
child2.SetParent(&parent);
std::vector<GameObject*> children = parent.GetChildren();
EXPECT_EQ(children.size(), 2u);
}
TEST(GameObject_Test, DetachChildren) {
GameObject parent("Parent");
GameObject child("Child");
child.SetParent(&parent);
EXPECT_EQ(parent.GetChildCount(), 1u);
parent.DetachChildren();
EXPECT_EQ(parent.GetChildCount(), 0u);
}
TEST(GameObject_Test, IsActive_DefaultTrue) {
GameObject go;
EXPECT_TRUE(go.IsActive());
}
TEST(GameObject_Test, SetActive_False) {
GameObject go;
go.SetActive(false);
EXPECT_FALSE(go.IsActive());
}
TEST(GameObject_Test, SetActive_PropagatesEnableDisableToChildren) {
GameObject parent("Parent");
GameObject child("Child");
TestComponent* comp = child.AddComponent<TestComponent>();
child.SetParent(&parent);
parent.SetActive(false);
EXPECT_EQ(comp->m_onDisableCount, 1);
parent.SetActive(true);
EXPECT_EQ(comp->m_onEnableCount, 1);
}
TEST(GameObject_Test, SetParent_PropagatesActiveHierarchyChanges) {
GameObject activeParent("ActiveParent");
GameObject inactiveParent("InactiveParent");
GameObject child("Child");
TestComponent* comp = child.AddComponent<TestComponent>();
inactiveParent.SetActive(false);
child.SetParent(&inactiveParent);
EXPECT_EQ(comp->m_onDisableCount, 1);
child.SetParent(&activeParent);
EXPECT_EQ(comp->m_onEnableCount, 1);
}
TEST(GameObject_Test, IsActiveInHierarchy_True) {
GameObject parent("Parent");
GameObject child("Child");
child.SetParent(&parent);
EXPECT_TRUE(child.IsActiveInHierarchy());
}
TEST(GameObject_Test, IsActiveInHierarchy_FalseWhenParentInactive) {
GameObject parent("Parent");
GameObject child("Child");
child.SetParent(&parent);
parent.SetActive(false);
EXPECT_FALSE(child.IsActiveInHierarchy());
}
TEST(GameObject_Test, Lifecycle_Awake) {
GameObject go;
TestComponent* comp = go.AddComponent<TestComponent>();
go.Awake();
EXPECT_TRUE(comp->m_awakeCalled);
}
TEST(GameObject_Test, Lifecycle_Start) {
GameObject go;
TestComponent* comp = go.AddComponent<TestComponent>();
go.Start();
EXPECT_TRUE(comp->m_startCalled);
EXPECT_EQ(comp->m_startCount, 1);
}
TEST(GameObject_Test, Lifecycle_Start_IsOnlyCalledOnce) {
GameObject go;
TestComponent* comp = go.AddComponent<TestComponent>();
go.Start();
go.Start();
EXPECT_EQ(comp->m_startCount, 1);
}
TEST(GameObject_Test, Lifecycle_Update) {
GameObject go;
TestComponent* comp = go.AddComponent<TestComponent>();
go.Update(0.016f);
EXPECT_TRUE(comp->m_updateCalled);
EXPECT_EQ(comp->m_updateCount, 1);
}
TEST(GameObject_Test, Destroy_CallsOnDestroyOnce_WhenStandalone) {
GameObject go;
TestComponent* comp = go.AddComponent<TestComponent>();
go.Destroy();
EXPECT_TRUE(comp->m_onDestroyCalled);
EXPECT_EQ(comp->m_onDestroyCount, 1);
}
TEST_F(GameObjectTest, Find_Exists) {
GameObject* go = testScene->CreateGameObject("TestObject");
GameObject* found = GameObject::Find("TestObject");
EXPECT_NE(found, nullptr);
EXPECT_EQ(found->GetName(), "TestObject");
}
TEST(GameObject_Test, Find_NotExists) {
GameObject* found = GameObject::Find("NonExistent");
EXPECT_EQ(found, nullptr);
}
TEST(GameObject_Test, GetChildCount) {
GameObject parent("Parent");
GameObject child1("Child1");
GameObject child2("Child2");
child1.SetParent(&parent);
child2.SetParent(&parent);
EXPECT_EQ(parent.GetChildCount(), 2u);
}
} // namespace

View File

@@ -1,182 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/GaussianSplatRendererComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
#include <XCEngine/Resources/Material/Material.h>
#include <chrono>
#include <cstring>
#include <sstream>
#include <thread>
using namespace XCEngine::Components;
using namespace XCEngine::Resources;
namespace {
GaussianSplat* CreateTestGaussianSplat(const char* name, const char* path) {
auto* gaussianSplat = new GaussianSplat();
IResource::ConstructParams params = {};
params.name = name;
params.path = path;
params.guid = ResourceGUID::Generate(path);
gaussianSplat->Initialize(params);
GaussianSplatMetadata metadata = {};
metadata.splatCount = 1u;
XCEngine::Containers::Array<GaussianSplatSection> sections;
sections.Resize(1);
sections[0].type = GaussianSplatSectionType::Positions;
sections[0].format = GaussianSplatSectionFormat::VectorFloat32;
sections[0].dataOffset = 0u;
sections[0].dataSize = sizeof(GaussianSplatPositionRecord);
sections[0].elementCount = 1u;
sections[0].elementStride = sizeof(GaussianSplatPositionRecord);
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
payload.Resize(sizeof(GaussianSplatPositionRecord));
const GaussianSplatPositionRecord positionRecord = { XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f) };
std::memcpy(payload.Data(), &positionRecord, sizeof(positionRecord));
EXPECT_TRUE(gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload)));
return gaussianSplat;
}
Material* CreateTestMaterial(const char* name, const char* path) {
auto* material = new Material();
IResource::ConstructParams params = {};
params.name = name;
params.path = path;
params.guid = ResourceGUID::Generate(path);
material->Initialize(params);
return material;
}
class FakeAsyncGaussianSplatLoader final : public IResourceLoader {
public:
ResourceType GetResourceType() const override { return ResourceType::GaussianSplat; }
XCEngine::Containers::Array<XCEngine::Containers::String> GetSupportedExtensions() const override {
XCEngine::Containers::Array<XCEngine::Containers::String> extensions;
extensions.PushBack("ply");
extensions.PushBack("xcgsplat");
return extensions;
}
bool CanLoad(const XCEngine::Containers::String& path) const override {
(void)path;
return true;
}
LoadResult Load(
const XCEngine::Containers::String& path,
const ImportSettings* settings = nullptr) override {
(void)settings;
return LoadResult(CreateTestGaussianSplat("AsyncGaussianSplat", path.CStr()));
}
ImportSettings* GetDefaultSettings() const override {
return nullptr;
}
};
bool PumpAsyncLoadsUntilIdle(
ResourceManager& manager,
std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (manager.IsAsyncLoading() && std::chrono::steady_clock::now() < deadline) {
manager.UpdateAsyncLoads();
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
manager.UpdateAsyncLoads();
return !manager.IsAsyncLoading();
}
TEST(GaussianSplatRendererComponent_Test, SetResourcesCachesHandlesPathsAndFlags) {
GameObject gameObject("GaussianSplatHolder");
auto* component = gameObject.AddComponent<GaussianSplatRendererComponent>();
GaussianSplat* gaussianSplat = CreateTestGaussianSplat("Room", "GaussianSplats/room.xcgsplat");
Material* material = CreateTestMaterial("GaussianSplatMaterial", "Materials/gaussian_splat.mat");
component->SetGaussianSplat(gaussianSplat);
component->SetMaterial(material);
component->SetCastShadows(false);
component->SetReceiveShadows(false);
EXPECT_EQ(component->GetGaussianSplat(), gaussianSplat);
EXPECT_EQ(component->GetGaussianSplatPath(), "GaussianSplats/room.xcgsplat");
EXPECT_EQ(component->GetMaterial(), material);
EXPECT_EQ(component->GetMaterialPath(), "Materials/gaussian_splat.mat");
EXPECT_FALSE(component->GetCastShadows());
EXPECT_FALSE(component->GetReceiveShadows());
component->ClearGaussianSplat();
component->ClearMaterial();
delete gaussianSplat;
delete material;
}
TEST(GaussianSplatRendererComponent_Test, SerializeAndDeserializePreservesVirtualPathsAndFlags) {
GaussianSplatRendererComponent source;
source.SetGaussianSplatPath("test://gaussian_splats/room.ply");
source.SetMaterialPath("test://materials/gaussian_splat.mat");
source.SetCastShadows(false);
source.SetReceiveShadows(true);
std::stringstream stream;
source.Serialize(stream);
const std::string serialized = stream.str();
EXPECT_NE(serialized.find("gaussianSplatPath=test://gaussian_splats/room.ply;"), std::string::npos);
EXPECT_NE(serialized.find("materialPath=test://materials/gaussian_splat.mat;"), std::string::npos);
EXPECT_NE(serialized.find("castShadows=0;"), std::string::npos);
EXPECT_NE(serialized.find("receiveShadows=1;"), std::string::npos);
GaussianSplatRendererComponent target;
std::stringstream deserializeStream(serialized);
target.Deserialize(deserializeStream);
EXPECT_EQ(target.GetGaussianSplatPath(), "test://gaussian_splats/room.ply");
EXPECT_EQ(target.GetMaterialPath(), "test://materials/gaussian_splat.mat");
EXPECT_FALSE(target.GetCastShadows());
EXPECT_TRUE(target.GetReceiveShadows());
EXPECT_FALSE(target.GetGaussianSplatAssetRef().IsValid());
EXPECT_FALSE(target.GetMaterialAssetRef().IsValid());
}
TEST(GaussianSplatRendererComponent_Test, DeferredSceneDeserializeLoadsGaussianSplatAsyncByPath) {
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
IResourceLoader* originalLoader = manager.GetLoader(ResourceType::GaussianSplat);
FakeAsyncGaussianSplatLoader fakeLoader;
manager.RegisterLoader(&fakeLoader);
GaussianSplatRendererComponent target;
const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount();
{
ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled());
std::stringstream stream(
"gaussianSplatPath=test://gaussian_splats/async_room.ply;gaussianSplatRef=;materialRef=;castShadows=1;receiveShadows=1;");
target.Deserialize(stream);
}
EXPECT_EQ(target.GetGaussianSplatPath(), "test://gaussian_splats/async_room.ply");
EXPECT_EQ(target.GetGaussianSplat(), nullptr);
EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
ASSERT_NE(target.GetGaussianSplat(), nullptr);
EXPECT_EQ(target.GetGaussianSplatPath(), "test://gaussian_splats/async_room.ply");
EXPECT_EQ(target.GetGaussianSplat()->GetSplatCount(), 1u);
manager.RegisterLoader(originalLoader);
manager.Shutdown();
}
} // namespace

View File

@@ -1,501 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Resources/Material/Material.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <chrono>
#include <thread>
using namespace XCEngine::Components;
using namespace XCEngine::Resources;
namespace {
Mesh* CreateTestMesh(const char* name, const char* path) {
auto* mesh = new Mesh();
IResource::ConstructParams params = {};
params.name = name;
params.path = path;
params.guid = ResourceGUID::Generate(path);
mesh->Initialize(params);
return mesh;
}
Material* CreateTestMaterial(const char* name, const char* path) {
auto* material = new Material();
IResource::ConstructParams params = {};
params.name = name;
params.path = path;
params.guid = ResourceGUID::Generate(path);
material->Initialize(params);
return material;
}
class FakeAsyncMeshLoader : public IResourceLoader {
public:
ResourceType GetResourceType() const override { return ResourceType::Mesh; }
XCEngine::Containers::Array<XCEngine::Containers::String> GetSupportedExtensions() const override {
XCEngine::Containers::Array<XCEngine::Containers::String> extensions;
extensions.PushBack("mesh");
return extensions;
}
bool CanLoad(const XCEngine::Containers::String& path) const override {
(void)path;
return true;
}
LoadResult Load(const XCEngine::Containers::String& path,
const ImportSettings* settings = nullptr) override {
(void)settings;
auto* mesh = new Mesh();
IResource::ConstructParams params = {};
params.name = "AsyncMesh";
params.path = path;
params.guid = ResourceGUID::Generate(path);
mesh->Initialize(params);
const StaticMeshVertex vertices[3] = {};
const XCEngine::Core::uint32 indices[3] = {0, 1, 2};
mesh->SetVertexData(vertices, sizeof(vertices), 3, sizeof(StaticMeshVertex), VertexAttribute::Position);
mesh->SetIndexData(indices, sizeof(indices), 3, true);
return LoadResult(mesh);
}
ImportSettings* GetDefaultSettings() const override {
return nullptr;
}
};
bool PumpAsyncLoadsUntilIdle(ResourceManager& manager,
std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (manager.IsAsyncLoading() && std::chrono::steady_clock::now() < deadline) {
manager.UpdateAsyncLoads();
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
manager.UpdateAsyncLoads();
return !manager.IsAsyncLoading();
}
TEST(MeshFilterComponent_Test, SetMeshCachesResourceAndPath) {
GameObject gameObject("MeshHolder");
auto* component = gameObject.AddComponent<MeshFilterComponent>();
Mesh* mesh = CreateTestMesh("Quad", "Meshes/quad.mesh");
component->SetMesh(mesh);
EXPECT_EQ(component->GetMesh(), mesh);
EXPECT_EQ(component->GetMeshPath(), "Meshes/quad.mesh");
component->ClearMesh();
delete mesh;
}
TEST(MeshFilterComponent_Test, SerializeAndDeserializePreservesPath) {
MeshFilterComponent source;
source.SetMeshPath("builtin://meshes/cube");
std::stringstream stream;
source.Serialize(stream);
const std::string serialized = stream.str();
EXPECT_NE(serialized.find("meshRef="), std::string::npos);
EXPECT_NE(serialized.find("meshPath=builtin://meshes/cube;"), std::string::npos);
MeshFilterComponent target;
std::stringstream deserializeStream(serialized);
target.Deserialize(deserializeStream);
EXPECT_EQ(target.GetMeshPath(), "builtin://meshes/cube");
ASSERT_NE(target.GetMesh(), nullptr);
EXPECT_FALSE(target.GetMeshAssetRef().IsValid());
}
TEST(MeshFilterComponent_Test, DeserializeIgnoresPlainMeshPathWithoutAssetRef) {
MeshFilterComponent target;
std::stringstream stream("meshPath=Meshes/legacy.mesh;meshRef=;");
target.Deserialize(stream);
EXPECT_TRUE(target.GetMeshPath().empty());
EXPECT_EQ(target.GetMesh(), nullptr);
EXPECT_FALSE(target.GetMeshAssetRef().IsValid());
}
TEST(MeshFilterComponent_Test, SetMeshPathPreservesPathWithoutLoadedResource) {
MeshFilterComponent component;
component.SetMeshPath("Meshes/runtime.mesh");
EXPECT_EQ(component.GetMeshPath(), "Meshes/runtime.mesh");
EXPECT_EQ(component.GetMesh(), nullptr);
component.SetMeshPath("");
EXPECT_EQ(component.GetMeshPath(), "");
EXPECT_EQ(component.GetMesh(), nullptr);
}
TEST(MeshFilterComponent_Test, SetMeshAssetRefPreservesProjectSubAssetReference) {
namespace fs = std::filesystem;
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_filter_asset_ref_test";
const fs::path assetsDir = projectRoot / "Assets";
const fs::path meshPath = assetsDir / "runtime.mesh";
fs::remove_all(projectRoot);
fs::create_directories(assetsDir);
{
std::ofstream meshFile(meshPath);
ASSERT_TRUE(meshFile.is_open());
meshFile << "placeholder";
}
manager.SetResourceRoot(projectRoot.string().c_str());
AssetRef meshRef;
ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.mesh", ResourceType::Mesh, meshRef));
MeshFilterComponent component;
component.SetMeshAssetRef(meshRef);
EXPECT_TRUE(component.GetMeshAssetRef().IsValid());
EXPECT_EQ(component.GetMeshAssetRef().assetGuid, meshRef.assetGuid);
EXPECT_EQ(component.GetMeshAssetRef().localID, meshRef.localID);
EXPECT_EQ(component.GetMeshAssetRef().resourceType, meshRef.resourceType);
EXPECT_EQ(component.GetMeshPath(), "Assets/runtime.mesh");
std::stringstream stream;
component.Serialize(stream);
EXPECT_NE(stream.str().find("meshRef="), std::string::npos);
EXPECT_EQ(stream.str().find("meshRef=;"), std::string::npos);
manager.SetResourceRoot("");
manager.Shutdown();
fs::remove_all(projectRoot);
}
TEST(MeshFilterComponent_Test, DeferredSceneDeserializeLoadsMeshAsyncByPath) {
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
IResourceLoader* originalLoader = manager.GetLoader(ResourceType::Mesh);
FakeAsyncMeshLoader fakeLoader;
manager.RegisterLoader(&fakeLoader);
MeshFilterComponent target;
const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount();
{
ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled());
std::stringstream stream("meshPath=test://meshes/async.mesh;meshRef=;");
target.Deserialize(stream);
}
EXPECT_EQ(target.GetMeshPath(), "test://meshes/async.mesh");
EXPECT_EQ(target.GetMesh(), nullptr);
EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
ASSERT_NE(target.GetMesh(), nullptr);
EXPECT_EQ(target.GetMeshPath(), "test://meshes/async.mesh");
EXPECT_EQ(target.GetMesh()->GetVertexCount(), 3u);
manager.RegisterLoader(originalLoader);
manager.Shutdown();
}
TEST(MeshRendererComponent_Test, SetMaterialsKeepsSlotsAndFlags) {
GameObject gameObject("RendererHolder");
auto* component = gameObject.AddComponent<MeshRendererComponent>();
Material* material0 = CreateTestMaterial("M0", "Materials/m0.mat");
Material* material1 = CreateTestMaterial("M1", "Materials/m1.mat");
component->SetMaterial(0, material0);
component->SetMaterial(1, material1);
component->SetCastShadows(false);
component->SetReceiveShadows(false);
component->SetRenderLayer(7);
ASSERT_EQ(component->GetMaterialCount(), 2u);
EXPECT_EQ(component->GetMaterial(0), material0);
EXPECT_EQ(component->GetMaterial(1), material1);
EXPECT_EQ(component->GetMaterialPaths()[0], "Materials/m0.mat");
EXPECT_EQ(component->GetMaterialPaths()[1], "Materials/m1.mat");
EXPECT_FALSE(component->GetCastShadows());
EXPECT_FALSE(component->GetReceiveShadows());
EXPECT_EQ(component->GetRenderLayer(), 7u);
component->ClearMaterials();
delete material0;
delete material1;
}
TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesMaterialPathsAndSettings) {
MeshRendererComponent source;
source.SetMaterialPath(0, "builtin://materials/default-primitive");
source.SetMaterialPath(1, "builtin://materials/default-primitive");
source.SetCastShadows(false);
source.SetReceiveShadows(true);
source.SetRenderLayer(3);
std::stringstream stream;
source.Serialize(stream);
const std::string serialized = stream.str();
EXPECT_NE(
serialized.find("materialPaths=builtin://materials/default-primitive|builtin://materials/default-primitive;"),
std::string::npos);
EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos);
MeshRendererComponent target;
std::stringstream deserializeStream(serialized);
target.Deserialize(deserializeStream);
ASSERT_EQ(target.GetMaterialCount(), 2u);
ASSERT_NE(target.GetMaterial(0), nullptr);
ASSERT_NE(target.GetMaterial(1), nullptr);
EXPECT_EQ(target.GetMaterialPaths()[0], "builtin://materials/default-primitive");
EXPECT_EQ(target.GetMaterialPaths()[1], "builtin://materials/default-primitive");
EXPECT_FALSE(target.GetCastShadows());
EXPECT_TRUE(target.GetReceiveShadows());
EXPECT_EQ(target.GetRenderLayer(), 3u);
}
TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesTrailingEmptyMaterialSlots) {
MeshRendererComponent source;
source.SetMaterialPath(0, "builtin://materials/default-primitive");
source.SetMaterialPath(1, "");
source.SetCastShadows(false);
source.SetReceiveShadows(true);
source.SetRenderLayer(9);
std::stringstream stream;
source.Serialize(stream);
const std::string serialized = stream.str();
EXPECT_NE(serialized.find("materialPaths=builtin://materials/default-primitive|;"), std::string::npos);
EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos);
MeshRendererComponent target;
std::stringstream deserializeStream(serialized);
target.Deserialize(deserializeStream);
ASSERT_EQ(target.GetMaterialCount(), 2u);
ASSERT_NE(target.GetMaterial(0), nullptr);
EXPECT_EQ(target.GetMaterial(1), nullptr);
EXPECT_EQ(target.GetMaterialPath(0), "builtin://materials/default-primitive");
EXPECT_EQ(target.GetMaterialPath(1), "");
EXPECT_FALSE(target.GetCastShadows());
EXPECT_TRUE(target.GetReceiveShadows());
EXPECT_EQ(target.GetRenderLayer(), 9u);
}
TEST(MeshRendererComponent_Test, SetMaterialPathPreservesPathWithoutLoadedResource) {
MeshRendererComponent component;
component.SetMaterialPath(1, "Materials/runtime.mat");
ASSERT_EQ(component.GetMaterialCount(), 2u);
EXPECT_EQ(component.GetMaterial(0), nullptr);
EXPECT_EQ(component.GetMaterial(1), nullptr);
EXPECT_EQ(component.GetMaterialPath(0), "");
EXPECT_EQ(component.GetMaterialPath(1), "Materials/runtime.mat");
component.SetMaterialPath(1, "");
EXPECT_EQ(component.GetMaterialPath(1), "");
EXPECT_EQ(component.GetMaterial(1), nullptr);
}
TEST(MeshRendererComponent_Test, SetMaterialAssetRefPreservesProjectSubAssetReference) {
namespace fs = std::filesystem;
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_renderer_sub_asset_ref_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 << " \"renderState\": {\n";
materialFile << " \"cull\": \"back\"\n";
materialFile << " }\n";
materialFile << "}";
}
manager.SetResourceRoot(projectRoot.string().c_str());
AssetRef materialRef;
ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, materialRef));
MeshRendererComponent component;
component.SetMaterialAssetRef(0, materialRef);
ASSERT_EQ(component.GetMaterialCount(), 1u);
EXPECT_TRUE(component.GetMaterialAssetRefs()[0].IsValid());
EXPECT_EQ(component.GetMaterialAssetRefs()[0].assetGuid, materialRef.assetGuid);
EXPECT_EQ(component.GetMaterialAssetRefs()[0].localID, materialRef.localID);
EXPECT_EQ(component.GetMaterialAssetRefs()[0].resourceType, materialRef.resourceType);
EXPECT_EQ(component.GetMaterialPath(0), "Assets/runtime.material");
ASSERT_NE(component.GetMaterial(0), nullptr);
std::stringstream stream;
component.Serialize(stream);
EXPECT_NE(stream.str().find("materialRefs="), std::string::npos);
EXPECT_EQ(stream.str().find("materialRefs=;"), std::string::npos);
manager.SetResourceRoot("");
manager.Shutdown();
fs::remove_all(projectRoot);
}
TEST(MeshRendererComponent_Test, DeserializeIgnoresPlainMaterialPathsWithoutAssetRefs) {
MeshRendererComponent target;
std::stringstream stream(
"materialPaths=Materials/legacy0.mat|;materialRefs=|;castShadows=0;receiveShadows=1;renderLayer=5;");
target.Deserialize(stream);
ASSERT_EQ(target.GetMaterialCount(), 2u);
EXPECT_EQ(target.GetMaterialPath(0), "");
EXPECT_EQ(target.GetMaterialPath(1), "");
EXPECT_EQ(target.GetMaterial(0), nullptr);
EXPECT_EQ(target.GetMaterial(1), nullptr);
EXPECT_FALSE(target.GetCastShadows());
EXPECT_TRUE(target.GetReceiveShadows());
EXPECT_EQ(target.GetRenderLayer(), 5u);
}
TEST(MeshRendererComponent_Test, SerializeAndDeserializeLoadsProjectMaterialByAssetRef) {
namespace fs = std::filesystem;
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_renderer_asset_ref_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 << " \"renderState\": {\n";
materialFile << " \"cull\": \"back\"\n";
materialFile << " }\n";
materialFile << "}";
}
manager.SetResourceRoot(projectRoot.string().c_str());
MeshRendererComponent source;
source.SetMaterialPath(0, "Assets/runtime.material");
ASSERT_EQ(source.GetMaterialCount(), 1u);
ASSERT_NE(source.GetMaterial(0), nullptr);
ASSERT_EQ(source.GetMaterialPath(0), "Assets/runtime.material");
ASSERT_EQ(source.GetMaterialAssetRefs().size(), 1u);
EXPECT_TRUE(source.GetMaterialAssetRefs()[0].IsValid());
std::stringstream stream;
source.Serialize(stream);
const std::string serialized = stream.str();
EXPECT_NE(serialized.find("materialPaths=;"), std::string::npos);
EXPECT_NE(serialized.find("materialRefs="), std::string::npos);
EXPECT_EQ(serialized.find("materialRefs=;"), std::string::npos);
std::stringstream deserializeStream(serialized);
MeshRendererComponent target;
target.Deserialize(deserializeStream);
ASSERT_EQ(target.GetMaterialCount(), 1u);
EXPECT_EQ(target.GetMaterialPath(0), "Assets/runtime.material");
ASSERT_NE(target.GetMaterial(0), nullptr);
EXPECT_TRUE(target.GetMaterialAssetRefs()[0].IsValid());
manager.SetResourceRoot("");
manager.Shutdown();
fs::remove_all(projectRoot);
}
TEST(MeshRendererComponent_Test, DeferredSceneDeserializeLoadsProjectMaterialAsync) {
namespace fs = std::filesystem;
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_renderer_async_asset_ref_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 << " \"renderState\": {\n";
materialFile << " \"cull\": \"back\"\n";
materialFile << " }\n";
materialFile << "}";
}
manager.SetResourceRoot(projectRoot.string().c_str());
MeshRendererComponent source;
source.SetMaterialPath(0, "Assets/runtime.material");
std::stringstream serializedStream;
source.Serialize(serializedStream);
MeshRendererComponent target;
const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount();
{
ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled());
std::stringstream deserializeStream(serializedStream.str());
target.Deserialize(deserializeStream);
EXPECT_EQ(manager.GetAsyncPendingCount(), pendingBeforeDeserialize);
}
ASSERT_EQ(target.GetMaterialCount(), 1u);
EXPECT_EQ(target.GetMaterialPath(0), "Assets/runtime.material");
EXPECT_EQ(target.GetMaterial(0), nullptr);
EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
ASSERT_NE(target.GetMaterial(0), nullptr);
EXPECT_EQ(target.GetMaterialPath(0), "Assets/runtime.material");
EXPECT_TRUE(target.GetMaterialAssetRefs()[0].IsValid());
manager.SetResourceRoot("");
manager.Shutdown();
fs::remove_all(projectRoot);
}
} // namespace

View File

@@ -1,83 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/BoxColliderComponent.h>
#include <XCEngine/Components/CapsuleColliderComponent.h>
#include <XCEngine/Components/RigidbodyComponent.h>
#include <XCEngine/Components/SphereColliderComponent.h>
#include <XCEngine/Physics/PhysicsTypes.h>
#include <sstream>
using namespace XCEngine::Components;
namespace {
TEST(PhysicsComponents_Test, Rigidbody_SerializeRoundTripPreservesFields) {
RigidbodyComponent source;
source.SetBodyType(XCEngine::Physics::PhysicsBodyType::Kinematic);
source.SetMass(3.5f);
source.SetLinearDamping(0.2f);
source.SetAngularDamping(0.8f);
source.SetLinearVelocity(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
source.SetAngularVelocity(XCEngine::Math::Vector3(-4.0f, 5.0f, -6.0f));
source.SetUseGravity(false);
source.SetEnableCCD(true);
std::stringstream stream;
source.Serialize(stream);
RigidbodyComponent target;
target.Deserialize(stream);
EXPECT_EQ(target.GetBodyType(), XCEngine::Physics::PhysicsBodyType::Kinematic);
EXPECT_FLOAT_EQ(target.GetMass(), 3.5f);
EXPECT_FLOAT_EQ(target.GetLinearDamping(), 0.2f);
EXPECT_FLOAT_EQ(target.GetAngularDamping(), 0.8f);
EXPECT_EQ(target.GetLinearVelocity(), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
EXPECT_EQ(target.GetAngularVelocity(), XCEngine::Math::Vector3(-4.0f, 5.0f, -6.0f));
EXPECT_FALSE(target.GetUseGravity());
EXPECT_TRUE(target.GetEnableCCD());
}
TEST(PhysicsComponents_Test, BoxCollider_SerializeRoundTripPreservesFields) {
BoxColliderComponent source;
source.SetTrigger(true);
source.SetCenter(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
source.SetStaticFriction(0.7f);
source.SetDynamicFriction(0.2f);
source.SetRestitution(0.4f);
source.SetSize(XCEngine::Math::Vector3(2.0f, 4.0f, 6.0f));
std::stringstream stream;
source.Serialize(stream);
BoxColliderComponent target;
target.Deserialize(stream);
EXPECT_TRUE(target.IsTrigger());
EXPECT_EQ(target.GetCenter(), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
EXPECT_FLOAT_EQ(target.GetStaticFriction(), 0.7f);
EXPECT_FLOAT_EQ(target.GetDynamicFriction(), 0.2f);
EXPECT_FLOAT_EQ(target.GetRestitution(), 0.4f);
EXPECT_EQ(target.GetSize(), XCEngine::Math::Vector3(2.0f, 4.0f, 6.0f));
}
TEST(PhysicsComponents_Test, SphereCollider_InvalidRadiusIsSanitized) {
SphereColliderComponent sphere;
sphere.SetRadius(-10.0f);
EXPECT_GT(sphere.GetRadius(), 0.0f);
}
TEST(PhysicsComponents_Test, CapsuleCollider_HeightStaysAtLeastDiameter) {
CapsuleColliderComponent capsule;
capsule.SetRadius(1.25f);
capsule.SetHeight(1.0f);
capsule.SetAxis(ColliderAxis::Z);
EXPECT_FLOAT_EQ(capsule.GetRadius(), 1.25f);
EXPECT_FLOAT_EQ(capsule.GetHeight(), 2.5f);
EXPECT_EQ(capsule.GetAxis(), ColliderAxis::Z);
}
} // namespace

View File

@@ -1,333 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/TransformComponent.h>
#include <XCEngine/Core/Math/Math.h>
using namespace XCEngine::Components;
using namespace XCEngine::Math;
namespace {
class TransformComponentTest : public ::testing::Test {
protected:
void SetUp() override {
transform = std::make_unique<TransformComponent>();
}
std::unique_ptr<TransformComponent> transform;
};
TEST(TransformComponent_Test, DefaultConstructor_IdentityValues) {
TransformComponent tc;
EXPECT_EQ(tc.GetLocalPosition(), Vector3::Zero());
EXPECT_TRUE(tc.GetLocalRotation().Dot(Quaternion::Identity()) > 0.99f);
EXPECT_EQ(tc.GetLocalScale(), Vector3::One());
}
TEST(TransformComponent_Test, LocalPosition_GetSet) {
TransformComponent tc;
Vector3 pos(1.0f, 2.0f, 3.0f);
tc.SetLocalPosition(pos);
EXPECT_EQ(tc.GetLocalPosition(), pos);
}
TEST(TransformComponent_Test, LocalRotation_GetSet) {
TransformComponent tc;
Quaternion rot = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f);
tc.SetLocalRotation(rot);
EXPECT_TRUE(tc.GetLocalRotation().Dot(rot) > 0.99f);
}
TEST(TransformComponent_Test, LocalScale_GetSet) {
TransformComponent tc;
Vector3 scale(2.0f, 2.0f, 2.0f);
tc.SetLocalScale(scale);
EXPECT_EQ(tc.GetLocalScale(), scale);
}
TEST(TransformComponent_Test, LocalEulerAngles_GetSet) {
TransformComponent tc;
Vector3 eulers(45.0f, 30.0f, 60.0f);
tc.SetLocalEulerAngles(eulers);
Vector3 result = tc.GetLocalEulerAngles();
EXPECT_TRUE(result.Magnitude() > 0.0f);
}
TEST(TransformComponent_Test, WorldPosition_NoParent_EqualsLocal) {
TransformComponent tc;
Vector3 pos(1.0f, 2.0f, 3.0f);
tc.SetLocalPosition(pos);
EXPECT_EQ(tc.GetPosition(), pos);
}
TEST(TransformComponent_Test, WorldPosition_WithParent) {
TransformComponent parent;
TransformComponent child;
parent.SetLocalPosition(Vector3(1.0f, 0.0f, 0.0f));
child.SetParent(&parent);
child.SetLocalPosition(Vector3(2.0f, 0.0f, 0.0f));
Vector3 worldPos = child.GetPosition();
EXPECT_NEAR(worldPos.x, 3.0f, 0.001f);
}
TEST(TransformComponent_Test, WorldRotation_WithParent) {
TransformComponent parent;
TransformComponent child;
parent.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f));
child.SetParent(&parent);
child.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f));
Quaternion worldRot = child.GetRotation();
EXPECT_TRUE(worldRot.Magnitude() > 0.0f);
}
TEST(TransformComponent_Test, WorldRotation_WithScaledParentPreservesRotation) {
TransformComponent parent;
TransformComponent child;
parent.SetLocalScale(Vector3(0.38912f, 0.38912f, 0.38912f));
parent.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.25f));
child.SetParent(&parent);
child.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Right(), PI * 0.125f));
const Quaternion expectedWorldRotation = parent.GetLocalRotation() * child.GetLocalRotation();
const Quaternion actualWorldRotation = child.GetRotation();
EXPECT_GT(std::abs(actualWorldRotation.Dot(expectedWorldRotation)), 0.999f);
}
TEST(TransformComponent_Test, SetWorldRotation_WithScaledParentPreservesTargetRotation) {
TransformComponent parent;
TransformComponent child;
parent.SetLocalScale(Vector3(0.38912f, 0.38912f, 0.38912f));
child.SetParent(&parent);
const Quaternion targetWorldRotation = Quaternion::FromAxisAngle(Vector3::Forward(), PI * 0.33f);
child.SetRotation(targetWorldRotation);
const Quaternion actualWorldRotation = child.GetRotation();
EXPECT_GT(std::abs(actualWorldRotation.Dot(targetWorldRotation)), 0.999f);
}
TEST(TransformComponent_Test, WorldScale_WithParent) {
TransformComponent parent;
TransformComponent child;
parent.SetLocalScale(Vector3(2.0f, 2.0f, 2.0f));
child.SetParent(&parent);
child.SetLocalScale(Vector3(2.0f, 2.0f, 2.0f));
Vector3 worldScale = child.GetScale();
EXPECT_NEAR(worldScale.x, 4.0f, 0.001f);
EXPECT_NEAR(worldScale.y, 4.0f, 0.001f);
EXPECT_NEAR(worldScale.z, 4.0f, 0.001f);
}
TEST(TransformComponent_Test, DirectionVectors_Forward) {
TransformComponent tc;
Vector3 forward = tc.GetForward();
EXPECT_NEAR(forward.z, 1.0f, 0.001f);
}
TEST(TransformComponent_Test, DirectionVectors_Right) {
TransformComponent tc;
Vector3 right = tc.GetRight();
EXPECT_NEAR(right.x, 1.0f, 0.001f);
}
TEST(TransformComponent_Test, DirectionVectors_Up) {
TransformComponent tc;
Vector3 up = tc.GetUp();
EXPECT_NEAR(up.y, 1.0f, 0.001f);
}
TEST(TransformComponent_Test, LocalToWorldMatrix_Identity) {
TransformComponent tc;
const Matrix4x4& matrix = tc.GetLocalToWorldMatrix();
EXPECT_EQ(matrix[0][0], 1.0f);
EXPECT_EQ(matrix[1][1], 1.0f);
EXPECT_EQ(matrix[2][2], 1.0f);
EXPECT_EQ(matrix[3][3], 1.0f);
}
TEST(TransformComponent_Test, LocalToWorldMatrix_Translation) {
TransformComponent tc;
Vector3 pos(5.0f, 10.0f, 15.0f);
tc.SetLocalPosition(pos);
const Matrix4x4& matrix = tc.GetLocalToWorldMatrix();
EXPECT_NEAR(matrix[0][3], pos.x, 0.001f);
EXPECT_NEAR(matrix[1][3], pos.y, 0.001f);
EXPECT_NEAR(matrix[2][3], pos.z, 0.001f);
}
TEST(TransformComponent_Test, LocalToWorldMatrix_Rotation) {
TransformComponent tc;
tc.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f));
const Matrix4x4& matrix = tc.GetLocalToWorldMatrix();
EXPECT_NEAR(matrix[0][0], 0.0f, 0.001f);
EXPECT_NEAR(matrix[0][2], 1.0f, 0.001f);
}
TEST(TransformComponent_Test, LookAt_Target) {
TransformComponent tc;
Vector3 target(10.0f, 0.0f, 0.0f);
tc.SetPosition(Vector3::Zero());
tc.LookAt(target);
Vector3 forward = tc.GetForward();
EXPECT_TRUE(forward.Magnitude() > 0.9f);
}
TEST(TransformComponent_Test, Rotate_Eulers) {
TransformComponent tc;
tc.Rotate(Vector3(90.0f, 0.0f, 0.0f));
Vector3 eulers = tc.GetLocalEulerAngles();
EXPECT_TRUE(eulers.Magnitude() > 0.0f);
}
TEST(TransformComponent_Test, Translate_Self) {
TransformComponent tc;
Vector3 initialPos = tc.GetLocalPosition();
tc.Translate(Vector3(1.0f, 2.0f, 3.0f), Space::Self);
Vector3 newPos = tc.GetLocalPosition();
EXPECT_NEAR(newPos.x, initialPos.x + 1.0f, 0.001f);
EXPECT_NEAR(newPos.y, initialPos.y + 2.0f, 0.001f);
EXPECT_NEAR(newPos.z, initialPos.z + 3.0f, 0.001f);
}
TEST(TransformComponent_Test, TransformPoint_LocalToWorld) {
TransformComponent tc;
tc.SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f));
Vector3 localPoint(1.0f, 0.0f, 0.0f);
Vector3 worldPoint = tc.TransformPoint(localPoint);
EXPECT_NEAR(worldPoint.x, 2.0f, 0.001f);
EXPECT_NEAR(worldPoint.y, 2.0f, 0.001f);
EXPECT_NEAR(worldPoint.z, 3.0f, 0.001f);
}
TEST(TransformComponent_Test, InverseTransformPoint_WorldToLocal) {
TransformComponent tc;
tc.SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f));
Vector3 worldPoint(2.0f, 2.0f, 3.0f);
Vector3 localPoint = tc.InverseTransformPoint(worldPoint);
EXPECT_NEAR(localPoint.x, 1.0f, 0.001f);
EXPECT_NEAR(localPoint.y, 0.0f, 0.001f);
EXPECT_NEAR(localPoint.z, 0.0f, 0.001f);
}
TEST(TransformComponent_Test, TransformDirection) {
TransformComponent tc;
tc.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f));
Vector3 localDir(1.0f, 0.0f, 0.0f);
Vector3 worldDir = tc.TransformDirection(localDir);
EXPECT_NEAR(worldDir.z, -1.0f, 0.1f);
}
TEST(TransformComponent_Test, SetDirty_PropagatesToChildren) {
TransformComponent parent;
TransformComponent child;
child.SetParent(&parent);
parent.SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f));
child.SetLocalPosition(Vector3(1.0f, 0.0f, 0.0f));
Vector3 childWorldPosBefore = child.GetPosition();
parent.SetLocalPosition(Vector3(10.0f, 0.0f, 0.0f));
Vector3 childWorldPosAfter = child.GetPosition();
EXPECT_NE(childWorldPosBefore.x, childWorldPosAfter.x);
}
TEST(TransformComponent_Test, GetChildCount_Empty) {
TransformComponent tc;
EXPECT_EQ(tc.GetChildCount(), 0u);
}
TEST(TransformComponent_Test, GetChild_InvalidIndex) {
TransformComponent tc;
EXPECT_EQ(tc.GetChild(0), nullptr);
}
TEST(TransformComponent_Test, DetachChildren) {
TransformComponent parent;
TransformComponent child1;
TransformComponent child2;
child1.SetParent(&parent);
child2.SetParent(&parent);
EXPECT_EQ(parent.GetChildCount(), 2u);
parent.DetachChildren();
EXPECT_EQ(parent.GetChildCount(), 0u);
}
TEST(TransformComponent_Test, SetAsFirstSibling) {
TransformComponent parent;
TransformComponent child1;
TransformComponent child2;
child1.SetParent(&parent);
child2.SetParent(&parent);
child2.SetAsFirstSibling();
EXPECT_EQ(child2.GetSiblingIndex(), 0);
}
TEST(TransformComponent_Test, SetAsLastSibling) {
TransformComponent parent;
TransformComponent child1;
TransformComponent child2;
child1.SetParent(&parent);
child2.SetParent(&parent);
child1.SetAsLastSibling();
EXPECT_EQ(child1.GetSiblingIndex(), 1);
}
} // namespace

View File

@@ -1,171 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/VolumeRendererComponent.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Core/Math/Bounds.h>
#include <XCEngine/Resources/Material/Material.h>
#include <XCEngine/Resources/Volume/VolumeField.h>
#include <chrono>
#include <sstream>
#include <thread>
using namespace XCEngine::Components;
using namespace XCEngine::Math;
using namespace XCEngine::Resources;
namespace {
VolumeField* CreateTestVolumeField(const char* name, const char* path) {
auto* volumeField = new VolumeField();
IResource::ConstructParams params = {};
params.name = name;
params.path = path;
params.guid = ResourceGUID::Generate(path);
volumeField->Initialize(params);
const unsigned char payload[8] = { 1, 3, 5, 7, 9, 11, 13, 15 };
EXPECT_TRUE(volumeField->Create(
VolumeStorageKind::NanoVDB,
payload,
sizeof(payload),
Bounds(Vector3::Zero(), Vector3::One()),
Vector3(0.5f, 0.5f, 0.5f)));
return volumeField;
}
Material* CreateTestMaterial(const char* name, const char* path) {
auto* material = new Material();
IResource::ConstructParams params = {};
params.name = name;
params.path = path;
params.guid = ResourceGUID::Generate(path);
material->Initialize(params);
return material;
}
class FakeAsyncVolumeFieldLoader : public IResourceLoader {
public:
ResourceType GetResourceType() const override { return ResourceType::VolumeField; }
XCEngine::Containers::Array<XCEngine::Containers::String> GetSupportedExtensions() const override {
XCEngine::Containers::Array<XCEngine::Containers::String> extensions;
extensions.PushBack("nvdb");
return extensions;
}
bool CanLoad(const XCEngine::Containers::String& path) const override {
(void)path;
return true;
}
LoadResult Load(
const XCEngine::Containers::String& path,
const ImportSettings* settings = nullptr) override {
(void)settings;
return LoadResult(CreateTestVolumeField("AsyncVolume", path.CStr()));
}
ImportSettings* GetDefaultSettings() const override {
return nullptr;
}
};
bool PumpAsyncLoadsUntilIdle(
ResourceManager& manager,
std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (manager.IsAsyncLoading() && std::chrono::steady_clock::now() < deadline) {
manager.UpdateAsyncLoads();
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
manager.UpdateAsyncLoads();
return !manager.IsAsyncLoading();
}
TEST(VolumeRendererComponent_Test, SetResourcesCachesHandlesPathsAndFlags) {
GameObject gameObject("VolumeHolder");
auto* component = gameObject.AddComponent<VolumeRendererComponent>();
VolumeField* volumeField = CreateTestVolumeField("Cloud", "Volumes/cloud.nvdb");
Material* material = CreateTestMaterial("VolumeMaterial", "Materials/volume.mat");
component->SetVolumeField(volumeField);
component->SetMaterial(material);
component->SetCastShadows(false);
component->SetReceiveShadows(false);
EXPECT_EQ(component->GetVolumeField(), volumeField);
EXPECT_EQ(component->GetVolumeFieldPath(), "Volumes/cloud.nvdb");
EXPECT_EQ(component->GetMaterial(), material);
EXPECT_EQ(component->GetMaterialPath(), "Materials/volume.mat");
EXPECT_FALSE(component->GetCastShadows());
EXPECT_FALSE(component->GetReceiveShadows());
component->ClearVolumeField();
component->ClearMaterial();
delete volumeField;
delete material;
}
TEST(VolumeRendererComponent_Test, SerializeAndDeserializePreservesVirtualPathsAndFlags) {
VolumeRendererComponent source;
source.SetVolumeFieldPath("test://volumes/cloud.nvdb");
source.SetMaterialPath("test://materials/volume.material");
source.SetCastShadows(false);
source.SetReceiveShadows(true);
std::stringstream stream;
source.Serialize(stream);
const std::string serialized = stream.str();
EXPECT_NE(serialized.find("volumePath=test://volumes/cloud.nvdb;"), std::string::npos);
EXPECT_NE(serialized.find("materialPath=test://materials/volume.material;"), std::string::npos);
EXPECT_NE(serialized.find("castShadows=0;"), std::string::npos);
EXPECT_NE(serialized.find("receiveShadows=1;"), std::string::npos);
VolumeRendererComponent target;
std::stringstream deserializeStream(serialized);
target.Deserialize(deserializeStream);
EXPECT_EQ(target.GetVolumeFieldPath(), "test://volumes/cloud.nvdb");
EXPECT_EQ(target.GetMaterialPath(), "test://materials/volume.material");
EXPECT_FALSE(target.GetCastShadows());
EXPECT_TRUE(target.GetReceiveShadows());
EXPECT_FALSE(target.GetVolumeFieldAssetRef().IsValid());
EXPECT_FALSE(target.GetMaterialAssetRef().IsValid());
}
TEST(VolumeRendererComponent_Test, DeferredSceneDeserializeLoadsVolumeFieldAsyncByPath) {
ResourceManager& manager = ResourceManager::Get();
manager.Initialize();
IResourceLoader* originalLoader = manager.GetLoader(ResourceType::VolumeField);
FakeAsyncVolumeFieldLoader fakeLoader;
manager.RegisterLoader(&fakeLoader);
VolumeRendererComponent target;
const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount();
{
ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled());
std::stringstream stream(
"volumePath=test://volumes/async.nvdb;volumeRef=;materialRef=;castShadows=1;receiveShadows=1;");
target.Deserialize(stream);
}
EXPECT_EQ(target.GetVolumeFieldPath(), "test://volumes/async.nvdb");
EXPECT_EQ(target.GetVolumeField(), nullptr);
EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
ASSERT_NE(target.GetVolumeField(), nullptr);
EXPECT_EQ(target.GetVolumeFieldPath(), "test://volumes/async.nvdb");
EXPECT_EQ(target.GetVolumeField()->GetStorageKind(), VolumeStorageKind::NanoVDB);
manager.RegisterLoader(originalLoader);
manager.Shutdown();
}
} // namespace

View File

@@ -1,96 +0,0 @@
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#include <mmsystem.h>
#endif
#define private public
#include <XCEngine/Audio/WindowsAudioBackend.h>
#undef private
using namespace XCEngine::Audio;
namespace {
#ifdef _WIN32
TEST(WaveOutBackend, RenderCallbackCanDrivePcm16ConversionForPullModel) {
WaveOut::WaveOutBackend backend;
backend.m_config.bufferSize = 2;
backend.m_config.channels = 2;
backend.m_config.sampleRate = 48000;
bool callbackInvoked = false;
backend.SetRenderCallback(
[&callbackInvoked](float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate) {
callbackInvoked = true;
EXPECT_EQ(frameCount, 2u);
EXPECT_EQ(channels, 2u);
EXPECT_EQ(sampleRate, 48000u);
buffer[0] = 0.25f;
buffer[1] = -0.5f;
buffer[2] = 0.75f;
buffer[3] = -1.0f;
});
backend.m_renderBuffer.assign(4, 0.0f);
ASSERT_TRUE(static_cast<bool>(backend.m_renderCallback));
backend.m_renderCallback(backend.m_renderBuffer.data(), 2, 2, 48000);
std::vector<int16_t> output(4, 0);
WaveOut::WaveOutBackend::FillPcm16Buffer(output, backend.m_renderBuffer);
EXPECT_TRUE(backend.UsesPullModel());
EXPECT_TRUE(callbackInvoked);
ASSERT_EQ(output.size(), 4u);
EXPECT_EQ(output[0], 8191);
EXPECT_EQ(output[1], -16383);
EXPECT_EQ(output[2], 24575);
EXPECT_EQ(output[3], -32767);
}
TEST(WaveOutBackend, SetDeviceAcceptsDefaultDeviceBeforeInitialize) {
WaveOut::WaveOutBackend backend;
EXPECT_TRUE(backend.SetDevice("Default Device"));
EXPECT_EQ(backend.m_deviceName, "Default Device");
EXPECT_TRUE(backend.m_requestedDeviceName.empty());
}
TEST(WaveOutBackend, SetDeviceRejectsHotSwitchAfterOpen) {
WaveOut::WaveOutBackend backend;
backend.m_deviceName = "Default Device";
backend.m_hWaveOut = reinterpret_cast<HWAVEOUT>(1);
EXPECT_FALSE(backend.SetDevice("Some Other Device"));
EXPECT_EQ(backend.m_deviceName, "Default Device");
backend.m_hWaveOut = nullptr;
}
TEST(WaveOutBackend, FillPcm16BufferClampsSamplesToPcm16Range) {
std::vector<float> input = {-2.0f, -0.5f, 0.5f, 2.0f};
std::vector<int16_t> output(4, 123);
WaveOut::WaveOutBackend::FillPcm16Buffer(output, input);
ASSERT_EQ(output.size(), 4u);
EXPECT_EQ(output[0], -32767);
EXPECT_EQ(output[1], -16383);
EXPECT_EQ(output[2], 16383);
EXPECT_EQ(output[3], 32767);
}
#endif
} // namespace

View File

@@ -1,55 +0,0 @@
# ============================================================
# Core/Asset Tests
# ============================================================
set(ASSET_TEST_SOURCES
test_artifact_container.cpp
test_iresource.cpp
test_resource_types.cpp
test_resource_guid.cpp
test_resource_handle.cpp
test_resource_manager.cpp
test_resource_cache.cpp
test_resource_dependency.cpp
test_shader_compilation_cache.cpp
)
add_executable(asset_tests ${ASSET_TEST_SOURCES})
if(MSVC)
set_target_properties(asset_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
endif()
target_link_libraries(asset_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(asset_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/tests/Fixtures
)
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
)

View File

@@ -1,113 +0,0 @@
#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;
}

View File

@@ -1,147 +0,0 @@
#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);
}

View File

@@ -1,69 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
namespace {
class TestResource : public IResource {
public:
ResourceType GetType() const override { return ResourceType::Texture; }
const String& GetName() const override { return m_name; }
const String& GetPath() const override { return m_path; }
ResourceGUID GetGUID() const override { return m_guid; }
bool IsValid() const override { return m_isValid; }
size_t GetMemorySize() const override { return m_memorySize; }
void Release() override { delete this; }
};
TEST(IResource, Initialize) {
TestResource* resource = new TestResource();
IResource::ConstructParams params;
params.name = "TestTexture";
params.path = "textures/test.png";
params.guid = ResourceGUID(12345);
params.memorySize = 1024;
resource->Initialize(params);
EXPECT_EQ(resource->GetName(), "TestTexture");
EXPECT_EQ(resource->GetPath(), "textures/test.png");
EXPECT_EQ(resource->GetGUID().value, 12345u);
EXPECT_EQ(resource->GetMemorySize(), 1024u);
EXPECT_TRUE(resource->IsValid());
resource->Release();
}
TEST(IResource, SetInvalid) {
TestResource* resource = new TestResource();
IResource::ConstructParams params;
params.name = "Test";
params.path = "test.png";
params.guid = ResourceGUID(1);
params.memorySize = 100;
resource->Initialize(params);
EXPECT_TRUE(resource->IsValid());
resource->SetInvalid();
EXPECT_FALSE(resource->IsValid());
resource->Release();
}
TEST(IResource, DefaultValues) {
TestResource* resource = new TestResource();
EXPECT_EQ(resource->GetMemorySize(), 0u);
EXPECT_FALSE(resource->IsValid());
resource->Release();
}
} // namespace

View File

@@ -1,147 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceCache.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
namespace {
class TestResource : public IResource {
public:
ResourceType GetType() const override { return ResourceType::Texture; }
const String& GetName() const override { return m_name; }
const String& GetPath() const override { return m_path; }
ResourceGUID GetGUID() const override { return m_guid; }
bool IsValid() const override { return m_isValid; }
size_t GetMemorySize() const override { return m_memorySize; }
void Release() override { delete this; }
};
TEST(ResourceCache, AddAndFind) {
ResourceCache cache;
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 1024 });
ResourceGUID guid(100);
cache.Add(guid, resource);
IResource* found = cache.Find(guid);
EXPECT_EQ(found, resource);
cache.Remove(guid);
found = cache.Find(guid);
EXPECT_EQ(found, nullptr);
}
TEST(ResourceCache, GetSize) {
ResourceCache cache;
EXPECT_EQ(cache.GetSize(), 0u);
TestResource* resource1 = new TestResource();
resource1->Initialize({ "Test1", "test1.png", ResourceGUID(1), 100 });
cache.Add(ResourceGUID(1), resource1);
EXPECT_EQ(cache.GetSize(), 1u);
TestResource* resource2 = new TestResource();
resource2->Initialize({ "Test2", "test2.png", ResourceGUID(2), 200 });
cache.Add(ResourceGUID(2), resource2);
EXPECT_EQ(cache.GetSize(), 2u);
}
TEST(ResourceCache, GetMemoryUsage) {
ResourceCache cache;
EXPECT_EQ(cache.GetMemoryUsage(), 0u);
TestResource* resource1 = new TestResource();
resource1->Initialize({ "Test1", "test1.png", ResourceGUID(1), 100 });
cache.Add(ResourceGUID(1), resource1);
EXPECT_EQ(cache.GetMemoryUsage(), 100u);
TestResource* resource2 = new TestResource();
resource2->Initialize({ "Test2", "test2.png", ResourceGUID(2), 200 });
cache.Add(ResourceGUID(2), resource2);
EXPECT_EQ(cache.GetMemoryUsage(), 300u);
}
TEST(ResourceCache, SetMemoryBudget) {
ResourceCache cache;
EXPECT_EQ(cache.GetMemoryBudget(), 512 * 1024 * 1024u);
cache.SetMemoryBudget(1024);
EXPECT_EQ(cache.GetMemoryBudget(), 1024u);
}
TEST(ResourceCache, Touch) {
ResourceCache cache;
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 100 });
ResourceGUID guid(100);
cache.Add(guid, resource);
cache.Touch(guid);
IResource* found = cache.Find(guid);
EXPECT_EQ(found, resource);
}
TEST(ResourceCache, Clear) {
ResourceCache cache;
TestResource* resource1 = new TestResource();
resource1->Initialize({ "Test1", "test1.png", ResourceGUID(1), 100 });
cache.Add(ResourceGUID(1), resource1);
TestResource* resource2 = new TestResource();
resource2->Initialize({ "Test2", "test2.png", ResourceGUID(2), 200 });
cache.Add(ResourceGUID(2), resource2);
EXPECT_EQ(cache.GetSize(), 2u);
cache.Clear();
EXPECT_EQ(cache.GetSize(), 0u);
EXPECT_EQ(cache.GetMemoryUsage(), 0u);
}
TEST(ResourceCache, OnMemoryPressure) {
ResourceCache cache;
cache.SetMemoryBudget(150);
TestResource* resource1 = new TestResource();
resource1->Initialize({ "Test1", "test1.png", ResourceGUID(1), 100 });
cache.Add(ResourceGUID(1), resource1);
EXPECT_EQ(cache.GetMemoryUsage(), 100u);
TestResource* resource2 = new TestResource();
resource2->Initialize({ "Test2", "test2.png", ResourceGUID(2), 100 });
cache.Add(ResourceGUID(2), resource2);
EXPECT_EQ(cache.GetMemoryUsage(), 200u);
cache.OnMemoryPressure(50);
EXPECT_LE(cache.GetMemoryUsage(), 150u);
}
TEST(ResourceCache, GetLRUList) {
ResourceCache cache;
TestResource* resource1 = new TestResource();
resource1->Initialize({ "Test1", "test1.png", ResourceGUID(1), 100 });
cache.Add(ResourceGUID(1), resource1);
TestResource* resource2 = new TestResource();
resource2->Initialize({ "Test2", "test2.png", ResourceGUID(2), 200 });
cache.Add(ResourceGUID(2), resource2);
auto lruList = cache.GetLRUList(2);
EXPECT_GE(lruList.Size(), 0u);
}
} // namespace

View File

@@ -1,131 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceDependencyGraph.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
namespace {
TEST(ResourceDependencyGraph, DefaultConstructor) {
ResourceDependencyGraph graph;
}
TEST(ResourceDependencyGraph, AddNode) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Texture);
EXPECT_TRUE(graph.HasNode(ResourceGUID(1)));
}
TEST(ResourceDependencyGraph, AddMultipleNodes) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Texture);
graph.AddNode(ResourceGUID(2), ResourceType::Mesh);
graph.AddNode(ResourceGUID(3), ResourceType::Material);
EXPECT_TRUE(graph.HasNode(ResourceGUID(1)));
EXPECT_TRUE(graph.HasNode(ResourceGUID(2)));
EXPECT_TRUE(graph.HasNode(ResourceGUID(3)));
}
TEST(ResourceDependencyGraph, RemoveNode) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Texture);
EXPECT_TRUE(graph.HasNode(ResourceGUID(1)));
graph.RemoveNode(ResourceGUID(1));
EXPECT_FALSE(graph.HasNode(ResourceGUID(1)));
}
TEST(ResourceDependencyGraph, AddDependency) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Material);
graph.AddNode(ResourceGUID(2), ResourceType::Texture);
graph.AddDependency(ResourceGUID(1), ResourceGUID(2));
auto deps = graph.GetDependencies(ResourceGUID(1));
EXPECT_EQ(deps.Size(), 1u);
EXPECT_EQ(deps[0], ResourceGUID(2));
}
TEST(ResourceDependencyGraph, GetDependents) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Material);
graph.AddNode(ResourceGUID(2), ResourceType::Texture);
graph.AddDependency(ResourceGUID(1), ResourceGUID(2));
auto dependents = graph.GetDependents(ResourceGUID(2));
EXPECT_EQ(dependents.Size(), 1u);
EXPECT_EQ(dependents[0], ResourceGUID(1));
}
TEST(ResourceDependencyGraph, GetAllDependencies) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Material);
graph.AddNode(ResourceGUID(2), ResourceType::Texture);
graph.AddNode(ResourceGUID(3), ResourceType::Shader);
graph.AddDependency(ResourceGUID(1), ResourceGUID(2));
graph.AddDependency(ResourceGUID(1), ResourceGUID(3));
auto allDeps = graph.GetAllDependencies(ResourceGUID(1));
EXPECT_EQ(allDeps.Size(), 2u);
}
TEST(ResourceDependencyGraph, RefCount) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Texture);
EXPECT_EQ(graph.GetRefCount(ResourceGUID(1)), 0u);
graph.IncrementRefCount(ResourceGUID(1));
EXPECT_EQ(graph.GetRefCount(ResourceGUID(1)), 1u);
graph.IncrementRefCount(ResourceGUID(1));
EXPECT_EQ(graph.GetRefCount(ResourceGUID(1)), 2u);
graph.DecrementRefCount(ResourceGUID(1));
EXPECT_EQ(graph.GetRefCount(ResourceGUID(1)), 1u);
}
TEST(ResourceDependencyGraph, Unload) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Texture);
EXPECT_TRUE(graph.Unload(ResourceGUID(1)));
graph.IncrementRefCount(ResourceGUID(1));
EXPECT_FALSE(graph.Unload(ResourceGUID(1)));
}
TEST(ResourceDependencyGraph, Clear) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Texture);
graph.AddNode(ResourceGUID(2), ResourceType::Mesh);
EXPECT_TRUE(graph.HasNode(ResourceGUID(1)));
EXPECT_TRUE(graph.HasNode(ResourceGUID(2)));
graph.Clear();
EXPECT_FALSE(graph.HasNode(ResourceGUID(1)));
EXPECT_FALSE(graph.HasNode(ResourceGUID(2)));
}
TEST(ResourceDependencyGraph, RemoveDependency) {
ResourceDependencyGraph graph;
graph.AddNode(ResourceGUID(1), ResourceType::Material);
graph.AddNode(ResourceGUID(2), ResourceType::Texture);
graph.AddDependency(ResourceGUID(1), ResourceGUID(2));
auto deps = graph.GetDependencies(ResourceGUID(1));
EXPECT_EQ(deps.Size(), 1u);
graph.RemoveDependency(ResourceGUID(1), ResourceGUID(2));
deps = graph.GetDependencies(ResourceGUID(1));
EXPECT_EQ(deps.Size(), 0u);
}
} // namespace

View File

@@ -1,64 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
namespace {
TEST(Resources_GUID, DefaultConstructor) {
ResourceGUID guid;
EXPECT_FALSE(guid.IsValid());
EXPECT_EQ(guid.value, 0);
}
TEST(Resources_GUID, ValueConstructor) {
ResourceGUID guid(12345);
EXPECT_TRUE(guid.IsValid());
EXPECT_EQ(guid.value, 12345);
}
TEST(Resources_GUID, Generate_FromCString) {
ResourceGUID guid1 = ResourceGUID::Generate("textures/player.png");
ResourceGUID guid2 = ResourceGUID::Generate("textures/player.png");
EXPECT_EQ(guid1, guid2);
EXPECT_TRUE(guid1.IsValid());
}
TEST(Resources_GUID, Generate_FromString) {
String path = "models/player.fbx";
ResourceGUID guid = ResourceGUID::Generate(path);
EXPECT_TRUE(guid.IsValid());
}
TEST(Resources_GUID, Generate_DifferentPaths) {
ResourceGUID guid1 = ResourceGUID::Generate("textures/a.png");
ResourceGUID guid2 = ResourceGUID::Generate("textures/b.png");
EXPECT_NE(guid1, guid2);
}
TEST(Resources_GUID, ToString) {
ResourceGUID guid(0x1234567890ABCDEF);
String str = guid.ToString();
EXPECT_EQ(str.Length(), 16u);
}
TEST(Resources_GUID, MakeResourceGUID_Helper) {
ResourceGUID guid = MakeResourceGUID("test/path");
EXPECT_TRUE(guid.IsValid());
}
TEST(Resources_GUID, EqualityOperators) {
ResourceGUID guid1(100);
ResourceGUID guid2(100);
ResourceGUID guid3(200);
EXPECT_TRUE(guid1 == guid2);
EXPECT_TRUE(guid1 != guid3);
}
} // namespace

View File

@@ -1,206 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceHandle.h>
#include <XCEngine/Core/Asset/IResource.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
namespace {
class TestResource : public IResource {
public:
ResourceType GetType() const override { return ResourceType::Texture; }
const String& GetName() const override { return m_name; }
const String& GetPath() const override { return m_path; }
ResourceGUID GetGUID() const override { return m_guid; }
bool IsValid() const override { return m_isValid; }
size_t GetMemorySize() const override { return m_memorySize; }
void Release() override {
m_released = true;
}
bool m_released = false;
};
TEST(ResourceHandle, DefaultConstructor) {
ResourceHandle<TestResource> handle;
EXPECT_FALSE(handle.IsValid());
EXPECT_EQ(handle.Get(), nullptr);
}
TEST(ResourceHandle, PointerConstructor) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 100 });
ResourceHandle<TestResource> handle(resource);
EXPECT_TRUE(handle.IsValid());
EXPECT_EQ(handle.Get(), resource);
EXPECT_EQ(handle->GetName(), "Test");
handle.Reset();
EXPECT_FALSE(handle.IsValid());
}
TEST(ResourceHandle, CopyConstructor) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 100 });
ResourceHandle<TestResource> handle1(resource);
ResourceHandle<TestResource> handle2(handle1);
EXPECT_TRUE(handle1.IsValid());
EXPECT_TRUE(handle2.IsValid());
EXPECT_EQ(handle1.Get(), handle2.Get());
handle1.Reset();
EXPECT_TRUE(handle2.IsValid());
handle2.Reset();
EXPECT_FALSE(handle1.IsValid());
EXPECT_FALSE(handle2.IsValid());
}
TEST(ResourceHandle, MoveConstructor) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 100 });
ResourceHandle<TestResource> handle1(resource);
ResourceHandle<TestResource> handle2(std::move(handle1));
EXPECT_FALSE(handle1.IsValid());
EXPECT_TRUE(handle2.IsValid());
EXPECT_EQ(handle2.Get(), resource);
handle2.Reset();
}
TEST(ResourceHandle, AssignmentOperator) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 100 });
ResourceHandle<TestResource> handle1(resource);
EXPECT_TRUE(handle1.IsValid());
ResourceHandle<TestResource> handle2;
handle2 = handle1;
EXPECT_TRUE(handle2.IsValid());
EXPECT_EQ(handle1.Get(), handle2.Get());
handle1.Reset();
EXPECT_TRUE(handle2.IsValid());
handle2.Reset();
}
TEST(ResourceHandle, MoveAssignment) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 100 });
ResourceHandle<TestResource> handle1(resource);
ResourceHandle<TestResource> handle2;
handle2 = std::move(handle1);
EXPECT_FALSE(handle1.IsValid());
EXPECT_TRUE(handle2.IsValid());
handle2.Reset();
}
TEST(ResourceHandle, GetGUID) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(12345), 100 });
ResourceHandle<TestResource> handle(resource);
EXPECT_EQ(handle.GetGUID().value, 12345u);
ResourceHandle<TestResource> emptyHandle;
EXPECT_EQ(emptyHandle.GetGUID().value, 0u);
handle.Reset();
}
TEST(ResourceHandle, GetResourceType) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 100 });
ResourceHandle<TestResource> handle(resource);
EXPECT_EQ(handle.GetResourceType(), ResourceType::Texture);
ResourceHandle<TestResource> emptyHandle;
EXPECT_EQ(emptyHandle.GetResourceType(), ResourceType::Unknown);
handle.Reset();
}
TEST(ResourceHandle, BoolOperator) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 100 });
ResourceHandle<TestResource> handle(resource);
if (handle) {
EXPECT_TRUE(true);
} else {
EXPECT_TRUE(false);
}
handle.Reset();
if (handle) {
EXPECT_TRUE(false);
} else {
EXPECT_TRUE(true);
}
}
TEST(ResourceHandle, Swap) {
TestResource* resource1 = new TestResource();
resource1->Initialize({ "Test1", "test1.png", ResourceGUID(100), 100 });
TestResource* resource2 = new TestResource();
resource2->Initialize({ "Test2", "test2.png", ResourceGUID(200), 200 });
ResourceHandle<TestResource> handle1(resource1);
ResourceHandle<TestResource> handle2(resource2);
handle1.Swap(handle2);
EXPECT_EQ(handle1.Get(), resource2);
EXPECT_EQ(handle2.Get(), resource1);
handle1.Reset();
handle2.Reset();
}
TEST(ResourceHandle, EqualityOperators) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(100), 100 });
ResourceHandle<TestResource> handle1(resource);
ResourceHandle<TestResource> handle2(handle1);
EXPECT_TRUE(handle1 == handle2);
EXPECT_FALSE(handle1 != handle2);
ResourceHandle<TestResource> handle3;
EXPECT_FALSE(handle1 == handle3);
EXPECT_TRUE(handle1 != handle3);
handle1.Reset();
handle2.Reset();
}
TEST(ResourceHandle, ResetDoesNotDereferenceDestroyedResourcePointer) {
TestResource* resource = new TestResource();
resource->Initialize({ "Test", "test.png", ResourceGUID(321), 100 });
ResourceHandle<TestResource> handle(resource);
delete resource;
handle.Reset();
EXPECT_EQ(handle.Get(), nullptr);
EXPECT_EQ(handle.GetGUID().value, 0u);
}
} // namespace

View File

@@ -1,62 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Types.h>
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
#include <XCEngine/Resources/Model/Model.h>
#include <XCEngine/Resources/UI/UIDocuments.h>
using namespace XCEngine::Resources;
namespace {
TEST(Resources_Types, ResourceType_EnumValues) {
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Unknown), 0);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Texture), 1);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Mesh), 2);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Material), 3);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Shader), 4);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::AudioClip), 5);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Binary), 6);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::AnimationClip), 7);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Skeleton), 8);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Font), 9);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::ParticleSystem), 10);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Scene), 11);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Prefab), 12);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UIView), 13);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UITheme), 14);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UISchema), 15);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::VolumeField), 16);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Model), 17);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::GaussianSplat), 18);
}
TEST(Resources_Types, GetResourceTypeName) {
EXPECT_STREQ(GetResourceTypeName(ResourceType::Texture), "Texture");
EXPECT_STREQ(GetResourceTypeName(ResourceType::Mesh), "Mesh");
EXPECT_STREQ(GetResourceTypeName(ResourceType::Material), "Material");
EXPECT_STREQ(GetResourceTypeName(ResourceType::UIView), "UIView");
EXPECT_STREQ(GetResourceTypeName(ResourceType::UITheme), "UITheme");
EXPECT_STREQ(GetResourceTypeName(ResourceType::UISchema), "UISchema");
EXPECT_STREQ(GetResourceTypeName(ResourceType::VolumeField), "VolumeField");
EXPECT_STREQ(GetResourceTypeName(ResourceType::Model), "Model");
EXPECT_STREQ(GetResourceTypeName(ResourceType::GaussianSplat), "GaussianSplat");
EXPECT_STREQ(GetResourceTypeName(ResourceType::Unknown), "Unknown");
}
TEST(Resources_Types, GetResourceType_TemplateSpecializations) {
EXPECT_EQ(GetResourceType<Texture>(), ResourceType::Texture);
EXPECT_EQ(GetResourceType<Mesh>(), ResourceType::Mesh);
EXPECT_EQ(GetResourceType<Material>(), ResourceType::Material);
EXPECT_EQ(GetResourceType<Shader>(), ResourceType::Shader);
EXPECT_EQ(GetResourceType<AudioClip>(), ResourceType::AudioClip);
EXPECT_EQ(GetResourceType<BinaryResource>(), ResourceType::Binary);
EXPECT_EQ(GetResourceType<UIView>(), ResourceType::UIView);
EXPECT_EQ(GetResourceType<UITheme>(), ResourceType::UITheme);
EXPECT_EQ(GetResourceType<UISchema>(), ResourceType::UISchema);
EXPECT_EQ(GetResourceType<VolumeField>(), ResourceType::VolumeField);
EXPECT_EQ(GetResourceType<Model>(), ResourceType::Model);
EXPECT_EQ(GetResourceType<GaussianSplat>(), ResourceType::GaussianSplat);
}
} // namespace

View File

@@ -1,92 +0,0 @@
#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);
}

View File

@@ -1,34 +0,0 @@
# ============================================================
# Core/IO Tests
# ============================================================
set(IO_TEST_SOURCES
test_iresource_loader.cpp
test_resource_path.cpp
test_resource_filesystem.cpp
test_file_archive.cpp
test_resource_package.cpp
)
add_executable(io_tests ${IO_TEST_SOURCES})
if(MSVC)
set_target_properties(io_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
endif()
target_link_libraries(io_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(io_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/tests/Fixtures
)
include(GoogleTest)
gtest_discover_tests(io_tests)

View File

@@ -1,51 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/IO/FileArchive.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
namespace {
TEST(FileArchive, DefaultConstructor) {
FileArchive archive;
EXPECT_FALSE(archive.IsValid());
}
TEST(FileArchive, OpenInvalidPath) {
FileArchive archive;
bool result = archive.Open("invalid/path/to/archive.zip");
EXPECT_TRUE(result);
EXPECT_TRUE(archive.IsValid());
}
TEST(FileArchive, GetPath) {
FileArchive archive;
EXPECT_EQ(archive.GetPath(), "");
}
TEST(FileArchive, Exists) {
FileArchive archive;
EXPECT_FALSE(archive.Exists("test.txt"));
}
TEST(FileArchive, GetSize) {
FileArchive archive;
EXPECT_EQ(archive.GetSize("test.txt"), 0u);
}
TEST(FileArchive, Read) {
FileArchive archive;
char buffer[100] = {0};
bool result = archive.Read("test.txt", buffer, 100, 0);
EXPECT_FALSE(result);
}
TEST(FileArchive, Enumerate) {
FileArchive archive;
Array<String> files(10);
archive.Enumerate("*.txt", files);
EXPECT_EQ(files.Size(), 0u);
}
} // namespace

View File

@@ -1,39 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
namespace {
TEST(LoadResult, DefaultConstructor) {
LoadResult result;
EXPECT_FALSE(result.success);
EXPECT_EQ(result.resource, nullptr);
EXPECT_FALSE(result);
}
TEST(LoadResult, FromBool) {
LoadResult result(true);
EXPECT_TRUE(result.success);
}
TEST(LoadResult, FromErrorString) {
XCEngine::Containers::String errorMsg = "Error loading file";
LoadResult result(errorMsg);
EXPECT_FALSE(result.success);
EXPECT_STREQ(result.errorMessage.CStr(), "Error loading file");
}
TEST(LoadResult, BoolOperator) {
LoadResult emptyResult;
EXPECT_FALSE(emptyResult);
LoadResult errorResult("error");
EXPECT_FALSE(errorResult);
}
} // namespace

View File

@@ -1,41 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/IO/ResourceFileSystem.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
namespace {
TEST(ResourceFileSystem, GetSingleton) {
ResourceFileSystem& fs = ResourceFileSystem::Get();
EXPECT_EQ(&fs, &ResourceFileSystem::Get());
}
TEST(ResourceFileSystem, Initialize) {
ResourceFileSystem fs;
fs.Initialize("test/path");
fs.Shutdown();
}
TEST(ResourceFileSystem, AddDirectory) {
ResourceFileSystem fs;
fs.Initialize("test/path");
bool result = fs.AddDirectory("textures");
EXPECT_TRUE(result);
fs.Shutdown();
}
TEST(ResourceFileSystem, Exists) {
ResourceFileSystem fs;
fs.Initialize("test/path");
bool exists = fs.Exists("test.txt");
EXPECT_TRUE(exists);
fs.Shutdown();
}
} // namespace

View File

@@ -1,75 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/IO/ResourcePackage.h>
#include <XCEngine/Core/Containers/Array.h>
#include <XCEngine/Core/Types.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
using namespace XCEngine::Core;
namespace {
TEST(ResourcePackageBuilder, DefaultConstructor) {
ResourcePackageBuilder builder;
EXPECT_TRUE(builder.GetOutputPath().Empty());
EXPECT_EQ(builder.GetProgress(), 0.0f);
}
TEST(ResourcePackageBuilder, SetOutputPath) {
ResourcePackageBuilder builder;
builder.SetOutputPath("test.pkg");
EXPECT_EQ(builder.GetOutputPath(), "test.pkg");
}
TEST(ResourcePackageBuilder, AddFile) {
ResourcePackageBuilder builder;
builder.SetOutputPath("test.pkg");
bool result = builder.AddFile("nonexistent.txt", "test.txt");
EXPECT_FALSE(result);
}
TEST(ResourcePackageBuilder, BuildWithoutFiles) {
ResourcePackageBuilder builder;
builder.SetOutputPath("test.pkg");
bool result = builder.Build();
EXPECT_FALSE(result);
}
TEST(ResourcePackage, DefaultConstructor) {
ResourcePackage pkg;
EXPECT_FALSE(pkg.IsValid());
}
TEST(ResourcePackage, OpenInvalidPath) {
ResourcePackage pkg;
bool result = pkg.Open("invalid/path/package.pkg");
EXPECT_FALSE(result);
EXPECT_FALSE(pkg.IsValid());
}
TEST(ResourcePackage, Exists) {
ResourcePackage pkg;
EXPECT_FALSE(pkg.Exists("test.txt"));
}
TEST(ResourcePackage, GetSize) {
ResourcePackage pkg;
EXPECT_EQ(pkg.GetSize("test.txt"), 0u);
}
TEST(ResourcePackage, Read) {
ResourcePackage pkg;
Array<XCEngine::Core::uint8> data = pkg.Read("test.txt");
EXPECT_EQ(data.Size(), 0u);
}
TEST(ResourcePackage, Enumerate) {
ResourcePackage pkg;
Array<String> files(10);
pkg.Enumerate("*.txt", files);
EXPECT_EQ(files.Size(), 0u);
}
} // namespace

View File

@@ -1,110 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/IO/ResourcePath.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
namespace {
TEST(ResourcePath, DefaultConstructor) {
ResourcePath path;
EXPECT_FALSE(path.IsValid());
}
TEST(ResourcePath, ConstructorWithCString) {
ResourcePath path("textures/player.png");
EXPECT_TRUE(path.IsValid());
EXPECT_EQ(path.GetPath(), "textures/player.png");
}
TEST(ResourcePath, ConstructorWithString) {
String str = "models/player.fbx";
ResourcePath path(str);
EXPECT_TRUE(path.IsValid());
EXPECT_EQ(path.GetPath(), str);
}
TEST(ResourcePath, GetExtension) {
ResourcePath path("textures/player.png");
EXPECT_EQ(path.GetExtension(), ".png");
}
TEST(ResourcePath, GetExtensionNoExtension) {
ResourcePath path("textures/player");
EXPECT_TRUE(path.GetExtension().Empty());
}
TEST(ResourcePath, GetStem) {
ResourcePath path("textures/player.png");
EXPECT_EQ(path.GetStem(), "player");
}
TEST(ResourcePath, GetStemNoExtension) {
ResourcePath path("textures/player");
EXPECT_EQ(path.GetStem(), "player");
}
TEST(ResourcePath, GetFileName) {
ResourcePath path("textures/player.png");
EXPECT_EQ(path.GetFileName(), "player.png");
}
TEST(ResourcePath, GetDirectory) {
ResourcePath path("textures/player.png");
EXPECT_EQ(path.GetDirectory(), "textures");
}
TEST(ResourcePath, GetDirectoryWithBackslash) {
ResourcePath path("textures\\player.png");
EXPECT_EQ(path.GetDirectory(), "textures");
}
TEST(ResourcePath, GetFullPath) {
ResourcePath path("textures/player.png");
EXPECT_EQ(path.GetFullPath(), "textures/player.png");
}
TEST(ResourcePath, HasExtension) {
ResourcePath path("textures/player.png");
EXPECT_TRUE(path.HasExtension(".png"));
EXPECT_FALSE(path.HasExtension(".jpg"));
}
TEST(ResourcePath, HasExtensionNoExtension) {
ResourcePath path("textures/player");
EXPECT_FALSE(path.HasExtension(".png"));
}
TEST(ResourcePath, HasAnyExtension) {
ResourcePath path("textures/player.png");
const char* extensions[] = { ".jpg", ".png", ".tga" };
EXPECT_TRUE(path.HasAnyExtension(extensions, 3));
}
TEST(ResourcePath, HasAnyExtensionNoMatch) {
ResourcePath path("textures/player.png");
const char* extensions[] = { ".jpg", ".tga", ".bmp" };
EXPECT_FALSE(path.HasAnyExtension(extensions, 3));
}
TEST(ResourcePath, ToGUID) {
ResourcePath path("textures/player.png");
ResourceGUID guid = path.ToGUID();
EXPECT_TRUE(guid.IsValid());
}
TEST(ResourcePath, SetPath) {
ResourcePath path;
path.SetPath("textures/player.png");
EXPECT_TRUE(path.IsValid());
EXPECT_EQ(path.GetPath(), "textures/player.png");
}
TEST(ResourcePath, GetRelativePath) {
ResourcePath path("textures/player.png");
EXPECT_EQ(path.GetRelativePath(), "textures/player.png");
}
} // namespace

View File

@@ -1,130 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Layout/LayoutEngine.h>
namespace {
using XCEngine::UI::UIRect;
using XCEngine::UI::UISize;
using XCEngine::UI::Layout::ArrangeOverlayLayout;
using XCEngine::UI::Layout::ArrangeStackLayout;
using XCEngine::UI::Layout::MeasureOverlayLayout;
using XCEngine::UI::Layout::MeasureStackLayout;
using XCEngine::UI::Layout::UILayoutAlignment;
using XCEngine::UI::Layout::UILayoutAxis;
using XCEngine::UI::Layout::UILayoutConstraints;
using XCEngine::UI::Layout::UILayoutItem;
using XCEngine::UI::Layout::UILayoutLength;
using XCEngine::UI::Layout::UILayoutThickness;
using XCEngine::UI::Layout::UIOverlayLayoutOptions;
using XCEngine::UI::Layout::UIStackLayoutOptions;
void ExpectRect(
const UIRect& rect,
float x,
float y,
float width,
float height) {
EXPECT_FLOAT_EQ(rect.x, x);
EXPECT_FLOAT_EQ(rect.y, y);
EXPECT_FLOAT_EQ(rect.width, width);
EXPECT_FLOAT_EQ(rect.height, height);
}
} // namespace
TEST(UI_Layout, MeasureHorizontalStackAccumulatesSpacingPaddingAndCrossExtent) {
UIStackLayoutOptions options = {};
options.axis = UILayoutAxis::Horizontal;
options.spacing = 5.0f;
options.padding = UILayoutThickness::Symmetric(10.0f, 6.0f);
std::vector<UILayoutItem> items(2);
items[0].desiredContentSize = UISize(40.0f, 20.0f);
items[1].desiredContentSize = UISize(60.0f, 30.0f);
const auto result = MeasureStackLayout(options, items);
EXPECT_FLOAT_EQ(result.desiredSize.width, 125.0f);
EXPECT_FLOAT_EQ(result.desiredSize.height, 42.0f);
}
TEST(UI_Layout, ArrangeHorizontalStackDistributesRemainingSpaceToStretchChildren) {
UIStackLayoutOptions options = {};
options.axis = UILayoutAxis::Horizontal;
options.spacing = 5.0f;
options.padding = UILayoutThickness::Uniform(10.0f);
std::vector<UILayoutItem> items(3);
items[0].width = UILayoutLength::Pixels(100.0f);
items[0].desiredContentSize = UISize(10.0f, 20.0f);
items[1].width = UILayoutLength::Stretch(1.0f);
items[1].desiredContentSize = UISize(30.0f, 20.0f);
items[2].width = UILayoutLength::Pixels(50.0f);
items[2].desiredContentSize = UISize(10.0f, 20.0f);
const auto result = ArrangeStackLayout(options, items, UIRect(0.0f, 0.0f, 300.0f, 80.0f));
ExpectRect(result.children[0].arrangedRect, 10.0f, 10.0f, 100.0f, 20.0f);
ExpectRect(result.children[1].arrangedRect, 115.0f, 10.0f, 120.0f, 20.0f);
ExpectRect(result.children[2].arrangedRect, 240.0f, 10.0f, 50.0f, 20.0f);
}
TEST(UI_Layout, ArrangeVerticalStackSupportsCrossAxisStretch) {
UIStackLayoutOptions options = {};
options.axis = UILayoutAxis::Vertical;
options.spacing = 4.0f;
options.padding = UILayoutThickness::Symmetric(8.0f, 6.0f);
std::vector<UILayoutItem> items(2);
items[0].desiredContentSize = UISize(40.0f, 10.0f);
items[0].horizontalAlignment = UILayoutAlignment::Stretch;
items[1].desiredContentSize = UISize(60.0f, 20.0f);
const auto result = ArrangeStackLayout(options, items, UIRect(0.0f, 0.0f, 200.0f, 100.0f));
ExpectRect(result.children[0].arrangedRect, 8.0f, 6.0f, 184.0f, 10.0f);
ExpectRect(result.children[1].arrangedRect, 8.0f, 20.0f, 60.0f, 20.0f);
}
TEST(UI_Layout, ArrangeOverlaySupportsCenterAndStretch) {
UIOverlayLayoutOptions options = {};
options.padding = UILayoutThickness::Uniform(10.0f);
std::vector<UILayoutItem> items(2);
items[0].desiredContentSize = UISize(40.0f, 20.0f);
items[0].horizontalAlignment = UILayoutAlignment::Center;
items[0].verticalAlignment = UILayoutAlignment::Center;
items[1].desiredContentSize = UISize(10.0f, 10.0f);
items[1].width = UILayoutLength::Stretch();
items[1].height = UILayoutLength::Stretch();
items[1].margin = UILayoutThickness::Uniform(5.0f);
const auto result = ArrangeOverlayLayout(options, items, UIRect(0.0f, 0.0f, 100.0f, 60.0f));
ExpectRect(result.children[0].arrangedRect, 30.0f, 20.0f, 40.0f, 20.0f);
ExpectRect(result.children[1].arrangedRect, 15.0f, 15.0f, 70.0f, 30.0f);
}
TEST(UI_Layout, MeasureOverlayRespectsItemMinMaxAndAvailableConstraints) {
UIOverlayLayoutOptions options = {};
std::vector<UILayoutItem> items(1);
items[0].width = UILayoutLength::Pixels(500.0f);
items[0].desiredContentSize = UISize(10.0f, 10.0f);
items[0].minSize = UISize(0.0f, 50.0f);
items[0].maxSize = UISize(200.0f, 120.0f);
const auto result = MeasureOverlayLayout(
options,
items,
UILayoutConstraints::Bounded(150.0f, 100.0f));
EXPECT_FLOAT_EQ(result.children[0].measuredSize.width, 150.0f);
EXPECT_FLOAT_EQ(result.children[0].measuredSize.height, 50.0f);
EXPECT_FLOAT_EQ(result.desiredSize.width, 150.0f);
EXPECT_FLOAT_EQ(result.desiredSize.height, 50.0f);
}

View File

@@ -1,40 +0,0 @@
# ============================================================
# UI Core Tests
# ============================================================
set(UI_TEST_SOURCES
test_ui_core.cpp
test_ui_expansion_model.cpp
test_ui_flat_hierarchy_helpers.cpp
test_ui_input_dispatcher.cpp
test_ui_keyboard_navigation_model.cpp
test_ui_property_edit_model.cpp
test_layout_engine.cpp
test_ui_selection_model.cpp
test_ui_runtime.cpp
test_ui_text_editing.cpp
test_ui_text_input_controller.cpp
)
add_executable(core_ui_tests ${UI_TEST_SOURCES})
if(MSVC)
set_target_properties(core_ui_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
endif()
target_link_libraries(core_ui_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(core_ui_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/tests/Fixtures
)
include(GoogleTest)
gtest_discover_tests(core_ui_tests)

View File

@@ -1,166 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Style/StyleResolver.h>
namespace {
using XCEngine::Math::Color;
using XCEngine::UI::Style::BuildBuiltinTheme;
using XCEngine::UI::Style::BuildTheme;
using XCEngine::UI::Style::UICornerRadius;
using XCEngine::UI::Style::UIBuiltinThemeKind;
using XCEngine::UI::Style::UIResolvedStyle;
using XCEngine::UI::Style::UIStyleLayer;
using XCEngine::UI::Style::UIStylePropertyId;
using XCEngine::UI::Style::UIStyleResolveContext;
using XCEngine::UI::Style::UIStyleSet;
using XCEngine::UI::Style::UIStyleSheet;
using XCEngine::UI::Style::UIStyleValue;
using XCEngine::UI::Style::UIStyleValueType;
using XCEngine::UI::Style::UITheme;
using XCEngine::UI::Style::UIThemeDefinition;
using XCEngine::UI::Style::UITokenResolveStatus;
void ExpectColorEq(const Color& actual, const Color& expected) {
EXPECT_FLOAT_EQ(actual.r, expected.r);
EXPECT_FLOAT_EQ(actual.g, expected.g);
EXPECT_FLOAT_EQ(actual.b, expected.b);
EXPECT_FLOAT_EQ(actual.a, expected.a);
}
TEST(UI_StyleSystem, ThemeDefinitionBuildsThemeAndResolvesAliases) {
UIThemeDefinition definition = {};
definition.name = "CustomTheme";
definition.SetToken("space.base", UIStyleValue(8.0f));
definition.SetToken("gap.control", UIStyleValue::Token("space.base"));
definition.SetToken("radius.card", UIStyleValue(UICornerRadius::Uniform(12.0f)));
const UITheme theme = BuildTheme(definition);
EXPECT_EQ(theme.GetName(), "CustomTheme");
const auto gapToken = theme.ResolveToken("gap.control", UIStyleValueType::Float);
ASSERT_EQ(gapToken.status, UITokenResolveStatus::Resolved);
ASSERT_NE(gapToken.value.TryGetFloat(), nullptr);
EXPECT_FLOAT_EQ(*gapToken.value.TryGetFloat(), 8.0f);
const auto radiusToken = theme.ResolveToken("radius.card", UIStyleValueType::CornerRadius);
ASSERT_EQ(radiusToken.status, UITokenResolveStatus::Resolved);
ASSERT_NE(radiusToken.value.TryGetCornerRadius(), nullptr);
EXPECT_TRUE(radiusToken.value.TryGetCornerRadius()->IsUniform());
EXPECT_FLOAT_EQ(radiusToken.value.TryGetCornerRadius()->topLeft, 12.0f);
}
TEST(UI_StyleSystem, ThemeResolvesParentTokensAndBuiltinVariantsDiffer) {
UIThemeDefinition baseDefinition = {};
baseDefinition.name = "Base";
baseDefinition.SetToken("color.surface", UIStyleValue(Color(0.20f, 0.21f, 0.22f, 1.0f)));
const UITheme baseTheme = BuildTheme(baseDefinition);
UIThemeDefinition childDefinition = {};
childDefinition.name = "Child";
childDefinition.SetToken("font.body", UIStyleValue(15.0f));
const UITheme childTheme = BuildTheme(childDefinition, &baseTheme);
const auto parentToken = childTheme.ResolveToken("color.surface", UIStyleValueType::Color);
ASSERT_EQ(parentToken.status, UITokenResolveStatus::Resolved);
ASSERT_NE(parentToken.value.TryGetColor(), nullptr);
ExpectColorEq(*parentToken.value.TryGetColor(), Color(0.20f, 0.21f, 0.22f, 1.0f));
const UITheme darkTheme = BuildBuiltinTheme(UIBuiltinThemeKind::NeutralDark);
const UITheme lightTheme = BuildBuiltinTheme(UIBuiltinThemeKind::NeutralLight);
const auto darkSurface = darkTheme.ResolveToken("color.surface", UIStyleValueType::Color);
const auto lightSurface = lightTheme.ResolveToken("color.surface", UIStyleValueType::Color);
ASSERT_EQ(darkSurface.status, UITokenResolveStatus::Resolved);
ASSERT_EQ(lightSurface.status, UITokenResolveStatus::Resolved);
ASSERT_NE(darkSurface.value.TryGetColor(), nullptr);
ASSERT_NE(lightSurface.value.TryGetColor(), nullptr);
EXPECT_NE(darkSurface.value.TryGetColor()->r, lightSurface.value.TryGetColor()->r);
}
TEST(UI_StyleSystem, ThemeReportsMissingCyclesAndTypeMismatches) {
UIThemeDefinition definition = {};
definition.SetToken("cycle.a", UIStyleValue::Token("cycle.b"));
definition.SetToken("cycle.b", UIStyleValue::Token("cycle.a"));
definition.SetToken("color.surface", UIStyleValue(Color(0.1f, 0.2f, 0.3f, 1.0f)));
const UITheme theme = BuildTheme(definition);
EXPECT_EQ(
theme.ResolveToken("missing.token", UIStyleValueType::Float).status,
UITokenResolveStatus::MissingToken);
EXPECT_EQ(
theme.ResolveToken("cycle.a", UIStyleValueType::Float).status,
UITokenResolveStatus::CycleDetected);
EXPECT_EQ(
theme.ResolveToken("color.surface", UIStyleValueType::Float).status,
UITokenResolveStatus::TypeMismatch);
}
TEST(UI_StyleSystem, StyleResolutionPrefersLocalThenNamedThenTypeThenDefault) {
UIStyleSheet styleSheet = {};
styleSheet.DefaultStyle().SetProperty(UIStylePropertyId::FontSize, UIStyleValue(12.0f));
styleSheet.GetOrCreateTypeStyle("Button").SetProperty(UIStylePropertyId::FontSize, UIStyleValue(14.0f));
styleSheet.GetOrCreateNamedStyle("Primary").SetProperty(UIStylePropertyId::FontSize, UIStyleValue(16.0f));
UIStyleSet localStyle = {};
localStyle.SetProperty(UIStylePropertyId::FontSize, UIStyleValue(18.0f));
UIStyleResolveContext context = {};
context.styleSheet = &styleSheet;
context.selector.typeName = "Button";
context.selector.styleName = "Primary";
context.localStyle = &localStyle;
auto resolution = XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::FontSize, context);
ASSERT_TRUE(resolution.resolved);
EXPECT_EQ(resolution.layer, UIStyleLayer::Local);
ASSERT_NE(resolution.value.TryGetFloat(), nullptr);
EXPECT_FLOAT_EQ(*resolution.value.TryGetFloat(), 18.0f);
localStyle.RemoveProperty(UIStylePropertyId::FontSize);
resolution = XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::FontSize, context);
ASSERT_TRUE(resolution.resolved);
EXPECT_EQ(resolution.layer, UIStyleLayer::Named);
EXPECT_FLOAT_EQ(*resolution.value.TryGetFloat(), 16.0f);
styleSheet.GetOrCreateNamedStyle("Primary").RemoveProperty(UIStylePropertyId::FontSize);
resolution = XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::FontSize, context);
ASSERT_TRUE(resolution.resolved);
EXPECT_EQ(resolution.layer, UIStyleLayer::Type);
EXPECT_FLOAT_EQ(*resolution.value.TryGetFloat(), 14.0f);
}
TEST(UI_StyleSystem, StyleResolutionFallsBackWhenHigherPriorityTokenCannotResolve) {
UIThemeDefinition themeDefinition = {};
themeDefinition.SetToken("color.accent", UIStyleValue(Color(0.90f, 0.20f, 0.10f, 1.0f)));
const UITheme theme = BuildTheme(themeDefinition);
UIStyleSheet styleSheet = {};
styleSheet.DefaultStyle().SetProperty(UIStylePropertyId::BorderWidth, UIStyleValue(1.0f));
styleSheet.GetOrCreateTypeStyle("Button")
.SetProperty(UIStylePropertyId::BackgroundColor, UIStyleValue::Token("color.accent"));
styleSheet.GetOrCreateNamedStyle("Danger")
.SetProperty(UIStylePropertyId::BackgroundColor, UIStyleValue::Token("missing.token"));
UIStyleResolveContext context = {};
context.theme = &theme;
context.styleSheet = &styleSheet;
context.selector.typeName = "Button";
context.selector.styleName = "Danger";
const auto backgroundResolution =
XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::BackgroundColor, context);
ASSERT_TRUE(backgroundResolution.resolved);
EXPECT_EQ(backgroundResolution.layer, UIStyleLayer::Type);
ASSERT_NE(backgroundResolution.value.TryGetColor(), nullptr);
ExpectColorEq(*backgroundResolution.value.TryGetColor(), Color(0.90f, 0.20f, 0.10f, 1.0f));
const UIResolvedStyle resolvedStyle = XCEngine::UI::Style::ResolveStyle(context);
const auto* borderWidthResolution = resolvedStyle.FindProperty(UIStylePropertyId::BorderWidth);
ASSERT_NE(borderWidthResolution, nullptr);
EXPECT_EQ(borderWidthResolution->layer, UIStyleLayer::Default);
ASSERT_NE(borderWidthResolution->value.TryGetFloat(), nullptr);
EXPECT_FLOAT_EQ(*borderWidthResolution->value.TryGetFloat(), 1.0f);
}
} // namespace

View File

@@ -1,980 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
#include <XCEngine/UI/Runtime/UISceneRuntimeContext.h>
#include <XCEngine/UI/Runtime/UIScreenStackController.h>
#include <XCEngine/UI/Runtime/UISystem.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <limits>
#include <string>
#include <vector>
namespace {
using XCEngine::UI::Runtime::UIScreenAsset;
using XCEngine::UI::Runtime::UIScreenFrameInput;
using XCEngine::UI::Runtime::UIScreenPlayer;
using XCEngine::UI::Runtime::UISceneRuntimeContext;
using XCEngine::UI::Runtime::UIDocumentScreenHost;
using XCEngine::UI::Runtime::UIScreenStackController;
using XCEngine::UI::Runtime::UISystem;
using XCEngine::Input::KeyCode;
namespace fs = std::filesystem;
class TempFileScope {
public:
TempFileScope(std::string stem, std::string extension, std::string contents) {
const auto uniqueId = std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count());
m_path = fs::temp_directory_path() / (std::move(stem) + "_" + uniqueId + std::move(extension));
std::ofstream output(m_path, std::ios::binary | std::ios::trunc);
output << contents;
}
~TempFileScope() {
std::error_code ec;
fs::remove(m_path, ec);
}
const fs::path& Path() const {
return m_path;
}
private:
fs::path m_path = {};
};
std::string BuildViewMarkup(const char* heroTitle, const char* overlayText = nullptr) {
std::string markup =
"<View name=\"Runtime Screen\">\n"
" <Column id=\"root\" padding=\"18\" gap=\"10\">\n"
" <Card id=\"hero\" title=\"" + std::string(heroTitle) + "\" subtitle=\"Shared XCUI runtime layer\" />\n"
" <Text id=\"status\" text=\"Ready for play\" />\n"
" <Row id=\"actions\" gap=\"12\">\n"
" <Button id=\"start\" text=\"Start\" />\n"
" <Button id=\"options\" text=\"Options\" />\n"
" </Row>\n";
if (overlayText != nullptr) {
markup += " <Card id=\"overlay\" title=\"" + std::string(overlayText) + "\" tone=\"accent\" />\n";
}
markup +=
" </Column>\n"
"</View>\n";
return markup;
}
UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) {
UIScreenAsset screen = {};
screen.screenId = screenId;
screen.documentPath = viewPath.string();
return screen;
}
bool DrawDataContainsText(
const XCEngine::UI::UIDrawData& drawData,
const std::string& text) {
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == XCEngine::UI::UIDrawCommandType::Text &&
command.text == text) {
return true;
}
}
}
return false;
}
std::vector<XCEngine::UI::UIRect> CollectFilledRects(
const XCEngine::UI::UIDrawData& drawData) {
std::vector<XCEngine::UI::UIRect> rects = {};
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == XCEngine::UI::UIDrawCommandType::FilledRect) {
rects.push_back(command.rect);
}
}
}
return rects;
}
const XCEngine::UI::UIDrawCommand* FindTextCommand(
const XCEngine::UI::UIDrawData& drawData,
const std::string& text) {
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == XCEngine::UI::UIDrawCommandType::Text &&
command.text == text) {
return &command;
}
}
}
return nullptr;
}
bool RectContainsPoint(
const XCEngine::UI::UIRect& rect,
const XCEngine::UI::UIPoint& point) {
return point.x >= rect.x &&
point.x <= rect.x + rect.width &&
point.y >= rect.y &&
point.y <= rect.y + rect.height;
}
bool TryFindSmallestFilledRectContainingPoint(
const XCEngine::UI::UIDrawData& drawData,
const XCEngine::UI::UIPoint& point,
XCEngine::UI::UIRect& outRect) {
bool found = false;
float bestArea = (std::numeric_limits<float>::max)();
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
if (command.type != XCEngine::UI::UIDrawCommandType::FilledRect ||
!RectContainsPoint(command.rect, point)) {
continue;
}
const float area = command.rect.width * command.rect.height;
if (!found || area < bestArea) {
outRect = command.rect;
bestArea = area;
found = true;
}
}
}
return found;
}
bool TryFindFilledRectForText(
const XCEngine::UI::UIDrawData& drawData,
const std::string& text,
XCEngine::UI::UIRect& outRect) {
const auto* textCommand = FindTextCommand(drawData, text);
return textCommand != nullptr &&
TryFindSmallestFilledRectContainingPoint(drawData, textCommand->position, outRect);
}
XCEngine::UI::UIPoint GetRectCenter(const XCEngine::UI::UIRect& rect) {
return XCEngine::UI::UIPoint(
rect.x + rect.width * 0.5f,
rect.y + rect.height * 0.5f);
}
std::size_t CountCommandsOfType(
const XCEngine::UI::UIDrawData& drawData,
XCEngine::UI::UIDrawCommandType type) {
std::size_t count = 0u;
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == type) {
++count;
}
}
}
return count;
}
UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
UIScreenFrameInput input = {};
input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f);
input.frameIndex = frameIndex;
input.focused = true;
return input;
}
const XCEngine::UI::Runtime::UISystemPresentedLayer* FindPresentedLayerById(
const XCEngine::UI::Runtime::UISystemFrameResult& frame,
XCEngine::UI::Runtime::UIScreenLayerId layerId) {
for (const XCEngine::UI::Runtime::UISystemPresentedLayer& layer : frame.layers) {
if (layer.layerId == layerId) {
return &layer;
}
}
return nullptr;
}
class RecordingDocumentHost final : public XCEngine::UI::Runtime::IUIScreenDocumentHost {
public:
struct BuildCall {
std::string displayName = {};
UIScreenFrameInput input = {};
};
XCEngine::UI::Runtime::UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) override {
XCEngine::UI::Runtime::UIScreenLoadResult result = {};
result.succeeded = asset.IsValid();
result.document.sourcePath = asset.documentPath;
result.document.displayName = asset.screenId.empty() ? asset.documentPath : asset.screenId;
return result;
}
XCEngine::UI::Runtime::UIScreenFrameResult BuildFrame(
const XCEngine::UI::Runtime::UIScreenDocument& document,
const UIScreenFrameInput& input) override {
m_buildCalls.push_back(BuildCall{ document.displayName, input });
XCEngine::UI::Runtime::UIScreenFrameResult result = {};
result.stats.documentLoaded = true;
result.stats.inputEventCount = input.events.size();
result.stats.presentedFrameIndex = input.frameIndex;
XCEngine::UI::UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName);
drawList.AddText(
XCEngine::UI::UIPoint(input.viewportRect.x, input.viewportRect.y),
document.displayName);
result.stats.drawListCount = result.drawData.GetDrawListCount();
result.stats.commandCount = result.drawData.GetTotalCommandCount();
return result;
}
const BuildCall* FindBuildCall(const std::string& displayName) const {
for (const BuildCall& call : m_buildCalls) {
if (call.displayName == displayName) {
return &call;
}
}
return nullptr;
}
std::size_t GetBuildCallCount() const {
return m_buildCalls.size();
}
private:
std::vector<BuildCall> m_buildCalls = {};
};
} // namespace
TEST(UIRuntimeTest, ScreenPlayerBuildsDrawDataFromDocumentTree) {
TempFileScope viewFile("xcui_runtime_screen", ".xcui", BuildViewMarkup("Runtime HUD"));
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.main_menu")));
const auto& frame = player.Update(BuildInputState());
EXPECT_TRUE(frame.stats.documentLoaded);
EXPECT_EQ(frame.stats.nodeCount, 7u);
EXPECT_EQ(frame.stats.drawListCount, frame.drawData.GetDrawListCount());
EXPECT_EQ(frame.stats.commandCount, frame.drawData.GetTotalCommandCount());
EXPECT_GE(frame.stats.textCommandCount, 5u);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Runtime HUD"));
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Ready for play"));
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Start"));
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Options"));
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
}
TEST(UIRuntimeTest, DocumentHostStretchesColumnChildrenAcrossCrossAxis) {
TempFileScope viewFile(
"xcui_runtime_stretch_column",
".xcui",
"<View name=\"Stretch Column\">\n"
" <Column padding=\"18\" gap=\"10\">\n"
" <Button text=\"Wide Button\" />\n"
" </Column>\n"
"</View>\n");
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.stretch.column")));
UIScreenFrameInput input = BuildInputState();
input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 400.0f, 220.0f);
const auto& frame = player.Update(input);
const auto* buttonText = FindTextCommand(frame.drawData, "Wide Button");
ASSERT_NE(buttonText, nullptr);
XCEngine::UI::UIRect buttonRect = {};
ASSERT_TRUE(TryFindSmallestFilledRectContainingPoint(frame.drawData, buttonText->position, buttonRect));
EXPECT_FLOAT_EQ(buttonRect.x, 34.0f);
EXPECT_FLOAT_EQ(buttonRect.width, 332.0f);
}
TEST(UIRuntimeTest, DocumentHostDoesNotLetExplicitHeightCrushCardContent) {
TempFileScope viewFile(
"xcui_runtime_card_height_floor",
".xcui",
"<View name=\"Card Height Floor\">\n"
" <Column padding=\"18\" gap=\"10\">\n"
" <Card title=\"Hero\" subtitle=\"Subtitle\" height=\"32\">\n"
" <Row gap=\"10\">\n"
" <Button text=\"Action\" />\n"
" </Row>\n"
" </Card>\n"
" </Column>\n"
"</View>\n");
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.card.height.floor")));
UIScreenFrameInput input = BuildInputState();
input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 500.0f, 300.0f);
const auto& frame = player.Update(input);
const auto* heroTitle = FindTextCommand(frame.drawData, "Hero");
const auto* actionText = FindTextCommand(frame.drawData, "Action");
ASSERT_NE(heroTitle, nullptr);
ASSERT_NE(actionText, nullptr);
XCEngine::UI::UIRect cardRect = {};
XCEngine::UI::UIRect buttonRect = {};
ASSERT_TRUE(TryFindSmallestFilledRectContainingPoint(frame.drawData, heroTitle->position, cardRect));
ASSERT_TRUE(TryFindSmallestFilledRectContainingPoint(frame.drawData, actionText->position, buttonRect));
EXPECT_GT(cardRect.height, 32.0f);
EXPECT_GE(buttonRect.y, cardRect.y + 50.0f);
EXPECT_LE(buttonRect.y + buttonRect.height, cardRect.y + cardRect.height - 8.0f);
}
TEST(UIRuntimeTest, DocumentHostScrollViewClipsOverflowingChildrenAndRespondsToWheelInput) {
TempFileScope viewFile(
"xcui_runtime_scroll_view",
".xcui",
"<View name=\"Scroll View Test\">\n"
" <Column padding=\"18\" gap=\"10\">\n"
" <Card title=\"Console\" subtitle=\"Scroll smoke\" height=\"200\">\n"
" <ScrollView id=\"log-scroll\" height=\"fill\">\n"
" <Column gap=\"8\">\n"
" <Text text=\"Line 01\" />\n"
" <Text text=\"Line 02\" />\n"
" <Text text=\"Line 03\" />\n"
" <Text text=\"Line 04\" />\n"
" <Text text=\"Line 05\" />\n"
" <Text text=\"Line 06\" />\n"
" <Text text=\"Line 07\" />\n"
" <Text text=\"Line 08\" />\n"
" <Text text=\"Line 09\" />\n"
" <Text text=\"Line 10\" />\n"
" <Text text=\"Line 11\" />\n"
" <Text text=\"Line 12\" />\n"
" </Column>\n"
" </ScrollView>\n"
" </Card>\n"
" </Column>\n"
"</View>\n");
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.scroll.view")));
UIScreenFrameInput firstInput = BuildInputState(1u);
firstInput.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 480.0f, 320.0f);
const auto& firstFrame = player.Update(firstInput);
const auto* line01Before = FindTextCommand(firstFrame.drawData, "Line 01");
ASSERT_NE(line01Before, nullptr);
const float line01BeforeY = line01Before->position.y;
EXPECT_GT(CountCommandsOfType(firstFrame.drawData, XCEngine::UI::UIDrawCommandType::PushClipRect), 0u);
EXPECT_GT(CountCommandsOfType(firstFrame.drawData, XCEngine::UI::UIDrawCommandType::PopClipRect), 0u);
UIScreenFrameInput secondInput = BuildInputState(2u);
secondInput.viewportRect = firstInput.viewportRect;
XCEngine::UI::UIInputEvent wheelEvent = {};
wheelEvent.type = XCEngine::UI::UIInputEventType::PointerWheel;
wheelEvent.position = XCEngine::UI::UIPoint(90.0f, 130.0f);
wheelEvent.wheelDelta = -120.0f;
secondInput.events.push_back(wheelEvent);
const auto& secondFrame = player.Update(secondInput);
const auto* line01After = FindTextCommand(secondFrame.drawData, "Line 01");
ASSERT_NE(line01After, nullptr);
const float line01AfterY = line01After->position.y;
EXPECT_LT(line01AfterY, line01BeforeY);
UIScreenFrameInput thirdInput = BuildInputState(3u);
thirdInput.viewportRect = firstInput.viewportRect;
const auto& thirdFrame = player.Update(thirdInput);
const auto* line01Persisted = FindTextCommand(thirdFrame.drawData, "Line 01");
ASSERT_NE(line01Persisted, nullptr);
EXPECT_FLOAT_EQ(line01Persisted->position.y, line01AfterY);
}
TEST(UIRuntimeTest, DocumentHostTracksHoverFocusAndPointerCaptureAcrossFrames) {
TempFileScope viewFile(
"xcui_runtime_input_states",
".xcui",
"<View name=\"Input State Test\">\n"
" <Column padding=\"18\" gap=\"10\">\n"
" <Button id=\"input-hover\" text=\"Hover / Focus\" />\n"
" <Button id=\"input-capture\" text=\"Pointer Capture\" capturePointer=\"true\" />\n"
" <Button id=\"input-route\" text=\"Route Target\" />\n"
" </Column>\n"
"</View>\n");
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.input.states")));
UIScreenFrameInput firstInput = BuildInputState(1u);
firstInput.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 520.0f, 260.0f);
const auto& firstFrame = player.Update(firstInput);
const auto* hoverButtonText = FindTextCommand(firstFrame.drawData, "Hover / Focus");
const auto* captureButtonText = FindTextCommand(firstFrame.drawData, "Pointer Capture");
const auto* routeButtonText = FindTextCommand(firstFrame.drawData, "Route Target");
ASSERT_NE(hoverButtonText, nullptr);
ASSERT_NE(captureButtonText, nullptr);
ASSERT_NE(routeButtonText, nullptr);
XCEngine::UI::UIRect hoverButtonRect = {};
XCEngine::UI::UIRect captureButtonRect = {};
XCEngine::UI::UIRect routeButtonRect = {};
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Hover / Focus", hoverButtonRect));
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Pointer Capture", captureButtonRect));
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Route Target", routeButtonRect));
const XCEngine::UI::UIPoint hoverButtonPoint = GetRectCenter(hoverButtonRect);
const XCEngine::UI::UIPoint captureButtonPoint = GetRectCenter(captureButtonRect);
const XCEngine::UI::UIPoint routeButtonPoint = GetRectCenter(routeButtonRect);
SCOPED_TRACE(
std::string("hoverPos=") + std::to_string(hoverButtonText->position.x) + "," + std::to_string(hoverButtonText->position.y) +
" hoverRect=" + std::to_string(hoverButtonRect.x) + "," + std::to_string(hoverButtonRect.y) + "," +
std::to_string(hoverButtonRect.width) + "," + std::to_string(hoverButtonRect.height) +
" capturePos=" + std::to_string(captureButtonText->position.x) + "," + std::to_string(captureButtonText->position.y) +
" captureRect=" + std::to_string(captureButtonRect.x) + "," + std::to_string(captureButtonRect.y) + "," +
std::to_string(captureButtonRect.width) + "," + std::to_string(captureButtonRect.height) +
" routePos=" + std::to_string(routeButtonText->position.x) + "," + std::to_string(routeButtonText->position.y) +
" routeRect=" + std::to_string(routeButtonRect.x) + "," + std::to_string(routeButtonRect.y) + "," +
std::to_string(routeButtonRect.width) + "," + std::to_string(routeButtonRect.height));
UIScreenFrameInput hoverInput = BuildInputState(2u);
hoverInput.viewportRect = firstInput.viewportRect;
XCEngine::UI::UIInputEvent hoverEvent = {};
hoverEvent.type = XCEngine::UI::UIInputEventType::PointerMove;
hoverEvent.position = hoverButtonPoint;
hoverInput.events.push_back(hoverEvent);
player.Update(hoverInput);
const auto& afterHover = host.GetInputDebugSnapshot();
EXPECT_NE(afterHover.hoveredStateKey.find("/input-hover"), std::string::npos);
EXPECT_TRUE(afterHover.focusedStateKey.empty());
UIScreenFrameInput captureDownInput = BuildInputState(3u);
captureDownInput.viewportRect = firstInput.viewportRect;
XCEngine::UI::UIInputEvent captureDownEvent = {};
captureDownEvent.type = XCEngine::UI::UIInputEventType::PointerButtonDown;
captureDownEvent.pointerButton = XCEngine::UI::UIPointerButton::Left;
captureDownEvent.position = captureButtonPoint;
captureDownInput.events.push_back(captureDownEvent);
player.Update(captureDownInput);
const auto& afterCaptureDown = host.GetInputDebugSnapshot();
SCOPED_TRACE(
std::string("afterCaptureDown hovered=") + afterCaptureDown.hoveredStateKey +
" focused=" + afterCaptureDown.focusedStateKey +
" active=" + afterCaptureDown.activeStateKey +
" capture=" + afterCaptureDown.captureStateKey +
" lastKind=" + afterCaptureDown.lastTargetKind +
" lastTarget=" + afterCaptureDown.lastTargetStateKey +
" result=" + afterCaptureDown.lastResult);
EXPECT_NE(afterCaptureDown.focusedStateKey.find("/input-capture"), std::string::npos);
EXPECT_NE(afterCaptureDown.activeStateKey.find("/input-capture"), std::string::npos);
EXPECT_NE(afterCaptureDown.captureStateKey.find("/input-capture"), std::string::npos);
UIScreenFrameInput dragInput = BuildInputState(4u);
dragInput.viewportRect = firstInput.viewportRect;
XCEngine::UI::UIInputEvent dragEvent = {};
dragEvent.type = XCEngine::UI::UIInputEventType::PointerMove;
dragEvent.position = routeButtonPoint;
dragInput.events.push_back(dragEvent);
player.Update(dragInput);
const auto& afterDrag = host.GetInputDebugSnapshot();
SCOPED_TRACE(
std::string("afterDrag hovered=") + afterDrag.hoveredStateKey +
" focused=" + afterDrag.focusedStateKey +
" active=" + afterDrag.activeStateKey +
" capture=" + afterDrag.captureStateKey +
" lastKind=" + afterDrag.lastTargetKind +
" lastTarget=" + afterDrag.lastTargetStateKey +
" result=" + afterDrag.lastResult);
EXPECT_NE(afterDrag.hoveredStateKey.find("/input-route"), std::string::npos);
EXPECT_NE(afterDrag.captureStateKey.find("/input-capture"), std::string::npos);
EXPECT_EQ(afterDrag.lastTargetKind, "Captured");
EXPECT_NE(afterDrag.lastTargetStateKey.find("/input-capture"), std::string::npos);
UIScreenFrameInput releaseInput = BuildInputState(5u);
releaseInput.viewportRect = firstInput.viewportRect;
XCEngine::UI::UIInputEvent releaseEvent = {};
releaseEvent.type = XCEngine::UI::UIInputEventType::PointerButtonUp;
releaseEvent.pointerButton = XCEngine::UI::UIPointerButton::Left;
releaseEvent.position = routeButtonPoint;
releaseInput.events.push_back(releaseEvent);
player.Update(releaseInput);
const auto& afterRelease = host.GetInputDebugSnapshot();
EXPECT_TRUE(afterRelease.activeStateKey.empty());
EXPECT_TRUE(afterRelease.captureStateKey.empty());
EXPECT_NE(afterRelease.focusedStateKey.find("/input-capture"), std::string::npos);
EXPECT_NE(afterRelease.hoveredStateKey.find("/input-route"), std::string::npos);
}
TEST(UIRuntimeTest, DocumentHostTraversesKeyboardFocusAndKeyboardActivationAcrossFrames) {
TempFileScope viewFile(
"xcui_runtime_keyboard_focus",
".xcui",
"<View name=\"Keyboard Focus Test\">\n"
" <Column padding=\"18\" gap=\"10\">\n"
" <Button id=\"focus-first\" text=\"First Focus\" />\n"
" <Button id=\"focus-second\" text=\"Second Focus\" />\n"
" <Button id=\"focus-third\" text=\"Third Focus\" />\n"
" </Column>\n"
"</View>\n");
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.keyboard.focus")));
UIScreenFrameInput firstInput = BuildInputState(1u);
firstInput.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 520.0f, 260.0f);
player.Update(firstInput);
const auto& initialSnapshot = host.GetInputDebugSnapshot();
EXPECT_TRUE(initialSnapshot.focusedStateKey.empty());
UIScreenFrameInput tabInput = BuildInputState(2u);
tabInput.viewportRect = firstInput.viewportRect;
XCEngine::UI::UIInputEvent tabEvent = {};
tabEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
tabEvent.keyCode = static_cast<std::int32_t>(KeyCode::Tab);
tabInput.events.push_back(tabEvent);
player.Update(tabInput);
const auto& afterFirstTab = host.GetInputDebugSnapshot();
EXPECT_NE(afterFirstTab.focusedStateKey.find("/focus-first"), std::string::npos);
EXPECT_EQ(afterFirstTab.lastResult, "Focus traversed");
UIScreenFrameInput secondTabInput = BuildInputState(3u);
secondTabInput.viewportRect = firstInput.viewportRect;
secondTabInput.events.push_back(tabEvent);
player.Update(secondTabInput);
const auto& afterSecondTab = host.GetInputDebugSnapshot();
EXPECT_NE(afterSecondTab.focusedStateKey.find("/focus-second"), std::string::npos);
UIScreenFrameInput reverseTabInput = BuildInputState(4u);
reverseTabInput.viewportRect = firstInput.viewportRect;
XCEngine::UI::UIInputEvent reverseTabEvent = tabEvent;
reverseTabEvent.modifiers.shift = true;
reverseTabInput.events.push_back(reverseTabEvent);
player.Update(reverseTabInput);
const auto& afterReverseTab = host.GetInputDebugSnapshot();
EXPECT_NE(afterReverseTab.focusedStateKey.find("/focus-first"), std::string::npos);
UIScreenFrameInput enterDownInput = BuildInputState(5u);
enterDownInput.viewportRect = firstInput.viewportRect;
XCEngine::UI::UIInputEvent enterDownEvent = {};
enterDownEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
enterDownEvent.keyCode = static_cast<std::int32_t>(KeyCode::Enter);
enterDownInput.events.push_back(enterDownEvent);
player.Update(enterDownInput);
const auto& afterEnterDown = host.GetInputDebugSnapshot();
EXPECT_NE(afterEnterDown.focusedStateKey.find("/focus-first"), std::string::npos);
EXPECT_NE(afterEnterDown.activeStateKey.find("/focus-first"), std::string::npos);
EXPECT_EQ(afterEnterDown.lastTargetKind, "Focused");
UIScreenFrameInput enterUpInput = BuildInputState(6u);
enterUpInput.viewportRect = firstInput.viewportRect;
XCEngine::UI::UIInputEvent enterUpEvent = {};
enterUpEvent.type = XCEngine::UI::UIInputEventType::KeyUp;
enterUpEvent.keyCode = static_cast<std::int32_t>(KeyCode::Enter);
enterUpInput.events.push_back(enterUpEvent);
player.Update(enterUpInput);
const auto& afterEnterUp = host.GetInputDebugSnapshot();
EXPECT_NE(afterEnterUp.focusedStateKey.find("/focus-first"), std::string::npos);
EXPECT_TRUE(afterEnterUp.activeStateKey.empty());
}
TEST(UIRuntimeTest, ScreenPlayerConsumeLastFrameReturnsDetachedPacketAndClearsBorrowedState) {
TempFileScope viewFile("xcui_runtime_consume_player", ".xcui", BuildViewMarkup("Runtime Consume"));
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.consume.player")));
const auto& firstFrame = player.Update(BuildInputState(2u));
ASSERT_TRUE(firstFrame.stats.documentLoaded);
EXPECT_EQ(firstFrame.stats.presentedFrameIndex, 2u);
EXPECT_TRUE(DrawDataContainsText(firstFrame.drawData, "Runtime Consume"));
XCEngine::UI::Runtime::UIScreenFrameResult consumedFrame = player.ConsumeLastFrame();
EXPECT_TRUE(consumedFrame.stats.documentLoaded);
EXPECT_EQ(consumedFrame.stats.presentedFrameIndex, 2u);
EXPECT_EQ(consumedFrame.stats.drawListCount, consumedFrame.drawData.GetDrawListCount());
EXPECT_EQ(consumedFrame.stats.commandCount, consumedFrame.drawData.GetTotalCommandCount());
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "Runtime Consume"));
EXPECT_EQ(player.GetPresentedFrameCount(), 1u);
const auto& clearedFrame = player.GetLastFrame();
EXPECT_FALSE(clearedFrame.stats.documentLoaded);
EXPECT_EQ(clearedFrame.stats.presentedFrameIndex, 0u);
EXPECT_EQ(clearedFrame.drawData.GetDrawListCount(), 0u);
EXPECT_TRUE(clearedFrame.errorMessage.empty());
const auto& secondFrame = player.Update(BuildInputState(3u));
EXPECT_TRUE(secondFrame.stats.documentLoaded);
EXPECT_EQ(secondFrame.stats.presentedFrameIndex, 3u);
EXPECT_TRUE(DrawDataContainsText(secondFrame.drawData, "Runtime Consume"));
EXPECT_EQ(player.GetPresentedFrameCount(), 2u);
EXPECT_EQ(consumedFrame.stats.presentedFrameIndex, 2u);
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "Runtime Consume"));
const XCEngine::UI::Runtime::UIScreenFrameResult emptyFrame = player.ConsumeLastFrame();
EXPECT_TRUE(emptyFrame.stats.documentLoaded);
EXPECT_EQ(emptyFrame.stats.presentedFrameIndex, 3u);
EXPECT_TRUE(DrawDataContainsText(emptyFrame.drawData, "Runtime Consume"));
const XCEngine::UI::Runtime::UIScreenFrameResult clearedAgain = player.ConsumeLastFrame();
EXPECT_FALSE(clearedAgain.stats.documentLoaded);
EXPECT_EQ(clearedAgain.stats.presentedFrameIndex, 0u);
EXPECT_EQ(clearedAgain.drawData.GetDrawListCount(), 0u);
}
TEST(UIRuntimeTest, UISystemForwardsActiveScreenToPlayer) {
TempFileScope baseView("xcui_runtime_base", ".xcui", BuildViewMarkup("Base Screen"));
TempFileScope overlayView("xcui_runtime_overlay", ".xcui", BuildViewMarkup("Overlay Screen", "Modal Dialog"));
UIDocumentScreenHost host = {};
UISystem system(host);
const auto baseLayer = system.PushScreen(
BuildScreenAsset(baseView.Path(), "runtime.base"));
ASSERT_NE(baseLayer, 0u);
XCEngine::UI::Runtime::UIScreenLayerOptions overlayOptions = {};
overlayOptions.debugName = "overlay";
overlayOptions.blocksLayersBelow = true;
const auto overlayLayer = system.PushScreen(
BuildScreenAsset(overlayView.Path(), "runtime.overlay"),
overlayOptions);
ASSERT_NE(overlayLayer, 0u);
const auto& frame = system.Update(BuildInputState(3u));
EXPECT_EQ(frame.presentedLayerCount, 1u);
EXPECT_EQ(frame.skippedLayerCount, 1u);
EXPECT_EQ(frame.layers.size(), 1u);
EXPECT_EQ(frame.layers.front().layerId, overlayLayer);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Overlay Screen"));
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Modal Dialog"));
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Base Screen"));
}
TEST(UIRuntimeTest, ScreenStackControllerAppliesHudAndMenuLayerPolicies) {
TempFileScope hudView("xcui_runtime_hud", ".xcui", BuildViewMarkup("HUD Screen"));
TempFileScope menuView("xcui_runtime_menu", ".xcui", BuildViewMarkup("Pause Menu", "Paused"));
UIDocumentScreenHost host = {};
UISystem system(host);
UIScreenStackController stack(system);
const auto hudLayer = stack.PushHud(BuildScreenAsset(hudView.Path(), "runtime.hud"), "hud");
const auto menuLayer = stack.PushMenu(BuildScreenAsset(menuView.Path(), "runtime.menu"), "menu");
ASSERT_NE(hudLayer, 0u);
ASSERT_NE(menuLayer, 0u);
ASSERT_EQ(stack.GetEntryCount(), 2u);
ASSERT_NE(stack.GetTop(), nullptr);
EXPECT_EQ(stack.GetTop()->layerId, menuLayer);
const auto* hudOptions = system.FindLayerOptions(hudLayer);
const auto* menuOptions = system.FindLayerOptions(menuLayer);
ASSERT_NE(hudOptions, nullptr);
ASSERT_NE(menuOptions, nullptr);
EXPECT_FALSE(hudOptions->acceptsInput);
EXPECT_FALSE(hudOptions->blocksLayersBelow);
EXPECT_TRUE(menuOptions->acceptsInput);
EXPECT_TRUE(menuOptions->blocksLayersBelow);
const auto& frame = system.Update(BuildInputState(4u));
EXPECT_EQ(frame.presentedLayerCount, 1u);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pause Menu"));
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "HUD Screen"));
}
TEST(UIRuntimeTest, ScreenStackControllerReplaceTopSwapsMenuContent) {
TempFileScope pauseView("xcui_runtime_pause", ".xcui", BuildViewMarkup("Pause Menu"));
TempFileScope settingsView("xcui_runtime_settings", ".xcui", BuildViewMarkup("Settings Menu"));
UIDocumentScreenHost host = {};
UISystem system(host);
UIScreenStackController stack(system);
const auto pauseLayer = stack.PushMenu(BuildScreenAsset(pauseView.Path(), "runtime.pause"), "pause");
ASSERT_NE(pauseLayer, 0u);
XCEngine::UI::Runtime::UIScreenLayerOptions replacementOptions = {};
replacementOptions.debugName = "settings";
replacementOptions.acceptsInput = true;
replacementOptions.blocksLayersBelow = true;
ASSERT_TRUE(stack.ReplaceTop(
BuildScreenAsset(settingsView.Path(), "runtime.settings"),
replacementOptions));
ASSERT_EQ(stack.GetEntryCount(), 1u);
ASSERT_NE(stack.GetTop(), nullptr);
EXPECT_EQ(stack.GetTop()->asset.screenId, "runtime.settings");
EXPECT_NE(stack.GetTop()->layerId, pauseLayer);
const auto& frame = system.Update(BuildInputState(5u));
EXPECT_EQ(frame.presentedLayerCount, 1u);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Settings Menu"));
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Pause Menu"));
}
TEST(UIRuntimeTest, ScreenStackControllerReplaceTopKeepsPreviousScreenWhenReplacementFails) {
TempFileScope pauseView("xcui_runtime_pause", ".xcui", BuildViewMarkup("Pause Menu"));
UIDocumentScreenHost host = {};
UISystem system(host);
UIScreenStackController stack(system);
const auto pauseLayer = stack.PushMenu(BuildScreenAsset(pauseView.Path(), "runtime.pause"), "pause");
ASSERT_NE(pauseLayer, 0u);
XCEngine::UI::Runtime::UIScreenLayerOptions replacementOptions = {};
replacementOptions.debugName = "broken";
replacementOptions.acceptsInput = true;
replacementOptions.blocksLayersBelow = true;
UIScreenAsset invalidAsset = {};
invalidAsset.screenId = "runtime.invalid";
invalidAsset.documentPath = (fs::temp_directory_path() / "xcui_missing_runtime_screen.xcui").string();
EXPECT_FALSE(stack.ReplaceTop(invalidAsset, replacementOptions));
ASSERT_EQ(stack.GetEntryCount(), 1u);
ASSERT_NE(stack.GetTop(), nullptr);
EXPECT_EQ(stack.GetTop()->layerId, pauseLayer);
EXPECT_EQ(stack.GetTop()->asset.screenId, "runtime.pause");
EXPECT_EQ(system.GetLayerCount(), 1u);
const auto& frame = system.Update(BuildInputState(6u));
EXPECT_EQ(frame.presentedLayerCount, 1u);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pause Menu"));
}
TEST(UIRuntimeTest, RoutesViewportAndFocusOnlyToTopInteractiveVisibleLayer) {
RecordingDocumentHost host = {};
UISystem system(host);
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
gameplayOptions.debugName = "gameplay";
gameplayOptions.acceptsInput = true;
gameplayOptions.blocksLayersBelow = false;
XCEngine::UI::Runtime::UIScreenLayerOptions overlayOptions = {};
overlayOptions.debugName = "overlay";
overlayOptions.acceptsInput = true;
overlayOptions.blocksLayersBelow = false;
const auto gameplayLayerId = system.PushScreen(
BuildScreenAsset(fs::path("gameplay_view.xcui"), "runtime.gameplay"),
gameplayOptions);
const auto overlayLayerId = system.PushScreen(
BuildScreenAsset(fs::path("overlay_view.xcui"), "runtime.overlay"),
overlayOptions);
ASSERT_NE(gameplayLayerId, 0u);
ASSERT_NE(overlayLayerId, 0u);
UIScreenFrameInput input = BuildInputState(8u);
input.viewportRect = XCEngine::UI::UIRect(15.0f, 25.0f, 1024.0f, 576.0f);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'I';
input.events.push_back(textEvent);
const auto& frame = system.Update(input);
ASSERT_EQ(frame.presentedLayerCount, 2u);
ASSERT_EQ(frame.layers.size(), 2u);
ASSERT_EQ(host.GetBuildCallCount(), 2u);
const auto* gameplayCall = host.FindBuildCall("runtime.gameplay");
const auto* overlayCall = host.FindBuildCall("runtime.overlay");
ASSERT_NE(gameplayCall, nullptr);
ASSERT_NE(overlayCall, nullptr);
EXPECT_EQ(gameplayCall->input.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(gameplayCall->input.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(gameplayCall->input.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(gameplayCall->input.viewportRect.height, input.viewportRect.height);
EXPECT_TRUE(gameplayCall->input.events.empty());
EXPECT_FALSE(gameplayCall->input.focused);
EXPECT_EQ(gameplayCall->input.frameIndex, input.frameIndex);
EXPECT_EQ(overlayCall->input.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(overlayCall->input.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(overlayCall->input.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(overlayCall->input.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(overlayCall->input.events.size(), 1u);
EXPECT_TRUE(overlayCall->input.focused);
EXPECT_EQ(overlayCall->input.frameIndex, input.frameIndex);
const auto* gameplayLayer = FindPresentedLayerById(frame, gameplayLayerId);
const auto* overlayLayer = FindPresentedLayerById(frame, overlayLayerId);
ASSERT_NE(gameplayLayer, nullptr);
ASSERT_NE(overlayLayer, nullptr);
EXPECT_EQ(gameplayLayer->stats.inputEventCount, 0u);
EXPECT_EQ(overlayLayer->stats.inputEventCount, 1u);
}
TEST(UIRuntimeTest, HiddenTopLayerLeavesUnderlyingLayerFocusedAndInteractive) {
RecordingDocumentHost host = {};
UISystem system(host);
XCEngine::UI::Runtime::UIScreenLayerOptions visibleOptions = {};
visibleOptions.debugName = "visible";
visibleOptions.acceptsInput = true;
visibleOptions.blocksLayersBelow = false;
XCEngine::UI::Runtime::UIScreenLayerOptions hiddenOptions = {};
hiddenOptions.debugName = "hidden";
hiddenOptions.visible = false;
hiddenOptions.acceptsInput = true;
hiddenOptions.blocksLayersBelow = false;
const auto visibleLayerId = system.PushScreen(
BuildScreenAsset(fs::path("visible_view.xcui"), "runtime.visible"),
visibleOptions);
const auto hiddenLayerId = system.PushScreen(
BuildScreenAsset(fs::path("hidden_view.xcui"), "runtime.hidden"),
hiddenOptions);
ASSERT_NE(visibleLayerId, 0u);
ASSERT_NE(hiddenLayerId, 0u);
UIScreenFrameInput input = BuildInputState(9u);
input.viewportRect = XCEngine::UI::UIRect(40.0f, 60.0f, 700.0f, 420.0f);
XCEngine::UI::UIInputEvent keyEvent = {};
keyEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
keyEvent.keyCode = 32;
input.events.push_back(keyEvent);
const auto& frame = system.Update(input);
ASSERT_EQ(frame.presentedLayerCount, 1u);
ASSERT_EQ(frame.skippedLayerCount, 1u);
ASSERT_EQ(frame.layers.size(), 1u);
ASSERT_EQ(host.GetBuildCallCount(), 1u);
const auto* visibleCall = host.FindBuildCall("runtime.visible");
ASSERT_NE(visibleCall, nullptr);
EXPECT_EQ(visibleCall->input.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(visibleCall->input.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(visibleCall->input.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(visibleCall->input.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(visibleCall->input.events.size(), 1u);
EXPECT_TRUE(visibleCall->input.focused);
EXPECT_EQ(host.FindBuildCall("runtime.hidden"), nullptr);
const auto* visibleLayer = FindPresentedLayerById(frame, visibleLayerId);
ASSERT_NE(visibleLayer, nullptr);
EXPECT_EQ(visibleLayer->stats.inputEventCount, 1u);
EXPECT_EQ(FindPresentedLayerById(frame, hiddenLayerId), nullptr);
}
TEST(UIRuntimeTest, UISystemConsumeLastFrameReturnsDetachedPresentationPacket) {
RecordingDocumentHost host = {};
UISystem system(host);
XCEngine::UI::Runtime::UIScreenLayerOptions options = {};
options.debugName = "runtime";
const auto layerId = system.PushScreen(
BuildScreenAsset(fs::path("runtime_consume_view.xcui"), "runtime.consume"),
options);
ASSERT_NE(layerId, 0u);
UIScreenFrameInput input = BuildInputState(12u);
input.viewportRect = XCEngine::UI::UIRect(48.0f, 72.0f, 1280.0f, 720.0f);
input.deltaTimeSeconds = 1.0 / 30.0;
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'R';
input.events.push_back(textEvent);
const auto& frame = system.Update(input);
ASSERT_EQ(frame.presentedLayerCount, 1u);
EXPECT_EQ(frame.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(frame.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(frame.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(frame.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(frame.submittedInputEventCount, 1u);
EXPECT_DOUBLE_EQ(frame.deltaTimeSeconds, input.deltaTimeSeconds);
EXPECT_TRUE(frame.focused);
XCEngine::UI::Runtime::UISystemFrameResult consumedFrame = system.ConsumeLastFrame();
EXPECT_EQ(consumedFrame.frameIndex, input.frameIndex);
EXPECT_EQ(consumedFrame.presentedLayerCount, 1u);
EXPECT_EQ(consumedFrame.layers.size(), 1u);
EXPECT_EQ(consumedFrame.layers.front().layerId, layerId);
EXPECT_EQ(consumedFrame.viewportRect.x, input.viewportRect.x);
EXPECT_EQ(consumedFrame.viewportRect.y, input.viewportRect.y);
EXPECT_EQ(consumedFrame.viewportRect.width, input.viewportRect.width);
EXPECT_EQ(consumedFrame.viewportRect.height, input.viewportRect.height);
EXPECT_EQ(consumedFrame.submittedInputEventCount, 1u);
EXPECT_DOUBLE_EQ(consumedFrame.deltaTimeSeconds, input.deltaTimeSeconds);
EXPECT_TRUE(consumedFrame.focused);
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "runtime.consume"));
const auto& clearedFrame = system.GetLastFrame();
EXPECT_EQ(clearedFrame.frameIndex, 0u);
EXPECT_EQ(clearedFrame.presentedLayerCount, 0u);
EXPECT_EQ(clearedFrame.submittedInputEventCount, 0u);
EXPECT_TRUE(clearedFrame.layers.empty());
EXPECT_EQ(clearedFrame.drawData.GetDrawListCount(), 0u);
const XCEngine::UI::Runtime::UISystemFrameResult emptyFrame = system.ConsumeLastFrame();
EXPECT_EQ(emptyFrame.frameIndex, 0u);
EXPECT_TRUE(emptyFrame.layers.empty());
EXPECT_EQ(emptyFrame.drawData.GetDrawListCount(), 0u);
}
TEST(UIRuntimeTest, SceneRuntimeContextConsumeLastFrameForwardsPresentationSnapshot) {
TempFileScope viewFile("xcui_runtime_context", ".xcui", BuildViewMarkup("Runtime Context"));
UISceneRuntimeContext runtimeContext = {};
const auto layerId = runtimeContext.GetStackController().PushMenu(
BuildScreenAsset(viewFile.Path(), "runtime.context"),
"context");
ASSERT_NE(layerId, 0u);
const XCEngine::UI::UIRect viewportRect(24.0f, 32.0f, 960.0f, 540.0f);
runtimeContext.SetViewportRect(viewportRect);
runtimeContext.SetFocused(true);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'C';
runtimeContext.QueueInputEvent(textEvent);
runtimeContext.Update(0.25);
XCEngine::UI::Runtime::UISystemFrameResult consumedFrame = runtimeContext.ConsumeLastFrame();
EXPECT_EQ(consumedFrame.frameIndex, 1u);
EXPECT_EQ(consumedFrame.presentedLayerCount, 1u);
EXPECT_EQ(consumedFrame.layers.size(), 1u);
EXPECT_EQ(consumedFrame.layers.front().layerId, layerId);
EXPECT_EQ(consumedFrame.viewportRect.x, viewportRect.x);
EXPECT_EQ(consumedFrame.viewportRect.y, viewportRect.y);
EXPECT_EQ(consumedFrame.viewportRect.width, viewportRect.width);
EXPECT_EQ(consumedFrame.viewportRect.height, viewportRect.height);
EXPECT_EQ(consumedFrame.submittedInputEventCount, 1u);
EXPECT_DOUBLE_EQ(consumedFrame.deltaTimeSeconds, 0.25);
EXPECT_TRUE(consumedFrame.focused);
EXPECT_TRUE(DrawDataContainsText(consumedFrame.drawData, "Runtime Context"));
const auto& clearedFrame = runtimeContext.GetLastFrame();
EXPECT_EQ(clearedFrame.frameIndex, 0u);
EXPECT_EQ(clearedFrame.presentedLayerCount, 0u);
EXPECT_TRUE(clearedFrame.layers.empty());
EXPECT_EQ(clearedFrame.drawData.GetDrawListCount(), 0u);
runtimeContext.Update(0.5);
const auto& secondFrame = runtimeContext.GetLastFrame();
EXPECT_EQ(secondFrame.frameIndex, 2u);
EXPECT_EQ(secondFrame.submittedInputEventCount, 0u);
EXPECT_DOUBLE_EQ(secondFrame.deltaTimeSeconds, 0.5);
EXPECT_TRUE(secondFrame.focused);
}

View File

@@ -1,32 +0,0 @@
# ============================================================
# Input Module Tests
# ============================================================
set(INPUT_TEST_SOURCES
test_input_manager.cpp
test_windows_input_module.cpp
test_xcui_input_dispatcher.cpp
)
add_executable(input_tests ${INPUT_TEST_SOURCES})
if(MSVC)
set_target_properties(input_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
endif()
target_link_libraries(input_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(input_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/tests/Fixtures
)
include(GoogleTest)
gtest_discover_tests(input_tests)

View File

@@ -1,464 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Input/InputManager.h>
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/Input/InputEvent.h>
#include <XCEngine/Input/InputAxis.h>
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Input;
using namespace XCEngine::Math;
using namespace XCEngine::Containers;
namespace {
TEST(InputManager, Singleton) {
InputManager& mgr1 = InputManager::Get();
InputManager& mgr2 = InputManager::Get();
EXPECT_EQ(&mgr1, &mgr2);
}
TEST(InputManager, InitializeShutdown) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
mgr.Shutdown();
}
TEST(InputManager, IsKeyDown) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
EXPECT_FALSE(mgr.IsKeyDown(KeyCode::A));
mgr.ProcessKeyDown(KeyCode::A, false, false, false, false, false);
EXPECT_TRUE(mgr.IsKeyDown(KeyCode::A));
mgr.ProcessKeyUp(KeyCode::A, false, false, false, false);
EXPECT_FALSE(mgr.IsKeyDown(KeyCode::A));
mgr.Shutdown();
}
TEST(InputManager, IsKeyPressed) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
EXPECT_FALSE(mgr.IsKeyPressed(KeyCode::A));
mgr.ProcessKeyDown(KeyCode::A, false, false, false, false, false);
EXPECT_TRUE(mgr.IsKeyPressed(KeyCode::A));
mgr.Update(0.016f);
EXPECT_FALSE(mgr.IsKeyPressed(KeyCode::A));
mgr.Shutdown();
}
TEST(InputManager, IsKeyUp) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
EXPECT_TRUE(mgr.IsKeyUp(KeyCode::A));
mgr.ProcessKeyDown(KeyCode::A, false, false, false, false, false);
EXPECT_FALSE(mgr.IsKeyUp(KeyCode::A));
mgr.ProcessKeyUp(KeyCode::A, false, false, false, false);
EXPECT_TRUE(mgr.IsKeyUp(KeyCode::A));
mgr.Shutdown();
}
TEST(InputManager, KeyEventModifiers) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
bool eventFired = false;
KeyEvent capturedEvent{};
uint64_t id = mgr.OnKeyEvent().Subscribe([&eventFired, &capturedEvent](const KeyEvent& e) {
eventFired = true;
capturedEvent = e;
});
mgr.ProcessKeyDown(KeyCode::A, false, true, true, false, false);
EXPECT_TRUE(eventFired);
EXPECT_EQ(capturedEvent.keyCode, KeyCode::A);
EXPECT_TRUE(capturedEvent.alt);
EXPECT_TRUE(capturedEvent.ctrl);
EXPECT_FALSE(capturedEvent.shift);
EXPECT_FALSE(capturedEvent.meta);
EXPECT_EQ(capturedEvent.type, KeyEvent::Type::Down);
mgr.OnKeyEvent().Unsubscribe(id);
mgr.Shutdown();
}
TEST(InputManager, KeyEventRepeat) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
bool eventFired = false;
KeyEvent capturedEvent{};
uint64_t id = mgr.OnKeyEvent().Subscribe([&eventFired, &capturedEvent](const KeyEvent& e) {
eventFired = true;
capturedEvent = e;
});
mgr.ProcessKeyDown(KeyCode::A, true, false, false, false, false);
EXPECT_TRUE(eventFired);
EXPECT_EQ(capturedEvent.type, KeyEvent::Type::Repeat);
mgr.OnKeyEvent().Unsubscribe(id);
mgr.Shutdown();
}
TEST(InputManager, MousePosition) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
mgr.ProcessMouseMove(100, 200, 10, -5);
Vector2 pos = mgr.GetMousePosition();
EXPECT_EQ(pos.x, 100.0f);
EXPECT_EQ(pos.y, 200.0f);
Vector2 delta = mgr.GetMouseDelta();
EXPECT_EQ(delta.x, 10.0f);
EXPECT_EQ(delta.y, -5.0f);
mgr.Shutdown();
}
TEST(InputManager, MouseButton) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
EXPECT_FALSE(mgr.IsMouseButtonDown(MouseButton::Left));
mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200);
EXPECT_TRUE(mgr.IsMouseButtonDown(MouseButton::Left));
mgr.ProcessMouseButton(MouseButton::Left, false, 100, 200);
EXPECT_FALSE(mgr.IsMouseButtonDown(MouseButton::Left));
mgr.Shutdown();
}
TEST(InputManager, MouseButtonClicked) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
EXPECT_FALSE(mgr.IsMouseButtonClicked(MouseButton::Left));
mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200);
EXPECT_TRUE(mgr.IsMouseButtonClicked(MouseButton::Left));
mgr.Update(0.016f);
EXPECT_FALSE(mgr.IsMouseButtonClicked(MouseButton::Left));
mgr.Shutdown();
}
TEST(InputManager, MouseButtonEvent) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
bool eventFired = false;
MouseButtonEvent capturedEvent{};
uint64_t id = mgr.OnMouseButton().Subscribe([&eventFired, &capturedEvent](const MouseButtonEvent& e) {
eventFired = true;
capturedEvent = e;
});
mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200);
EXPECT_TRUE(eventFired);
EXPECT_EQ(capturedEvent.button, MouseButton::Left);
EXPECT_EQ(capturedEvent.position.x, 100.0f);
EXPECT_EQ(capturedEvent.position.y, 200.0f);
EXPECT_EQ(capturedEvent.type, MouseButtonEvent::Pressed);
mgr.OnMouseButton().Unsubscribe(id);
mgr.Shutdown();
}
TEST(InputManager, MouseMoveEvent) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
bool eventFired = false;
MouseMoveEvent capturedEvent{};
uint64_t id = mgr.OnMouseMove().Subscribe([&eventFired, &capturedEvent](const MouseMoveEvent& e) {
eventFired = true;
capturedEvent = e;
});
mgr.ProcessMouseMove(100, 200, 10, -5);
EXPECT_TRUE(eventFired);
EXPECT_EQ(capturedEvent.position.x, 100.0f);
EXPECT_EQ(capturedEvent.position.y, 200.0f);
EXPECT_EQ(capturedEvent.delta.x, 10.0f);
EXPECT_EQ(capturedEvent.delta.y, -5.0f);
mgr.OnMouseMove().Unsubscribe(id);
mgr.Shutdown();
}
TEST(InputManager, MouseWheelEvent) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
bool eventFired = false;
MouseWheelEvent capturedEvent{};
uint64_t id = mgr.OnMouseWheel().Subscribe([&eventFired, &capturedEvent](const MouseWheelEvent& e) {
eventFired = true;
capturedEvent = e;
});
mgr.ProcessMouseWheel(1.0f, 100, 200);
EXPECT_TRUE(eventFired);
EXPECT_EQ(capturedEvent.delta, 1.0f);
mgr.OnMouseWheel().Unsubscribe(id);
mgr.Shutdown();
}
TEST(InputManager, DefaultAxes) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
float h = mgr.GetAxis("Horizontal");
EXPECT_GE(h, -1.0f);
EXPECT_LE(h, 1.0f);
float v = mgr.GetAxis("Vertical");
EXPECT_GE(v, -1.0f);
EXPECT_LE(v, 1.0f);
mgr.Shutdown();
}
TEST(InputManager, DefaultButtons) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
EXPECT_FALSE(mgr.GetButton("Jump"));
mgr.ProcessKeyDown(KeyCode::Space, false, false, false, false, false);
EXPECT_TRUE(mgr.GetButton("Jump"));
mgr.ProcessKeyUp(KeyCode::Space, false, false, false, false);
EXPECT_FALSE(mgr.GetButton("Jump"));
mgr.Shutdown();
}
TEST(InputManager, GetButtonDown) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
EXPECT_FALSE(mgr.GetButtonDown("Fire1"));
mgr.ProcessKeyDown(KeyCode::LeftCtrl, false, false, false, false, false);
EXPECT_TRUE(mgr.GetButtonDown("Fire1"));
mgr.Update(0.016f);
EXPECT_FALSE(mgr.GetButtonDown("Fire1"));
mgr.Shutdown();
}
TEST(InputManager, GetButtonUp) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
mgr.ProcessKeyDown(KeyCode::LeftCtrl, false, false, false, false, false);
EXPECT_TRUE(mgr.GetButton("Fire1"));
mgr.ProcessKeyUp(KeyCode::LeftCtrl, false, false, false, false);
EXPECT_TRUE(mgr.GetButtonUp("Fire1"));
mgr.Shutdown();
}
TEST(InputManager, AnyKeyIncludesKeyboardAndMouseButtons) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
EXPECT_FALSE(mgr.IsAnyKeyDown());
mgr.ProcessKeyDown(KeyCode::A, false, false, false, false, false);
EXPECT_TRUE(mgr.IsAnyKeyDown());
mgr.ProcessKeyUp(KeyCode::A, false, false, false, false);
EXPECT_FALSE(mgr.IsAnyKeyDown());
mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200);
EXPECT_TRUE(mgr.IsAnyKeyDown());
mgr.ProcessMouseButton(MouseButton::Left, false, 100, 200);
EXPECT_FALSE(mgr.IsAnyKeyDown());
mgr.Shutdown();
}
TEST(InputManager, AnyKeyPressedIsFrameScoped) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
EXPECT_FALSE(mgr.IsAnyKeyPressed());
mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200);
EXPECT_TRUE(mgr.IsAnyKeyPressed());
mgr.Update(0.016f);
EXPECT_FALSE(mgr.IsAnyKeyPressed());
EXPECT_TRUE(mgr.IsAnyKeyDown());
mgr.ProcessMouseButton(MouseButton::Left, false, 100, 200);
EXPECT_FALSE(mgr.IsAnyKeyPressed());
EXPECT_FALSE(mgr.IsAnyKeyDown());
mgr.Shutdown();
}
TEST(InputManager, RegisterAxis) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
mgr.ClearAxes();
InputAxis axis("TestAxis", KeyCode::W, KeyCode::S);
mgr.RegisterAxis(axis);
EXPECT_EQ(mgr.GetAxis("TestAxis"), 0.0f);
mgr.ProcessKeyDown(KeyCode::W, false, false, false, false, false);
EXPECT_EQ(mgr.GetAxis("TestAxis"), 1.0f);
mgr.ProcessKeyUp(KeyCode::W, false, false, false, false);
EXPECT_EQ(mgr.GetAxis("TestAxis"), 0.0f);
mgr.ProcessKeyDown(KeyCode::S, false, false, false, false, false);
EXPECT_EQ(mgr.GetAxis("TestAxis"), -1.0f);
mgr.Shutdown();
}
TEST(InputManager, GetAxisRaw) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
mgr.ClearAxes();
InputAxis axis("RawTest", KeyCode::D, KeyCode::A);
mgr.RegisterAxis(axis);
mgr.ProcessKeyDown(KeyCode::D, false, false, false, false, false);
EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 1.0f);
mgr.Update(0.016f);
EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 1.0f);
mgr.Shutdown();
}
TEST(InputManager, TextInputEvent) {
InputManager& mgr = InputManager::Get();
mgr.Initialize(nullptr);
bool eventFired = false;
TextInputEvent capturedEvent{};
uint64_t id = mgr.OnTextInput().Subscribe([&eventFired, &capturedEvent](const TextInputEvent& e) {
eventFired = true;
capturedEvent = e;
});
mgr.ProcessTextInput('A');
EXPECT_TRUE(eventFired);
EXPECT_EQ(capturedEvent.character, 'A');
mgr.OnTextInput().Unsubscribe(id);
mgr.Shutdown();
}
TEST(InputAxis, DefaultConstruction) {
InputAxis axis;
EXPECT_EQ(axis.GetValue(), 0.0f);
EXPECT_EQ(axis.GetPositiveKey(), KeyCode::None);
EXPECT_EQ(axis.GetNegativeKey(), KeyCode::None);
}
TEST(InputAxis, PositiveNegativeKeys) {
InputAxis axis("Test", KeyCode::W, KeyCode::S);
EXPECT_EQ(axis.GetPositiveKey(), KeyCode::W);
EXPECT_EQ(axis.GetNegativeKey(), KeyCode::S);
}
TEST(InputAxis, SetValue) {
InputAxis axis;
axis.SetValue(0.5f);
EXPECT_EQ(axis.GetValue(), 0.5f);
}
TEST(InputEvent, KeyEventConstruction) {
KeyEvent event;
event.keyCode = KeyCode::A;
event.alt = false;
event.ctrl = false;
event.shift = false;
event.meta = false;
event.type = KeyEvent::Type::Down;
EXPECT_EQ(event.keyCode, KeyCode::A);
EXPECT_FALSE(event.alt);
EXPECT_FALSE(event.ctrl);
EXPECT_FALSE(event.shift);
EXPECT_FALSE(event.meta);
EXPECT_EQ(event.type, KeyEvent::Type::Down);
}
TEST(InputEvent, MouseButtonEventConstruction) {
MouseButtonEvent event;
event.button = MouseButton::Left;
event.position.x = 100.0f;
event.position.y = 200.0f;
event.type = MouseButtonEvent::Pressed;
EXPECT_EQ(event.button, MouseButton::Left);
EXPECT_EQ(event.position.x, 100.0f);
EXPECT_EQ(event.position.y, 200.0f);
EXPECT_EQ(event.type, MouseButtonEvent::Pressed);
}
TEST(InputEvent, MouseMoveEventConstruction) {
MouseMoveEvent event;
event.position.x = 100.0f;
event.position.y = 200.0f;
event.delta.x = 10.0f;
event.delta.y = -5.0f;
EXPECT_EQ(event.position.x, 100.0f);
EXPECT_EQ(event.position.y, 200.0f);
EXPECT_EQ(event.delta.x, 10.0f);
EXPECT_EQ(event.delta.y, -5.0f);
}
TEST(InputEvent, MouseWheelEventConstruction) {
MouseWheelEvent event;
event.position.x = 100.0f;
event.position.y = 200.0f;
event.delta = 1.5f;
EXPECT_EQ(event.position.x, 100.0f);
EXPECT_EQ(event.position.y, 200.0f);
EXPECT_EQ(event.delta, 1.5f);
}
TEST(InputEvent, TextInputEventConstruction) {
TextInputEvent event;
event.character = 'X';
EXPECT_EQ(event.character, 'X');
}
} // namespace

View File

@@ -1,194 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Input/InputManager.h>
#include <XCEngine/Input/InputModule.h>
#include <XCEngine/Platform/Windows/WindowsInputModule.h>
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Containers/String.h>
using namespace XCEngine::Input;
using namespace XCEngine::Math;
using namespace XCEngine::Containers;
using namespace XCEngine::Input::Platform;
namespace {
TEST(WindowsInputModule, Construction) {
WindowsInputModule module;
}
TEST(WindowsInputModule, InitializeShutdown) {
WindowsInputModule module;
module.Initialize(nullptr);
module.Shutdown();
}
TEST(WindowsInputModule, HandleKeyDown) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module;
module.Initialize(nullptr);
EXPECT_FALSE(InputManager::Get().IsKeyDown(KeyCode::A));
module.HandleMessage(0, 0x0100, 'A', 0);
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::A));
module.Shutdown();
InputManager::Get().Shutdown();
}
TEST(WindowsInputModule, HandleKeyUp) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module;
module.Initialize(nullptr);
module.HandleMessage(0, 0x0100, 'A', 0);
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::A));
module.HandleMessage(0, 0x0101, 'A', 0);
EXPECT_FALSE(InputManager::Get().IsKeyDown(KeyCode::A));
module.Shutdown();
InputManager::Get().Shutdown();
}
TEST(WindowsInputModule, HandleMouseMove) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module;
module.Initialize(nullptr);
module.HandleMessage(0, 0x0200, 0, 0x00320078);
Vector2 pos = InputManager::Get().GetMousePosition();
EXPECT_EQ(pos.x, 120.0f);
EXPECT_EQ(pos.y, 50.0f);
module.Shutdown();
InputManager::Get().Shutdown();
}
TEST(WindowsInputModule, HandleLeftMouseButtonDown) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module;
module.Initialize(nullptr);
module.HandleMessage(0, 0x0201, 0, 0x00320078);
EXPECT_TRUE(InputManager::Get().IsMouseButtonDown(MouseButton::Left));
module.Shutdown();
InputManager::Get().Shutdown();
}
TEST(WindowsInputModule, HandleLeftMouseButtonUp) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module;
module.Initialize(nullptr);
module.HandleMessage(0, 0x0201, 0, 0x00320078);
EXPECT_TRUE(InputManager::Get().IsMouseButtonDown(MouseButton::Left));
module.HandleMessage(0, 0x0202, 0, 0x00320078);
EXPECT_FALSE(InputManager::Get().IsMouseButtonDown(MouseButton::Left));
module.Shutdown();
InputManager::Get().Shutdown();
}
TEST(WindowsInputModule, HandleRightMouseButton) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module;
module.Initialize(nullptr);
module.HandleMessage(0, 0x0204, 0, 0x00320078);
EXPECT_TRUE(InputManager::Get().IsMouseButtonDown(MouseButton::Right));
module.HandleMessage(0, 0x0205, 0, 0x00320078);
EXPECT_FALSE(InputManager::Get().IsMouseButtonDown(MouseButton::Right));
module.Shutdown();
InputManager::Get().Shutdown();
}
TEST(WindowsInputModule, HandleMiddleMouseButton) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module;
module.Initialize(nullptr);
module.HandleMessage(0, 0x0207, 0, 0x00320078);
EXPECT_TRUE(InputManager::Get().IsMouseButtonDown(MouseButton::Middle));
module.HandleMessage(0, 0x0208, 0, 0x00320078);
EXPECT_FALSE(InputManager::Get().IsMouseButtonDown(MouseButton::Middle));
module.Shutdown();
InputManager::Get().Shutdown();
}
TEST(WindowsInputModule, HandleMouseWheel) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module;
module.Initialize(nullptr);
module.HandleMessage(0, 0x020A, 0x00030000, 0x00780078);
float scrollDelta = InputManager::Get().GetMouseScrollDelta();
EXPECT_EQ(scrollDelta, 0.025f);
module.Shutdown();
InputManager::Get().Shutdown();
}
TEST(WindowsInputModule, HandleTextInput) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module;
module.Initialize(nullptr);
bool eventFired = false;
char capturedChar = 0;
uint64_t id = InputManager::Get().OnTextInput().Subscribe([&eventFired, &capturedChar](const TextInputEvent& e) {
eventFired = true;
capturedChar = e.character;
});
module.HandleMessage(0, 0x0102, 'X', 0);
EXPECT_TRUE(eventFired);
EXPECT_EQ(capturedChar, 'X');
InputManager::Get().OnTextInput().Unsubscribe(id);
module.Shutdown();
InputManager::Get().Shutdown();
}
TEST(WindowsInputModule, MultipleModules) {
InputManager::Get().Initialize(nullptr);
WindowsInputModule module1;
WindowsInputModule module2;
module1.Initialize(nullptr);
module2.Initialize(nullptr);
module1.HandleMessage(0, 0x0100, 'A', 0);
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::A));
module2.HandleMessage(0, 0x0100, 'B', 0);
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::B));
module1.Shutdown();
module2.Shutdown();
InputManager::Get().Shutdown();
}
} // namespace

View File

@@ -1,222 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/Input/UIFocusController.h>
#include <XCEngine/UI/Input/UIInputDispatcher.h>
#include <XCEngine/UI/Input/UIInputRouter.h>
#include <XCEngine/UI/Input/UIShortcutRegistry.h>
#include <cstdint>
#include <string>
#include <vector>
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIElementId;
using XCEngine::UI::UIFocusController;
using XCEngine::UI::UIInputDispatchDecision;
using XCEngine::UI::UIInputDispatcher;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIInputPath;
using XCEngine::UI::UIInputRouteContext;
using XCEngine::UI::UIInputRouter;
using XCEngine::UI::UIInputRoutingPhase;
using XCEngine::UI::UIInputTargetKind;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIShortcutBinding;
using XCEngine::UI::UIShortcutContext;
using XCEngine::UI::UIShortcutRegistry;
using XCEngine::UI::UIShortcutScope;
std::string BuildTraceLabel(
UIElementId elementId,
UIInputRoutingPhase phase) {
char phaseChar = 'T';
switch (phase) {
case UIInputRoutingPhase::Capture:
phaseChar = 'C';
break;
case UIInputRoutingPhase::Bubble:
phaseChar = 'B';
break;
case UIInputRoutingPhase::Target:
default:
phaseChar = 'T';
break;
}
return std::to_string(elementId) + phaseChar;
}
} // namespace
TEST(XCUIFocusControllerTest, SetFocusedPathTracksLostAndGainedSuffixes) {
UIFocusController controller = {};
EXPECT_FALSE(controller.HasFocus());
const auto initialChange = controller.SetFocusedPath({ 1u, 2u, 3u });
EXPECT_TRUE(initialChange.Changed());
EXPECT_EQ(initialChange.gainedPath.elements, (std::vector<UIElementId>{ 1u, 2u, 3u }));
EXPECT_TRUE(initialChange.lostPath.elements.empty());
EXPECT_TRUE(controller.HasFocus());
const auto change = controller.SetFocusedPath({ 1u, 4u });
EXPECT_TRUE(change.Changed());
EXPECT_EQ(change.previousPath.elements, (std::vector<UIElementId>{ 1u, 2u, 3u }));
EXPECT_EQ(change.currentPath.elements, (std::vector<UIElementId>{ 1u, 4u }));
EXPECT_EQ(change.lostPath.elements, (std::vector<UIElementId>{ 2u, 3u }));
EXPECT_EQ(change.gainedPath.elements, (std::vector<UIElementId>{ 4u }));
}
TEST(XCUIInputRouterTest, PointerCaptureOverridesHoveredPath) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerMove;
UIInputRouteContext context = {};
context.hoveredPath = { 10u, 20u, 30u };
context.capturePath = { 90u, 100u };
const auto plan = UIInputRouter::BuildRoutingPlan(event, context);
EXPECT_EQ(plan.targetKind, UIInputTargetKind::Captured);
EXPECT_EQ(plan.targetPath.elements, (std::vector<UIElementId>{ 90u, 100u }));
ASSERT_EQ(plan.steps.size(), 3u);
EXPECT_EQ(plan.steps[0].elementId, 90u);
EXPECT_EQ(plan.steps[0].phase, UIInputRoutingPhase::Capture);
EXPECT_EQ(plan.steps[1].elementId, 100u);
EXPECT_EQ(plan.steps[1].phase, UIInputRoutingPhase::Target);
EXPECT_EQ(plan.steps[2].elementId, 90u);
EXPECT_EQ(plan.steps[2].phase, UIInputRoutingPhase::Bubble);
}
TEST(XCUIInputRouterTest, KeyboardEventsRouteThroughFocusedPathInCaptureTargetBubbleOrder) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(KeyCode::Enter);
UIInputRouteContext context = {};
context.focusedPath = { 1u, 2u, 3u };
std::vector<std::string> trace = {};
const auto result = UIInputRouter::Dispatch(
event,
context,
[&trace](const auto& request) {
trace.push_back(BuildTraceLabel(request.elementId, request.phase));
return UIInputDispatchDecision{};
});
EXPECT_FALSE(result.handled);
EXPECT_EQ(trace, (std::vector<std::string>{ "1C", "2C", "3T", "2B", "1B" }));
}
TEST(XCUIShortcutRegistryTest, MatchingPrefersMostSpecificScopeThenNewestBinding) {
UIShortcutRegistry registry = {};
UIShortcutBinding globalBinding = {};
globalBinding.scope = UIShortcutScope::Global;
globalBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::S);
globalBinding.chord.modifiers.control = true;
globalBinding.commandId = "save.global";
registry.RegisterBinding(globalBinding);
UIShortcutBinding panelBinding = {};
panelBinding.scope = UIShortcutScope::Panel;
panelBinding.ownerId = 20u;
panelBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::S);
panelBinding.chord.modifiers.control = true;
panelBinding.commandId = "save.panel";
registry.RegisterBinding(panelBinding);
UIShortcutBinding widgetBinding = {};
widgetBinding.scope = UIShortcutScope::Widget;
widgetBinding.ownerId = 30u;
widgetBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::S);
widgetBinding.chord.modifiers.control = true;
widgetBinding.commandId = "save.widget";
registry.RegisterBinding(widgetBinding);
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(KeyCode::S);
event.modifiers.control = true;
UIShortcutContext context = {};
context.focusedPath = { 10u, 20u, 30u };
EXPECT_EQ(registry.Match(event, context).binding.commandId, "save.widget");
context.focusedPath = { 10u, 20u };
EXPECT_EQ(registry.Match(event, context).binding.commandId, "save.panel");
context.focusedPath.Clear();
EXPECT_EQ(registry.Match(event, context).binding.commandId, "save.global");
}
TEST(XCUIInputDispatcherTest, PointerDownTransfersFocusAndMaintainsActivePathUntilPointerUp) {
UIInputDispatcher dispatcher;
UIInputEvent pointerDown = {};
pointerDown.type = UIInputEventType::PointerButtonDown;
pointerDown.pointerButton = UIPointerButton::Left;
const auto downSummary = dispatcher.Dispatch(
pointerDown,
UIInputPath{ 100u, 200u },
[](const auto&) {
return UIInputDispatchDecision{};
});
EXPECT_TRUE(downSummary.focusChange.Changed());
EXPECT_EQ(dispatcher.GetFocusController().GetFocusedPath().elements, (std::vector<UIElementId>{ 100u, 200u }));
EXPECT_EQ(dispatcher.GetFocusController().GetActivePath().elements, (std::vector<UIElementId>{ 100u, 200u }));
EXPECT_EQ(downSummary.routing.plan.targetKind, UIInputTargetKind::Hovered);
UIInputEvent pointerUp = {};
pointerUp.type = UIInputEventType::PointerButtonUp;
pointerUp.pointerButton = UIPointerButton::Left;
dispatcher.Dispatch(
pointerUp,
UIInputPath{ 100u, 200u },
[](const auto&) {
return UIInputDispatchDecision{};
});
EXPECT_TRUE(dispatcher.GetFocusController().GetActivePath().Empty());
EXPECT_EQ(dispatcher.GetFocusController().GetFocusedPath().elements, (std::vector<UIElementId>{ 100u, 200u }));
}
TEST(XCUIInputDispatcherTest, ShortcutMatchConsumesKeyboardDispatchBeforeRouting) {
UIInputDispatcher dispatcher;
dispatcher.GetFocusController().SetFocusedPath({ 1u, 2u, 3u });
UIShortcutBinding binding = {};
binding.scope = UIShortcutScope::Widget;
binding.ownerId = 3u;
binding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
binding.chord.modifiers.control = true;
binding.commandId = "palette.open";
dispatcher.GetShortcutRegistry().RegisterBinding(binding);
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(KeyCode::P);
event.modifiers.control = true;
bool handlerCalled = false;
const auto summary = dispatcher.Dispatch(
event,
{},
[&handlerCalled](const auto&) {
handlerCalled = true;
UIInputDispatchDecision decision = {};
decision.handled = true;
return decision;
});
EXPECT_TRUE(summary.shortcutHandled);
EXPECT_EQ(summary.commandId, "palette.open");
EXPECT_FALSE(handlerCalled);
EXPECT_FALSE(summary.routing.plan.HasTargetPath());
}

View File

@@ -1,32 +0,0 @@
# ============================================================
# Physics Tests
# ============================================================
set(PHYSICS_TEST_SOURCES
test_physics_world.cpp
)
add_executable(physics_tests ${PHYSICS_TEST_SOURCES})
if(MSVC)
set_target_properties(physics_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
endif()
target_link_libraries(physics_tests PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(physics_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
)
if(WIN32 AND XCENGINE_ENABLE_PHYSX)
xcengine_copy_physx_runtime_dlls(physics_tests)
endif()
include(GoogleTest)
gtest_discover_tests(physics_tests)

View File

@@ -1,467 +0,0 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/BoxColliderComponent.h>
#include <XCEngine/Components/CapsuleColliderComponent.h>
#include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/RigidbodyComponent.h>
#include <XCEngine/Components/SphereColliderComponent.h>
#include <XCEngine/Physics/PhysicsWorld.h>
#include <XCEngine/Scene/Scene.h>
namespace {
TEST(PhysicsWorld_Test, DefaultWorldStartsUninitialized) {
XCEngine::Physics::PhysicsWorld world;
EXPECT_FALSE(world.IsInitialized());
EXPECT_EQ(world.GetNativeActorCount(), 0u);
EXPECT_EQ(world.GetNativeShapeCount(), 0u);
}
TEST(PhysicsWorld_Test, InitializeStoresCreateInfoWithoutMarkingWorldReadyYet) {
XCEngine::Components::Scene scene("PhysicsScene");
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo;
createInfo.scene = &scene;
createInfo.gravity = XCEngine::Math::Vector3(0.0f, -3.5f, 0.0f);
const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable();
EXPECT_EQ(world.Initialize(createInfo), expectedInitialized);
EXPECT_EQ(world.IsInitialized(), expectedInitialized);
EXPECT_EQ(world.GetCreateInfo().scene, &scene);
EXPECT_EQ(world.GetCreateInfo().gravity, XCEngine::Math::Vector3(0.0f, -3.5f, 0.0f));
}
TEST(PhysicsWorld_Test, StepWithoutInitializationIsNoOp) {
XCEngine::Physics::PhysicsWorld world;
world.Step(0.016f);
EXPECT_FALSE(world.IsInitialized());
}
TEST(PhysicsWorld_Test, InitializeBuildsNativeActorsFromSceneComponents) {
using namespace XCEngine::Components;
Scene scene("PhysicsScene");
GameObject* ground = scene.CreateGameObject("Ground");
ground->AddComponent<BoxColliderComponent>();
GameObject* player = scene.CreateGameObject("Player");
player->AddComponent<RigidbodyComponent>();
player->AddComponent<SphereColliderComponent>();
GameObject* childTrigger = scene.CreateGameObject("ChildTrigger", player);
childTrigger->AddComponent<CapsuleColliderComponent>();
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable();
ASSERT_EQ(world.Initialize(createInfo), expectedInitialized);
EXPECT_EQ(world.GetTrackedRigidbodyCount(), 1u);
EXPECT_EQ(world.GetTrackedColliderCount(), 3u);
if (!expectedInitialized) {
EXPECT_EQ(world.GetNativeActorCount(), 0u);
EXPECT_EQ(world.GetNativeShapeCount(), 0u);
return;
}
EXPECT_EQ(world.GetNativeActorCount(), 2u);
EXPECT_EQ(world.GetNativeShapeCount(), 3u);
}
TEST(PhysicsWorld_Test, ComponentChangesRebuildNativeActorOwnership) {
using namespace XCEngine::Components;
Scene scene("PhysicsScene");
GameObject* player = scene.CreateGameObject("Player");
player->AddComponent<RigidbodyComponent>();
SphereColliderComponent* playerSphere = player->AddComponent<SphereColliderComponent>();
BoxColliderComponent* playerBox = player->AddComponent<BoxColliderComponent>();
GameObject* child = scene.CreateGameObject("Child", player);
child->AddComponent<CapsuleColliderComponent>();
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable();
ASSERT_EQ(world.Initialize(createInfo), expectedInitialized);
EXPECT_EQ(world.GetTrackedRigidbodyCount(), 1u);
EXPECT_EQ(world.GetTrackedColliderCount(), 3u);
if (!expectedInitialized) {
return;
}
EXPECT_EQ(world.GetNativeActorCount(), 1u);
EXPECT_EQ(world.GetNativeShapeCount(), 3u);
ASSERT_TRUE(player->RemoveComponent(playerBox));
EXPECT_EQ(world.GetTrackedRigidbodyCount(), 1u);
EXPECT_EQ(world.GetTrackedColliderCount(), 2u);
EXPECT_EQ(world.GetNativeActorCount(), 1u);
EXPECT_EQ(world.GetNativeShapeCount(), 2u);
child->AddComponent<RigidbodyComponent>();
EXPECT_EQ(world.GetTrackedRigidbodyCount(), 2u);
EXPECT_EQ(world.GetTrackedColliderCount(), 2u);
EXPECT_EQ(world.GetNativeActorCount(), 2u);
EXPECT_EQ(world.GetNativeShapeCount(), 2u);
scene.DestroyGameObject(player);
EXPECT_EQ(world.GetTrackedRigidbodyCount(), 0u);
EXPECT_EQ(world.GetTrackedColliderCount(), 0u);
EXPECT_EQ(world.GetNativeActorCount(), 0u);
EXPECT_EQ(world.GetNativeShapeCount(), 0u);
(void)playerSphere;
}
TEST(PhysicsWorld_Test, DynamicSimulationWritesBackTransform) {
using namespace XCEngine::Components;
if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) {
GTEST_SKIP() << "PhysX backend is not available in this build.";
}
Scene scene("PhysicsScene");
GameObject* ground = scene.CreateGameObject("Ground");
ground->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, -0.5f, 0.0f));
BoxColliderComponent* groundCollider = ground->AddComponent<BoxColliderComponent>();
groundCollider->SetSize(XCEngine::Math::Vector3(20.0f, 1.0f, 20.0f));
GameObject* ball = scene.CreateGameObject("Ball");
ball->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 4.0f, 0.0f));
RigidbodyComponent* rigidbody = ball->AddComponent<RigidbodyComponent>();
rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic);
rigidbody->SetMass(1.0f);
rigidbody->SetUseGravity(true);
SphereColliderComponent* sphere = ball->AddComponent<SphereColliderComponent>();
sphere->SetRadius(0.5f);
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
ASSERT_TRUE(world.Initialize(createInfo));
const float initialY = ball->GetTransform()->GetPosition().y;
for (int index = 0; index < 30; ++index) {
world.Step(1.0f / 60.0f);
}
EXPECT_LT(ball->GetTransform()->GetPosition().y, initialY - 0.1f);
}
TEST(PhysicsWorld_Test, RaycastHitsClosestCollider) {
using namespace XCEngine::Components;
Scene scene("PhysicsScene");
GameObject* ground = scene.CreateGameObject("Ground");
ground->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, -0.5f, 0.0f));
BoxColliderComponent* groundCollider = ground->AddComponent<BoxColliderComponent>();
groundCollider->SetSize(XCEngine::Math::Vector3(20.0f, 1.0f, 20.0f));
GameObject* target = scene.CreateGameObject("Target");
target->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 2.0f, 0.0f));
SphereColliderComponent* sphereCollider = target->AddComponent<SphereColliderComponent>();
sphereCollider->SetRadius(0.5f);
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable();
ASSERT_EQ(world.Initialize(createInfo), expectedInitialized);
XCEngine::Physics::RaycastHit hit;
const bool result = world.Raycast(
XCEngine::Math::Vector3(0.0f, 5.0f, 0.0f),
XCEngine::Math::Vector3::Down(),
10.0f,
hit);
if (!expectedInitialized) {
EXPECT_FALSE(result);
EXPECT_EQ(hit.gameObject, nullptr);
return;
}
ASSERT_TRUE(result);
EXPECT_EQ(hit.gameObject, target);
EXPECT_NEAR(hit.distance, 2.5f, 0.02f);
EXPECT_NEAR(hit.point.x, 0.0f, 0.001f);
EXPECT_NEAR(hit.point.y, 2.5f, 0.02f);
EXPECT_NEAR(hit.point.z, 0.0f, 0.001f);
EXPECT_NEAR(hit.normal.x, 0.0f, 0.001f);
EXPECT_NEAR(hit.normal.y, 1.0f, 0.02f);
EXPECT_NEAR(hit.normal.z, 0.0f, 0.001f);
EXPECT_FALSE(hit.isTrigger);
}
TEST(PhysicsWorld_Test, RaycastMissClearsHitOutput) {
using namespace XCEngine::Components;
Scene scene("PhysicsScene");
GameObject* target = scene.CreateGameObject("Target");
target->AddComponent<BoxColliderComponent>();
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable();
ASSERT_EQ(world.Initialize(createInfo), expectedInitialized);
XCEngine::Physics::RaycastHit hit;
hit.gameObject = target;
hit.point = XCEngine::Math::Vector3::One();
hit.normal = XCEngine::Math::Vector3::Up();
hit.distance = 123.0f;
hit.isTrigger = true;
const bool result = world.Raycast(
XCEngine::Math::Vector3(0.0f, 5.0f, 0.0f),
XCEngine::Math::Vector3::Up(),
10.0f,
hit);
EXPECT_FALSE(result);
EXPECT_EQ(hit.gameObject, nullptr);
EXPECT_EQ(hit.point, XCEngine::Math::Vector3::Zero());
EXPECT_EQ(hit.normal, XCEngine::Math::Vector3::Zero());
EXPECT_FLOAT_EQ(hit.distance, 0.0f);
EXPECT_FALSE(hit.isTrigger);
}
TEST(PhysicsWorld_Test, RaycastCanHitTriggerCollider) {
using namespace XCEngine::Components;
Scene scene("PhysicsScene");
GameObject* trigger = scene.CreateGameObject("Trigger");
SphereColliderComponent* sphereCollider = trigger->AddComponent<SphereColliderComponent>();
sphereCollider->SetRadius(1.0f);
sphereCollider->SetTrigger(true);
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable();
ASSERT_EQ(world.Initialize(createInfo), expectedInitialized);
XCEngine::Physics::RaycastHit hit;
const bool result = world.Raycast(
XCEngine::Math::Vector3(0.0f, 0.0f, -5.0f),
XCEngine::Math::Vector3::Forward(),
10.0f,
hit);
if (!expectedInitialized) {
EXPECT_FALSE(result);
EXPECT_EQ(hit.gameObject, nullptr);
return;
}
ASSERT_TRUE(result);
EXPECT_EQ(hit.gameObject, trigger);
EXPECT_NEAR(hit.distance, 4.0f, 0.02f);
EXPECT_TRUE(hit.isTrigger);
EXPECT_NEAR(hit.normal.x, 0.0f, 0.001f);
EXPECT_NEAR(hit.normal.y, 0.0f, 0.001f);
EXPECT_NEAR(hit.normal.z, -1.0f, 0.02f);
}
TEST(PhysicsWorld_Test, RuntimeColliderGeometryChangesUpdateRaycast) {
using namespace XCEngine::Components;
Scene scene("PhysicsScene");
GameObject* target = scene.CreateGameObject("Target");
BoxColliderComponent* boxCollider = target->AddComponent<BoxColliderComponent>();
boxCollider->SetSize(XCEngine::Math::Vector3(1.0f, 1.0f, 1.0f));
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable();
ASSERT_EQ(world.Initialize(createInfo), expectedInitialized);
XCEngine::Physics::RaycastHit hit;
EXPECT_FALSE(world.Raycast(
XCEngine::Math::Vector3(0.0f, 1.5f, -5.0f),
XCEngine::Math::Vector3::Forward(),
10.0f,
hit));
boxCollider->SetSize(XCEngine::Math::Vector3(1.0f, 4.0f, 1.0f));
const bool result = world.Raycast(
XCEngine::Math::Vector3(0.0f, 1.5f, -5.0f),
XCEngine::Math::Vector3::Forward(),
10.0f,
hit);
if (!expectedInitialized) {
EXPECT_FALSE(result);
EXPECT_EQ(hit.gameObject, nullptr);
return;
}
ASSERT_TRUE(result);
EXPECT_EQ(hit.gameObject, target);
EXPECT_NEAR(hit.distance, 4.5f, 0.02f);
}
TEST(PhysicsWorld_Test, RuntimeTriggerFlagChangesUpdateRaycastHit) {
using namespace XCEngine::Components;
Scene scene("PhysicsScene");
GameObject* target = scene.CreateGameObject("Target");
SphereColliderComponent* sphereCollider = target->AddComponent<SphereColliderComponent>();
sphereCollider->SetRadius(1.0f);
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable();
ASSERT_EQ(world.Initialize(createInfo), expectedInitialized);
sphereCollider->SetTrigger(true);
XCEngine::Physics::RaycastHit hit;
const bool result = world.Raycast(
XCEngine::Math::Vector3(0.0f, 0.0f, -5.0f),
XCEngine::Math::Vector3::Forward(),
10.0f,
hit);
if (!expectedInitialized) {
EXPECT_FALSE(result);
EXPECT_EQ(hit.gameObject, nullptr);
return;
}
ASSERT_TRUE(result);
EXPECT_EQ(hit.gameObject, target);
EXPECT_TRUE(hit.isTrigger);
}
TEST(PhysicsWorld_Test, RuntimeBodyTypeChangesRebuildActorAndEnableSimulation) {
using namespace XCEngine::Components;
if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) {
GTEST_SKIP() << "PhysX backend is not available in this build.";
}
Scene scene("PhysicsScene");
GameObject* ground = scene.CreateGameObject("Ground");
ground->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, -0.5f, 0.0f));
BoxColliderComponent* groundCollider = ground->AddComponent<BoxColliderComponent>();
groundCollider->SetSize(XCEngine::Math::Vector3(20.0f, 1.0f, 20.0f));
GameObject* body = scene.CreateGameObject("Body");
body->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 4.0f, 0.0f));
RigidbodyComponent* rigidbody = body->AddComponent<RigidbodyComponent>();
rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Static);
SphereColliderComponent* sphereCollider = body->AddComponent<SphereColliderComponent>();
sphereCollider->SetRadius(0.5f);
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
ASSERT_TRUE(world.Initialize(createInfo));
ASSERT_EQ(world.GetNativeActorCount(), 2u);
const float initialY = body->GetTransform()->GetPosition().y;
world.Step(1.0f / 60.0f);
EXPECT_NEAR(body->GetTransform()->GetPosition().y, initialY, 0.001f);
rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic);
world.Step(1.0f / 60.0f);
EXPECT_EQ(world.GetNativeActorCount(), 2u);
for (int index = 0; index < 30; ++index) {
world.Step(1.0f / 60.0f);
}
EXPECT_LT(body->GetTransform()->GetPosition().y, initialY - 0.1f);
}
TEST(PhysicsWorld_Test, RuntimeLinearVelocityMovesDynamicBody) {
using namespace XCEngine::Components;
if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) {
GTEST_SKIP() << "PhysX backend is not available in this build.";
}
Scene scene("PhysicsScene");
GameObject* body = scene.CreateGameObject("Body");
body->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 1.0f, 0.0f));
RigidbodyComponent* rigidbody = body->AddComponent<RigidbodyComponent>();
rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic);
rigidbody->SetUseGravity(false);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(0.0f, 2.0f, 0.0f));
SphereColliderComponent* sphereCollider = body->AddComponent<SphereColliderComponent>();
sphereCollider->SetRadius(0.5f);
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
ASSERT_TRUE(world.Initialize(createInfo));
const float initialY = body->GetTransform()->GetPosition().y;
for (int index = 0; index < 30; ++index) {
world.Step(1.0f / 60.0f);
}
EXPECT_GT(body->GetTransform()->GetPosition().y, initialY + 0.9f);
EXPECT_NEAR(rigidbody->GetLinearVelocity().y, 2.0f, 0.02f);
}
TEST(PhysicsWorld_Test, AddForceImpulseMovesDynamicBody) {
using namespace XCEngine::Components;
if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) {
GTEST_SKIP() << "PhysX backend is not available in this build.";
}
Scene scene("PhysicsScene");
GameObject* body = scene.CreateGameObject("Body");
body->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 1.0f, 0.0f));
RigidbodyComponent* rigidbody = body->AddComponent<RigidbodyComponent>();
rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic);
rigidbody->SetUseGravity(false);
rigidbody->SetMass(1.0f);
SphereColliderComponent* sphereCollider = body->AddComponent<SphereColliderComponent>();
sphereCollider->SetRadius(0.5f);
XCEngine::Physics::PhysicsWorld world;
XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {};
createInfo.scene = &scene;
ASSERT_TRUE(world.Initialize(createInfo));
const float initialY = body->GetTransform()->GetPosition().y;
rigidbody->AddForce(
XCEngine::Math::Vector3(0.0f, 3.0f, 0.0f),
XCEngine::Physics::PhysicsForceMode::Impulse);
for (int index = 0; index < 10; ++index) {
world.Step(1.0f / 60.0f);
}
EXPECT_GT(body->GetTransform()->GetPosition().y, initialY + 0.35f);
EXPECT_NEAR(rigidbody->GetLinearVelocity().y, 3.0f, 0.02f);
}
} // namespace

View File

@@ -1,76 +0,0 @@
cmake_minimum_required(VERSION 3.15)
project(RHIEngineTests)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(D3D12)
add_subdirectory(OpenGL)
add_subdirectory(Vulkan)
add_subdirectory(unit)
add_subdirectory(integration)
add_custom_target(rhi_abstraction_unit_tests
DEPENDS
rhi_unit_tests
)
add_custom_target(rhi_abstraction_integration_tests
DEPENDS
rhi_integration_minimal
rhi_integration_triangle
rhi_integration_quad
rhi_integration_sphere
rhi_integration_backpack
)
add_custom_target(rhi_abstraction_tests
DEPENDS
rhi_abstraction_unit_tests
rhi_abstraction_integration_tests
)
add_custom_target(rhi_backend_unit_tests
DEPENDS
rhi_d3d12_tests
rhi_opengl_tests
rhi_vulkan_tests
)
add_custom_target(rhi_vulkan_backend_tests
DEPENDS
rhi_vulkan_tests
vulkan_minimal_test
vulkan_triangle_test
vulkan_quad_test
vulkan_sphere_test
)
add_custom_target(rhi_backend_integration_tests
DEPENDS
d3d12_minimal_test
d3d12_triangle_test
d3d12_quad_test
d3d12_sphere_test
opengl_minimal_test
opengl_triangle_test
opengl_quad_test
opengl_sphere_test
vulkan_minimal_test
vulkan_triangle_test
vulkan_quad_test
vulkan_sphere_test
)
add_custom_target(rhi_backend_tests
DEPENDS
rhi_backend_unit_tests
rhi_backend_integration_tests
)
add_custom_target(rhi_all_tests
DEPENDS
rhi_abstraction_tests
rhi_backend_tests
)

View File

@@ -1,9 +0,0 @@
cmake_minimum_required(VERSION 3.15)
project(rhi_d3d12_tests)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(unit)
add_subdirectory(integration)

View File

@@ -1,118 +0,0 @@
# D3D12 测试专项规范
## 1. 构建命令
```bash
# 构建所有
cmake --build <build> --config Debug
# 只构建单元测试
cmake --build <build> --target d3d12_engine_tests --config Debug
# 只构建集成测试
cmake --build <build> --target D3D12_Sphere D3D12_Triangle D3D12_Quad D3D12_Minimal --config Debug
```
## 2. 运行测试
```bash
# 运行所有已注册的测试(单元 + 集成)
cd <build>
ctest -C Debug --output-on-failure
# 只运行单元测试
cd <build>/tests/RHI/D3D12/unit
ctest -C Debug --output-on-failure
# 只运行集成测试
cd <build>/tests/RHI/D3D12/integration
ctest -C Debug --output-on-failure
```
## 3. 集成测试列表
| 测试名 | Target | Golden Image |
|--------|--------|-------------|
| D3D12_Minimal_Integration | D3D12_Minimal | `minimal/GT.ppm` |
| D3D12_Quad_Integration | D3D12_Quad | `quad/GT.ppm` |
| D3D12_Sphere_Integration | D3D12_Sphere | `sphere/GT.ppm` |
| D3D12_Triangle_Integration | D3D12_Triangle | `triangle/GT.ppm` |
## 4. CTest 注册机制
集成测试通过 `add_test()` 注册到 CTest
```cmake
add_test(NAME D3D12_Sphere_Integration
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:D3D12_Sphere>/run_integration_test.py
$<TARGET_FILE:D3D12_Sphere>
sphere.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:D3D12_Sphere>
)
```
`run_integration_test.py` 负责:
1. 启动 exe
2. 等待完成
3. 调用 `compare_ppm.py` 比对图像
4. 返回 0通过/ 1失败
## 5. CI 配置
```yaml
name: D3D12 Tests
on: [push, pull_request]
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build D3D12
run: cmake --build build --config Debug
- name: Run Tests
run: cd build && ctest -C Debug --output-on-failure
```
## 6. 目录结构
```
tests/RHI/D3D12/
├── unit/
│ ├── CMakeLists.txt
│ ├── fixtures/
│ │ ├── D3D12TestFixture.h
│ │ └── D3D12TestFixture.cpp
│ ├── test_*.cpp
│ └── ...
└── integration/
├── CMakeLists.txt
├── run_integration_test.py # 测试 wrapper
├── compare_ppm.py # 图像比对
├── minimal/ # 最小测试
│ ├── main.cpp
│ └── GT.ppm
├── quad/
│ ├── main.cpp
│ └── GT.ppm
├── sphere/
│ ├── main.cpp
│ ├── GT.ppm
│ └── Res/
│ ├── Image/
│ └── Shader/
└── triangle/
├── main.cpp
└── GT.ppm
```
---
**最后更新**: 2026-03-22

View File

@@ -1,8 +0,0 @@
cmake_minimum_required(VERSION 3.15)
find_package(Python3 REQUIRED)
add_subdirectory(minimal)
add_subdirectory(triangle)
add_subdirectory(quad)
add_subdirectory(sphere)

View File

@@ -1,75 +0,0 @@
import sys
import os
def read_ppm(filename):
with open(filename, "rb") as f:
header = f.readline()
if header != b"P6\n":
raise ValueError(f"Not a P6 PPM file: {filename}")
while True:
line = f.readline()
if not line.startswith(b"#"):
break
dims = line.split()
width, height = int(dims[0]), int(dims[1])
line = f.readline()
maxval = int(line.strip())
data = f.read()
return width, height, data
def compare_ppm(file1, file2, threshold):
w1, h1, d1 = read_ppm(file1)
w2, h2, d2 = read_ppm(file2)
if w1 != w2 or h1 != h2:
print(f"ERROR: Size mismatch - {file1}: {w1}x{h1}, {file2}: {w2}x{h2}")
return False
total_pixels = w1 * h1
diff_count = 0
for i in range(len(d1)):
diff = abs(d1[i] - d2[i])
if diff > threshold:
diff_count += 1
diff_percent = (diff_count / (total_pixels * 3)) * 100
print(f"Image 1: {file1} ({w1}x{h1})")
print(f"Image 2: {file2} ({w2}x{h2})")
print(f"Threshold: {threshold}")
print(f"Different pixels: {diff_count} / {total_pixels * 3} ({diff_percent:.2f}%)")
if diff_percent <= 1.0:
print("PASS: Images match!")
return True
else:
print("FAIL: Images differ!")
return False
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Usage: python compare_ppm.py <file1.ppm> <file2.ppm> <threshold>")
sys.exit(1)
file1 = sys.argv[1]
file2 = sys.argv[2]
threshold = int(sys.argv[3])
if not os.path.exists(file1):
print(f"ERROR: File not found: {file1}")
sys.exit(1)
if not os.path.exists(file2):
print(f"ERROR: File not found: {file2}")
sys.exit(1)
result = compare_ppm(file1, file2, threshold)
sys.exit(0 if result else 1)

View File

@@ -1,54 +0,0 @@
cmake_minimum_required(VERSION 3.15)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
project(d3d12_minimal_test)
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
add_executable(d3d12_minimal_test
WIN32
main.cpp
)
target_include_directories(d3d12_minimal_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${ENGINE_ROOT_DIR}/include
)
target_compile_definitions(d3d12_minimal_test PRIVATE
UNICODE
_UNICODE
)
target_link_libraries(d3d12_minimal_test PRIVATE
d3d12
dxgi
d3dcompiler
winmm
XCEngine
)
add_custom_command(TARGET d3d12_minimal_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/compare_ppm.py
$<TARGET_FILE_DIR:d3d12_minimal_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/run_integration_test.py
$<TARGET_FILE_DIR:d3d12_minimal_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
$<TARGET_FILE_DIR:d3d12_minimal_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
$<TARGET_FILE_DIR:d3d12_minimal_test>/
)
add_test(NAME d3d12_minimal_test
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:d3d12_minimal_test>/run_integration_test.py
$<TARGET_FILE:d3d12_minimal_test>
minimal.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:d3d12_minimal_test>
)

View File

@@ -1,338 +0,0 @@
#include <windows.h>
#include <d3d12.h>
#include <dxgi1_4.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdarg.h>
#include <string>
#include "XCEngine/RHI/RHIEnums.h"
#include "XCEngine/RHI/RHITypes.h"
#include "XCEngine/RHI/D3D12/D3D12Device.h"
#include "XCEngine/RHI/D3D12/D3D12CommandQueue.h"
#include "XCEngine/RHI/D3D12/D3D12CommandAllocator.h"
#include "XCEngine/RHI/D3D12/D3D12CommandList.h"
#include "XCEngine/RHI/D3D12/D3D12DescriptorHeap.h"
#include "XCEngine/RHI/D3D12/D3D12Fence.h"
#include "XCEngine/RHI/D3D12/D3D12SwapChain.h"
#include "XCEngine/RHI/D3D12/D3D12Buffer.h"
#include "XCEngine/RHI/D3D12/D3D12Texture.h"
#include "XCEngine/RHI/D3D12/D3D12ResourceView.h"
#include "XCEngine/RHI/D3D12/D3D12Screenshot.h"
#include "XCEngine/Debug/Logger.h"
#include "XCEngine/Debug/ConsoleLogSink.h"
#include "XCEngine/Debug/FileLogSink.h"
#include "XCEngine/Debug/RenderDocCapture.h"
#include "XCEngine/Core/Containers/String.h"
using namespace XCEngine::RHI;
using namespace XCEngine::Debug;
using namespace XCEngine::Containers;
#pragma comment(lib,"d3d12.lib")
#pragma comment(lib,"dxgi.lib")
#pragma comment(lib,"dxguid.lib")
#pragma comment(lib,"d3dcompiler.lib")
#pragma comment(lib,"winmm.lib")
// Global D3D12 objects
D3D12Device gDevice;
D3D12CommandQueue gCommandQueue;
D3D12SwapChain gSwapChain;
D3D12CommandAllocator gCommandAllocator;
D3D12CommandList gCommandList;
// Render targets
D3D12Texture gDepthStencil;
D3D12DescriptorHeap gRTVHeap;
D3D12DescriptorHeap gDSVHeap;
D3D12ResourceView gRTVs[2];
D3D12ResourceView gDSV;
UINT gRTVDescriptorSize = 0;
UINT gDSVDescriptorSize = 0;
int gCurrentRTIndex = 0;
// Window
HWND gHWND = nullptr;
int gWidth = 1280;
int gHeight = 720;
// Log helper
void Log(const char* format, ...) {
char buffer[1024];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
Logger::Get().Debug(LogCategory::Rendering, String(buffer));
}
// Window procedure
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
// Initialize D3D12
bool InitD3D12() {
// Create device
RHIDeviceDesc deviceDesc;
deviceDesc.adapterIndex = 0;
deviceDesc.enableDebugLayer = false;
deviceDesc.enableGPUValidation = false;
if (!gDevice.Initialize(deviceDesc)) {
Log("[ERROR] Failed to initialize D3D12 device");
return false;
}
ID3D12Device* device = gDevice.GetDevice();
IDXGIFactory4* factory = gDevice.GetFactory();
// Create command queue
if (!gCommandQueue.Initialize(device, CommandQueueType::Direct)) {
Log("[ERROR] Failed to initialize command queue");
return false;
}
// Create swap chain using encapsulated interface
if (!gSwapChain.Initialize(factory, gCommandQueue.GetCommandQueue(), gHWND, gWidth, gHeight, 2)) {
Log("[ERROR] Failed to initialize swap chain");
return false;
}
// Initialize depth stencil
gDepthStencil.InitializeDepthStencil(device, gWidth, gHeight);
// Create RTV heap
gRTVHeap.Initialize(device, DescriptorHeapType::RTV, 2);
gRTVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::RTV);
// Create DSV heap
gDSVHeap.Initialize(device, DescriptorHeapType::DSV, 1);
gDSVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::DSV);
// Create RTVs for back buffers using encapsulated interface
for (int i = 0; i < 2; i++) {
D3D12Texture& backBuffer = gSwapChain.GetBackBuffer(i);
CPUDescriptorHandle rtvCpuHandle = gRTVHeap.GetCPUDescriptorHandle(i);
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr };
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = D3D12ResourceView::CreateRenderTargetDesc(Format::R8G8B8A8_UNorm, D3D12_RTV_DIMENSION_TEXTURE2D);
gRTVs[i].InitializeAsRenderTarget(device, backBuffer.GetResource(), &rtvDesc, &gRTVHeap, i);
}
// Create DSV
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = D3D12ResourceView::CreateDepthStencilDesc(Format::D24_UNorm_S8_UInt, D3D12_DSV_DIMENSION_TEXTURE2D);
gDSV.InitializeAsDepthStencil(device, gDepthStencil.GetResource(), &dsvDesc, &gDSVHeap, 0);
// Create command allocator and list
gCommandAllocator.Initialize(device, CommandQueueType::Direct);
gCommandList.Initialize(device, CommandQueueType::Direct, gCommandAllocator.GetCommandAllocator());
Log("[INFO] D3D12 initialized successfully");
return true;
}
// Wait for GPU
void WaitForGPU() {
gCommandQueue.WaitForIdle();
}
// Execute command list
void ExecuteCommandList() {
gCommandList.Close();
void* commandLists[] = { &gCommandList };
gCommandQueue.ExecuteCommandLists(1, commandLists);
}
// Begin rendering
void BeginRender() {
gCurrentRTIndex = gSwapChain.GetCurrentBackBufferIndex();
// Transition render target
D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex);
gCommandList.TransitionBarrier(currentBackBuffer.GetResource(),
ResourceStates::Present, ResourceStates::RenderTarget);
// Set render targets using encapsulated interface
CPUDescriptorHandle rtvCpuHandle = gRTVHeap.GetCPUDescriptorHandle(gCurrentRTIndex);
CPUDescriptorHandle dsvCpuHandle = gDSVHeap.GetCPUDescriptorHandle(0);
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr };
D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = { dsvCpuHandle.ptr };
gCommandList.SetRenderTargetsHandle(1, &rtvHandle, &dsvHandle);
// Set viewport and scissor
Viewport viewport = { 0.0f, 0.0f, (float)gWidth, (float)gHeight, 0.0f, 1.0f };
Rect scissorRect = { 0, 0, gWidth, gHeight };
gCommandList.SetViewport(viewport);
gCommandList.SetScissorRect(scissorRect);
// Clear
float clearColor[] = { 1.0f, 0.0f, 0.0f, 1.0f };
gCommandList.ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
gCommandList.ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
}
// End rendering
void EndRender() {
D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex);
gCommandList.TransitionBarrier(currentBackBuffer.GetResource(),
ResourceStates::RenderTarget, ResourceStates::Present);
}
// Main entry
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
// Initialize logger
Logger::Get().Initialize();
Logger::Get().AddSink(std::make_unique<ConsoleLogSink>());
Logger::Get().SetMinimumLevel(LogLevel::Debug);
Log("[INFO] D3D12 Integration Test Starting");
// Register window class
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"D3D12Test";
if (!RegisterClassEx(&wc)) {
MessageBox(NULL, L"Failed to register window class", L"Error", MB_OK);
return -1;
}
// Create window
RECT rect = { 0, 0, gWidth, gHeight };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
gHWND = CreateWindowEx(0, L"D3D12Test", L"D3D12 Integration Test",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
NULL, NULL, hInstance, NULL);
if (!gHWND) {
MessageBox(NULL, L"Failed to create window", L"Error", MB_OK);
return -1;
}
RenderDocCapture::Get().Initialize(nullptr, gHWND);
RenderDocCapture::Get().SetCaptureFilePath(".\\minimal_frame30");
// Initialize D3D12
if (!InitD3D12()) {
MessageBox(NULL, L"Failed to initialize D3D12", L"Error", MB_OK);
return -1;
}
// Set device for RenderDoc (must be called after D3D12 init)
RenderDocCapture::Get().SetDevice(gDevice.GetDevice());
// Show window
ShowWindow(gHWND, nShowCmd);
UpdateWindow(gHWND);
// Main loop
MSG msg = {};
int frameCount = 0;
const int targetFrameCount = 30;
while (true) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
// Wait for previous frame to complete before resetting
if (frameCount > 0) {
gCommandQueue.WaitForPreviousFrame();
}
// Reset command list for this frame
gCommandAllocator.Reset();
gCommandList.Reset();
// Render
BeginRender();
// (Add rendering code here)
frameCount++;
EndRender();
// Execute
ExecuteCommandList();
if (frameCount >= targetFrameCount) {
if (RenderDocCapture::Get().EndCapture()) {
Log("[INFO] RenderDoc capture ended");
}
WaitForGPU();
Log("[INFO] GPU idle, taking screenshot...");
bool screenshotResult = D3D12Screenshot::Capture(
gDevice,
gCommandQueue,
gSwapChain.GetBackBuffer(gCurrentRTIndex),
"minimal.ppm"
);
if (screenshotResult) {
Log("[INFO] Screenshot saved to minimal.ppm");
} else {
Log("[ERROR] Screenshot failed");
}
Log("[INFO] RenderDoc capture completed");
break;
}
if (frameCount == targetFrameCount - 1) {
if (RenderDocCapture::Get().BeginCapture("D3D12_Minimal_Test")) {
Log("[INFO] RenderDoc capture started");
}
}
EndRender();
ExecuteCommandList();
gSwapChain.Present(0, 0);
if (frameCount >= targetFrameCount) {
WaitForGPU();
Log("[INFO] GPU idle, taking screenshot...");
bool screenshotResult = D3D12Screenshot::Capture(
gDevice,
gCommandQueue,
gSwapChain.GetBackBuffer(gCurrentRTIndex),
"minimal.ppm"
);
if (screenshotResult) {
Log("[INFO] Screenshot saved to minimal.ppm");
} else {
Log("[ERROR] Screenshot failed");
}
Log("[INFO] RenderDoc capture completed");
break;
}
}
}
// Shutdown
RenderDocCapture::Get().Shutdown();
gCommandList.Shutdown();
gCommandAllocator.Shutdown();
gSwapChain.Shutdown();
gDevice.Shutdown();
Logger::Get().Shutdown();
Log("[INFO] D3D12 Integration Test Finished");
return 0;
}

View File

@@ -1,63 +0,0 @@
cmake_minimum_required(VERSION 3.15)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
project(d3d12_quad_test)
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
add_executable(d3d12_quad_test
WIN32
main.cpp
)
set_target_properties(d3d12_quad_test PROPERTIES LINK_FLAGS "/INCREMENTAL:NO")
target_include_directories(d3d12_quad_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${ENGINE_ROOT_DIR}/include
${ENGINE_ROOT_DIR}
)
target_compile_definitions(d3d12_quad_test PRIVATE
UNICODE
_UNICODE
)
target_link_libraries(d3d12_quad_test PRIVATE
d3d12
dxgi
d3dcompiler
winmm
XCEngine
)
add_custom_command(TARGET d3d12_quad_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/Res
$<TARGET_FILE_DIR:d3d12_quad_test>/Res
)
add_custom_command(TARGET d3d12_quad_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/compare_ppm.py
$<TARGET_FILE_DIR:d3d12_quad_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/run_integration_test.py
$<TARGET_FILE_DIR:d3d12_quad_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
$<TARGET_FILE_DIR:d3d12_quad_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
$<TARGET_FILE_DIR:d3d12_quad_test>/
)
add_test(NAME d3d12_quad_test
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:d3d12_quad_test>/run_integration_test.py
$<TARGET_FILE:d3d12_quad_test>
quad.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:d3d12_quad_test>
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -1,23 +0,0 @@
struct Vertex {
float4 pos : POSITION;
float4 texcoord : TEXCOORD0;
};
struct VSOut {
float4 pos : SV_POSITION;
float4 texcoord : TEXCOORD0;
};
VSOut MainVS(Vertex v) {
VSOut o;
o.pos = v.pos;
o.texcoord = v.texcoord;
return o;
}
Texture2D T_DiffuseTexture : register(t0);
SamplerState samplerState : register(s0);
float4 MainPS(VSOut i) : SV_TARGET {
return T_DiffuseTexture.Sample(samplerState, i.texcoord.xy);
}

View File

@@ -1,671 +0,0 @@
#include <windows.h>
#include <filesystem>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <d3d12.h>
#include <dxgi1_4.h>
#include "XCEngine/RHI/RHIEnums.h"
#include "XCEngine/RHI/RHITypes.h"
#include "XCEngine/RHI/RHIBuffer.h"
#include "XCEngine/RHI/RHIDescriptorPool.h"
#include "XCEngine/RHI/RHIDescriptorSet.h"
#include "XCEngine/RHI/RHIPipelineLayout.h"
#include "XCEngine/RHI/RHIPipelineState.h"
#include "XCEngine/RHI/RHIResourceView.h"
#include "XCEngine/RHI/RHISampler.h"
#include "XCEngine/RHI/RHITexture.h"
#include "XCEngine/RHI/D3D12/D3D12Device.h"
#include "XCEngine/RHI/D3D12/D3D12CommandQueue.h"
#include "XCEngine/RHI/D3D12/D3D12CommandAllocator.h"
#include "XCEngine/RHI/D3D12/D3D12CommandList.h"
#include "XCEngine/RHI/D3D12/D3D12DescriptorHeap.h"
#include "XCEngine/RHI/D3D12/D3D12SwapChain.h"
#include "XCEngine/RHI/D3D12/D3D12Texture.h"
#include "XCEngine/RHI/D3D12/D3D12ResourceView.h"
#include "XCEngine/RHI/D3D12/D3D12Screenshot.h"
#include "XCEngine/Debug/Logger.h"
#include "XCEngine/Debug/ConsoleLogSink.h"
#include "XCEngine/Debug/FileLogSink.h"
#include "XCEngine/Debug/RenderDocCapture.h"
#include "XCEngine/Core/Containers/String.h"
#include "third_party/stb/stb_image.h"
using namespace XCEngine::RHI;
using namespace XCEngine::Debug;
using namespace XCEngine::Containers;
#pragma comment(lib,"d3d12.lib")
#pragma comment(lib,"dxgi.lib")
#pragma comment(lib,"dxguid.lib")
#pragma comment(lib,"d3dcompiler.lib")
#pragma comment(lib,"winmm.lib")
namespace {
struct Vertex {
float pos[4];
float uv[2];
};
constexpr Vertex kQuadVertices[] = {
{ { -0.5f, -0.5f, 0.0f, 1.0f }, { 0.0f, 1.0f } },
{ { -0.5f, 0.5f, 0.0f, 1.0f }, { 0.0f, 0.0f } },
{ { 0.5f, -0.5f, 0.0f, 1.0f }, { 1.0f, 1.0f } },
{ { 0.5f, 0.5f, 0.0f, 1.0f }, { 1.0f, 0.0f } },
};
constexpr uint32_t kQuadIndices[] = { 0, 1, 2, 2, 1, 3 };
const char kQuadHlsl[] = R"(
Texture2D gTexture : register(t0);
SamplerState gSampler : register(s0);
struct VSInput {
float4 position : POSITION;
float2 texcoord : TEXCOORD;
};
struct PSInput {
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD;
};
PSInput MainVS(VSInput input) {
PSInput output;
output.position = input.position;
output.texcoord = input.texcoord;
return output;
}
float4 MainPS(PSInput input) : SV_TARGET {
return gTexture.Sample(gSampler, input.texcoord);
}
)";
D3D12Device gDevice;
D3D12CommandQueue gCommandQueue;
D3D12SwapChain gSwapChain;
D3D12CommandAllocator gCommandAllocator;
D3D12CommandList gCommandList;
D3D12Texture gDepthStencil;
D3D12DescriptorHeap gRTVHeap;
D3D12DescriptorHeap gDSVHeap;
D3D12ResourceView gRTVs[2];
D3D12ResourceView gDSV;
RHIBuffer* gVertexBuffer = nullptr;
RHIResourceView* gVertexBufferView = nullptr;
RHIBuffer* gIndexBuffer = nullptr;
RHIResourceView* gIndexBufferView = nullptr;
RHITexture* gTexture = nullptr;
RHIResourceView* gTextureView = nullptr;
RHISampler* gSampler = nullptr;
RHIDescriptorPool* gTexturePool = nullptr;
RHIDescriptorSet* gTextureSet = nullptr;
RHIDescriptorPool* gSamplerPool = nullptr;
RHIDescriptorSet* gSamplerSet = nullptr;
RHIPipelineLayout* gPipelineLayout = nullptr;
RHIPipelineState* gPipelineState = nullptr;
UINT gRTVDescriptorSize = 0;
UINT gDSVDescriptorSize = 0;
int gCurrentRTIndex = 0;
HWND gHWND = nullptr;
int gWidth = 1280;
int gHeight = 720;
template <typename T>
void ShutdownAndDelete(T*& object) {
if (object != nullptr) {
object->Shutdown();
delete object;
object = nullptr;
}
}
void Log(const char* format, ...) {
char buffer[1024];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
Logger::Get().Debug(LogCategory::Rendering, String(buffer));
}
std::filesystem::path GetExecutableDirectory() {
char exePath[MAX_PATH] = {};
const DWORD length = GetModuleFileNameA(nullptr, exePath, MAX_PATH);
if (length == 0 || length >= MAX_PATH) {
return std::filesystem::current_path();
}
return std::filesystem::path(exePath).parent_path();
}
std::filesystem::path ResolveRuntimePath(const char* relativePath) {
return GetExecutableDirectory() / relativePath;
}
bool LoadTexture() {
const std::filesystem::path texturePath = ResolveRuntimePath("Res/Image/earth.png");
const std::string texturePathString = texturePath.string();
stbi_set_flip_vertically_on_load(0);
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* pixels = stbi_load(texturePathString.c_str(), &width, &height, &channels, STBI_rgb_alpha);
if (pixels == nullptr) {
Log("[ERROR] Failed to load texture: %s", texturePathString.c_str());
return false;
}
TextureDesc textureDesc = {};
textureDesc.width = static_cast<uint32_t>(width);
textureDesc.height = static_cast<uint32_t>(height);
textureDesc.depth = 1;
textureDesc.mipLevels = 1;
textureDesc.arraySize = 1;
textureDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
textureDesc.textureType = static_cast<uint32_t>(TextureType::Texture2D);
textureDesc.sampleCount = 1;
textureDesc.sampleQuality = 0;
textureDesc.flags = 0;
gTexture = gDevice.CreateTexture(
textureDesc,
pixels,
static_cast<size_t>(width) * static_cast<size_t>(height) * 4,
static_cast<uint32_t>(width) * 4);
stbi_image_free(pixels);
if (gTexture == nullptr) {
Log("[ERROR] Failed to create RHI texture");
return false;
}
ResourceViewDesc textureViewDesc = {};
textureViewDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
textureViewDesc.dimension = ResourceViewDimension::Texture2D;
textureViewDesc.mipLevel = 0;
gTextureView = gDevice.CreateShaderResourceView(gTexture, textureViewDesc);
if (gTextureView == nullptr) {
Log("[ERROR] Failed to create texture SRV");
return false;
}
SamplerDesc samplerDesc = {};
samplerDesc.filter = static_cast<uint32_t>(FilterMode::Linear);
samplerDesc.addressU = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressV = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressW = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.mipLodBias = 0.0f;
samplerDesc.maxAnisotropy = 1;
samplerDesc.comparisonFunc = static_cast<uint32_t>(ComparisonFunc::Always);
samplerDesc.borderColorR = 0.0f;
samplerDesc.borderColorG = 0.0f;
samplerDesc.borderColorB = 0.0f;
samplerDesc.borderColorA = 0.0f;
samplerDesc.minLod = 0.0f;
samplerDesc.maxLod = 1000.0f;
gSampler = gDevice.CreateSampler(samplerDesc);
if (gSampler == nullptr) {
Log("[ERROR] Failed to create sampler");
return false;
}
DescriptorPoolDesc texturePoolDesc = {};
texturePoolDesc.type = DescriptorHeapType::CBV_SRV_UAV;
texturePoolDesc.descriptorCount = 1;
texturePoolDesc.shaderVisible = true;
gTexturePool = gDevice.CreateDescriptorPool(texturePoolDesc);
if (gTexturePool == nullptr) {
Log("[ERROR] Failed to create texture descriptor pool");
return false;
}
DescriptorSetLayoutBinding textureBinding = {};
textureBinding.binding = 0;
textureBinding.type = static_cast<uint32_t>(DescriptorType::SRV);
textureBinding.count = 1;
DescriptorSetLayoutDesc textureLayoutDesc = {};
textureLayoutDesc.bindings = &textureBinding;
textureLayoutDesc.bindingCount = 1;
gTextureSet = gTexturePool->AllocateSet(textureLayoutDesc);
if (gTextureSet == nullptr) {
Log("[ERROR] Failed to allocate texture descriptor set");
return false;
}
gTextureSet->Update(0, gTextureView);
DescriptorPoolDesc samplerPoolDesc = {};
samplerPoolDesc.type = DescriptorHeapType::Sampler;
samplerPoolDesc.descriptorCount = 1;
samplerPoolDesc.shaderVisible = true;
gSamplerPool = gDevice.CreateDescriptorPool(samplerPoolDesc);
if (gSamplerPool == nullptr) {
Log("[ERROR] Failed to create sampler descriptor pool");
return false;
}
DescriptorSetLayoutBinding samplerBinding = {};
samplerBinding.binding = 0;
samplerBinding.type = static_cast<uint32_t>(DescriptorType::Sampler);
samplerBinding.count = 1;
DescriptorSetLayoutDesc samplerLayoutDesc = {};
samplerLayoutDesc.bindings = &samplerBinding;
samplerLayoutDesc.bindingCount = 1;
gSamplerSet = gSamplerPool->AllocateSet(samplerLayoutDesc);
if (gSamplerSet == nullptr) {
Log("[ERROR] Failed to allocate sampler descriptor set");
return false;
}
gSamplerSet->UpdateSampler(0, gSampler);
return true;
}
GraphicsPipelineDesc CreateQuadPipelineDesc() {
GraphicsPipelineDesc desc = {};
desc.pipelineLayout = gPipelineLayout;
desc.topologyType = static_cast<uint32_t>(PrimitiveTopologyType::Triangle);
desc.renderTargetFormats[0] = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
desc.depthStencilFormat = static_cast<uint32_t>(Format::Unknown);
desc.sampleCount = 1;
desc.rasterizerState.fillMode = static_cast<uint32_t>(FillMode::Solid);
desc.rasterizerState.cullMode = static_cast<uint32_t>(CullMode::None);
desc.rasterizerState.frontFace = static_cast<uint32_t>(FrontFace::CounterClockwise);
desc.rasterizerState.depthClipEnable = true;
desc.depthStencilState.depthTestEnable = false;
desc.depthStencilState.depthWriteEnable = false;
desc.depthStencilState.stencilEnable = false;
InputElementDesc position = {};
position.semanticName = "POSITION";
position.semanticIndex = 0;
position.format = static_cast<uint32_t>(Format::R32G32B32A32_Float);
position.inputSlot = 0;
position.alignedByteOffset = 0;
desc.inputLayout.elements.push_back(position);
InputElementDesc texcoord = {};
texcoord.semanticName = "TEXCOORD";
texcoord.semanticIndex = 0;
texcoord.format = static_cast<uint32_t>(Format::R32G32_Float);
texcoord.inputSlot = 0;
texcoord.alignedByteOffset = sizeof(float) * 4;
desc.inputLayout.elements.push_back(texcoord);
desc.vertexShader.source.assign(kQuadHlsl, kQuadHlsl + strlen(kQuadHlsl));
desc.vertexShader.sourceLanguage = ShaderLanguage::HLSL;
desc.vertexShader.entryPoint = L"MainVS";
desc.vertexShader.profile = L"vs_5_0";
desc.fragmentShader.source.assign(kQuadHlsl, kQuadHlsl + strlen(kQuadHlsl));
desc.fragmentShader.sourceLanguage = ShaderLanguage::HLSL;
desc.fragmentShader.entryPoint = L"MainPS";
desc.fragmentShader.profile = L"ps_5_0";
return desc;
}
bool InitializeQuadResources() {
BufferDesc vertexBufferDesc = {};
vertexBufferDesc.size = sizeof(kQuadVertices);
vertexBufferDesc.stride = sizeof(Vertex);
vertexBufferDesc.bufferType = static_cast<uint32_t>(BufferType::Vertex);
gVertexBuffer = gDevice.CreateBuffer(vertexBufferDesc);
if (gVertexBuffer == nullptr) {
Log("[ERROR] Failed to create vertex buffer");
return false;
}
gVertexBuffer->SetData(kQuadVertices, sizeof(kQuadVertices));
gVertexBuffer->SetStride(sizeof(Vertex));
gVertexBuffer->SetBufferType(BufferType::Vertex);
ResourceViewDesc vertexViewDesc = {};
vertexViewDesc.dimension = ResourceViewDimension::Buffer;
vertexViewDesc.structureByteStride = sizeof(Vertex);
gVertexBufferView = gDevice.CreateVertexBufferView(gVertexBuffer, vertexViewDesc);
if (gVertexBufferView == nullptr) {
Log("[ERROR] Failed to create vertex buffer view");
return false;
}
BufferDesc indexBufferDesc = {};
indexBufferDesc.size = sizeof(kQuadIndices);
indexBufferDesc.stride = sizeof(uint32_t);
indexBufferDesc.bufferType = static_cast<uint32_t>(BufferType::Index);
gIndexBuffer = gDevice.CreateBuffer(indexBufferDesc);
if (gIndexBuffer == nullptr) {
Log("[ERROR] Failed to create index buffer");
return false;
}
gIndexBuffer->SetData(kQuadIndices, sizeof(kQuadIndices));
gIndexBuffer->SetStride(sizeof(uint32_t));
gIndexBuffer->SetBufferType(BufferType::Index);
ResourceViewDesc indexViewDesc = {};
indexViewDesc.dimension = ResourceViewDimension::Buffer;
indexViewDesc.format = static_cast<uint32_t>(Format::R32_UInt);
gIndexBufferView = gDevice.CreateIndexBufferView(gIndexBuffer, indexViewDesc);
if (gIndexBufferView == nullptr) {
Log("[ERROR] Failed to create index buffer view");
return false;
}
if (!LoadTexture()) {
return false;
}
RHIPipelineLayoutDesc pipelineLayoutDesc = {};
pipelineLayoutDesc.textureCount = 1;
pipelineLayoutDesc.samplerCount = 1;
gPipelineLayout = gDevice.CreatePipelineLayout(pipelineLayoutDesc);
if (gPipelineLayout == nullptr) {
Log("[ERROR] Failed to create pipeline layout");
return false;
}
GraphicsPipelineDesc pipelineDesc = CreateQuadPipelineDesc();
gPipelineState = gDevice.CreatePipelineState(pipelineDesc);
if (gPipelineState == nullptr || !gPipelineState->IsValid()) {
Log("[ERROR] Failed to create pipeline state");
return false;
}
Log("[INFO] Quad resources initialized successfully");
return true;
}
void ShutdownQuadResources() {
ShutdownAndDelete(gPipelineState);
ShutdownAndDelete(gPipelineLayout);
ShutdownAndDelete(gTextureSet);
ShutdownAndDelete(gSamplerSet);
ShutdownAndDelete(gTexturePool);
ShutdownAndDelete(gSamplerPool);
ShutdownAndDelete(gSampler);
ShutdownAndDelete(gTextureView);
ShutdownAndDelete(gTexture);
ShutdownAndDelete(gVertexBufferView);
ShutdownAndDelete(gIndexBufferView);
ShutdownAndDelete(gVertexBuffer);
ShutdownAndDelete(gIndexBuffer);
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
bool InitD3D12() {
RHIDeviceDesc deviceDesc = {};
deviceDesc.adapterIndex = 0;
deviceDesc.enableDebugLayer = false;
deviceDesc.enableGPUValidation = false;
if (!gDevice.Initialize(deviceDesc)) {
Log("[ERROR] Failed to initialize D3D12 device");
return false;
}
ID3D12Device* device = gDevice.GetDevice();
IDXGIFactory4* factory = gDevice.GetFactory();
if (!gCommandQueue.Initialize(device, CommandQueueType::Direct)) {
Log("[ERROR] Failed to initialize command queue");
return false;
}
if (!gSwapChain.Initialize(factory, gCommandQueue.GetCommandQueue(), gHWND, gWidth, gHeight, 2)) {
Log("[ERROR] Failed to initialize swap chain");
return false;
}
if (!gDepthStencil.InitializeDepthStencil(device, gWidth, gHeight)) {
Log("[ERROR] Failed to initialize depth stencil");
return false;
}
if (!gRTVHeap.Initialize(device, DescriptorHeapType::RTV, 2)) {
Log("[ERROR] Failed to initialize RTV heap");
return false;
}
gRTVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::RTV);
if (!gDSVHeap.Initialize(device, DescriptorHeapType::DSV, 1)) {
Log("[ERROR] Failed to initialize DSV heap");
return false;
}
gDSVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::DSV);
for (int i = 0; i < 2; ++i) {
D3D12Texture& backBuffer = gSwapChain.GetBackBuffer(i);
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = D3D12ResourceView::CreateRenderTargetDesc(
Format::R8G8B8A8_UNorm,
D3D12_RTV_DIMENSION_TEXTURE2D);
gRTVs[i].InitializeAsRenderTarget(device, backBuffer.GetResource(), &rtvDesc, &gRTVHeap, i);
}
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = D3D12ResourceView::CreateDepthStencilDesc(
Format::D24_UNorm_S8_UInt,
D3D12_DSV_DIMENSION_TEXTURE2D);
gDSV.InitializeAsDepthStencil(device, gDepthStencil.GetResource(), &dsvDesc, &gDSVHeap, 0);
if (!gCommandAllocator.Initialize(device, CommandQueueType::Direct)) {
Log("[ERROR] Failed to initialize command allocator");
return false;
}
if (!gCommandList.Initialize(device, CommandQueueType::Direct, gCommandAllocator.GetCommandAllocator())) {
Log("[ERROR] Failed to initialize command list");
return false;
}
if (!InitializeQuadResources()) {
Log("[ERROR] Failed to initialize quad resources");
return false;
}
Log("[INFO] D3D12 initialized successfully");
return true;
}
void WaitForGPU() {
gCommandQueue.WaitForIdle();
}
void ExecuteCommandList() {
gCommandList.Close();
void* commandLists[] = { &gCommandList };
gCommandQueue.ExecuteCommandLists(1, commandLists);
}
void BeginRender() {
gCurrentRTIndex = gSwapChain.GetCurrentBackBufferIndex();
D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex);
gCommandList.TransitionBarrier(currentBackBuffer.GetResource(), ResourceStates::Present, ResourceStates::RenderTarget);
CPUDescriptorHandle rtvCpuHandle = gRTVHeap.GetCPUDescriptorHandle(gCurrentRTIndex);
CPUDescriptorHandle dsvCpuHandle = gDSVHeap.GetCPUDescriptorHandle(0);
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr };
D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = { dsvCpuHandle.ptr };
gCommandList.SetRenderTargetsHandle(1, &rtvHandle, &dsvHandle);
Viewport viewport = { 0.0f, 0.0f, static_cast<float>(gWidth), static_cast<float>(gHeight), 0.0f, 1.0f };
Rect scissorRect = { 0, 0, gWidth, gHeight };
gCommandList.SetViewport(viewport);
gCommandList.SetScissorRect(scissorRect);
const float clearColor[] = { 0.0f, 0.0f, 1.0f, 1.0f };
gCommandList.ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
gCommandList.ClearDepthStencilView(
dsvHandle,
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,
1.0f,
0,
0,
nullptr);
}
void EndRender() {
D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex);
gCommandList.TransitionBarrier(currentBackBuffer.GetResource(), ResourceStates::RenderTarget, ResourceStates::Present);
}
void RenderQuad() {
RHIDescriptorSet* descriptorSets[] = { gTextureSet, gSamplerSet };
RHIResourceView* vertexBuffers[] = { gVertexBufferView };
const uint64_t offsets[] = { 0 };
const uint32_t strides[] = { sizeof(Vertex) };
gCommandList.SetPipelineState(gPipelineState);
gCommandList.SetGraphicsDescriptorSets(0, 2, descriptorSets, gPipelineLayout);
gCommandList.SetPrimitiveTopology(PrimitiveTopology::TriangleList);
gCommandList.SetVertexBuffers(0, 1, vertexBuffers, offsets, strides);
gCommandList.SetIndexBuffer(gIndexBufferView, 0);
gCommandList.DrawIndexed(static_cast<uint32_t>(sizeof(kQuadIndices) / sizeof(kQuadIndices[0])));
}
} // namespace
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
Logger::Get().Initialize();
Logger::Get().AddSink(std::make_unique<ConsoleLogSink>());
Logger::Get().AddSink(std::make_unique<FileLogSink>("quad_test.log"));
Logger::Get().SetMinimumLevel(LogLevel::Debug);
Log("[INFO] D3D12 Quad Test Starting");
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(WNDCLASSEXW);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"D3D12QuadTest";
if (!RegisterClassExW(&wc)) {
Log("[ERROR] Failed to register window class");
return -1;
}
RECT rect = { 0, 0, gWidth, gHeight };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
gHWND = CreateWindowExW(
0,
L"D3D12QuadTest",
L"D3D12 Quad Test",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
rect.right - rect.left,
rect.bottom - rect.top,
NULL,
NULL,
hInstance,
NULL);
if (!gHWND) {
Log("[ERROR] Failed to create window");
return -1;
}
RenderDocCapture::Get().Initialize(nullptr, gHWND);
RenderDocCapture::Get().SetCaptureFilePath(".\\quad_frame30");
if (!InitD3D12()) {
Log("[ERROR] Failed to initialize D3D12");
return -1;
}
RenderDocCapture::Get().SetDevice(gDevice.GetDevice());
ShowWindow(gHWND, nShowCmd);
UpdateWindow(gHWND);
MSG msg = {};
int frameCount = 0;
const int targetFrameCount = 30;
while (true) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
if (frameCount > 0) {
gCommandQueue.WaitForPreviousFrame();
}
gCommandAllocator.Reset();
gCommandList.Reset();
BeginRender();
RenderQuad();
frameCount++;
if (frameCount >= targetFrameCount) {
Log("[INFO] Reached target frame count %d - taking screenshot!", targetFrameCount);
ExecuteCommandList();
if (RenderDocCapture::Get().EndCapture()) {
Log("[INFO] RenderDoc capture ended");
}
WaitForGPU();
const bool screenshotResult = D3D12Screenshot::Capture(
gDevice,
gCommandQueue,
gSwapChain.GetBackBuffer(gCurrentRTIndex),
"quad.ppm");
if (screenshotResult) {
Log("[INFO] Screenshot saved to quad.ppm");
} else {
Log("[ERROR] Screenshot failed");
}
break;
}
if (frameCount == targetFrameCount - 1) {
if (RenderDocCapture::Get().BeginCapture("D3D12_Quad_Test")) {
Log("[INFO] RenderDoc capture started");
}
}
EndRender();
ExecuteCommandList();
gSwapChain.Present(0, 0);
}
}
ShutdownQuadResources();
gCommandList.Shutdown();
gCommandAllocator.Shutdown();
gSwapChain.Shutdown();
gDevice.Shutdown();
RenderDocCapture::Get().Shutdown();
Logger::Get().Shutdown();
return 0;
}

View File

@@ -1,124 +0,0 @@
import sys
import os
import subprocess
import time
import shutil
def run_integration_test(exe_path, output_ppm, gt_ppm, threshold, timeout=120):
"""
Run a D3D12 integration test and compare output with golden template.
Args:
exe_path: Path to the test executable
output_ppm: Filename of the output screenshot
gt_ppm: Path to the golden template PPM file
threshold: Pixel difference threshold for comparison
timeout: Maximum time to wait for test completion (seconds)
Returns:
0 on success, non-zero on failure
"""
exe_dir = os.path.dirname(os.path.abspath(exe_path))
output_path = os.path.join(exe_dir, output_ppm)
print(f"[Integration Test] Starting: {exe_path}")
print(f"[Integration Test] Working directory: {exe_dir}")
print(f"[Integration Test] Expected output: {output_path}")
if not os.path.exists(exe_path):
print(f"[Integration Test] ERROR: Executable not found: {exe_path}")
return 1
if not os.path.exists(gt_ppm):
print(f"[Integration Test] ERROR: Golden template not found: {gt_ppm}")
return 1
if os.path.exists(output_path):
print(f"[Integration Test] Removing old output: {output_path}")
os.remove(output_path)
try:
print(f"[Integration Test] Launching process...")
start_time = time.time()
process = subprocess.Popen(
[exe_path],
cwd=exe_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == "nt" else 0,
)
returncode = None
while time.time() - start_time < timeout:
returncode = process.poll()
if returncode is not None:
break
time.sleep(0.5)
if returncode is None:
print(f"[Integration Test] ERROR: Process timed out after {timeout}s")
process.kill()
return 1
elapsed = time.time() - start_time
print(
f"[Integration Test] Process finished in {elapsed:.1f}s with exit code: {returncode}"
)
if returncode != 0:
print(f"[Integration Test] ERROR: Process returned non-zero exit code")
stdout, stderr = process.communicate(timeout=5)
if stdout:
print(
f"[Integration Test] STDOUT:\n{stdout.decode('utf-8', errors='replace')}"
)
if stderr:
print(
f"[Integration Test] STDERR:\n{stderr.decode('utf-8', errors='replace')}"
)
return 1
except Exception as e:
print(f"[Integration Test] ERROR: Failed to run process: {e}")
return 1
if not os.path.exists(output_path):
print(f"[Integration Test] ERROR: Output file not created: {output_path}")
return 1
print(f"[Integration Test] Running image comparison...")
script_dir = os.path.dirname(os.path.abspath(__file__))
compare_script = os.path.join(script_dir, "compare_ppm.py")
try:
result = subprocess.run(
[sys.executable, compare_script, output_path, gt_ppm, str(threshold)],
cwd=exe_dir,
capture_output=True,
text=True,
)
print(result.stdout)
if result.stderr:
print(f"[Integration Test] Comparison STDERR: {result.stderr}")
return result.returncode
except Exception as e:
print(f"[Integration Test] ERROR: Failed to run comparison: {e}")
return 1
if __name__ == "__main__":
if len(sys.argv) != 5:
print(
"Usage: run_integration_test.py <exe_path> <output_ppm> <gt_ppm> <threshold>"
)
sys.exit(1)
exe_path = sys.argv[1]
output_ppm = sys.argv[2]
gt_ppm = sys.argv[3]
threshold = int(sys.argv[4])
exit_code = run_integration_test(exe_path, output_ppm, gt_ppm, threshold)
sys.exit(exit_code)

View File

@@ -1,61 +0,0 @@
cmake_minimum_required(VERSION 3.15)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
project(d3d12_sphere_test)
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
add_executable(d3d12_sphere_test
WIN32
main.cpp
)
target_include_directories(d3d12_sphere_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${ENGINE_ROOT_DIR}/include
${ENGINE_ROOT_DIR}
)
target_compile_definitions(d3d12_sphere_test PRIVATE
UNICODE
_UNICODE
)
target_link_libraries(d3d12_sphere_test PRIVATE
d3d12
dxgi
d3dcompiler
winmm
XCEngine
)
add_custom_command(TARGET d3d12_sphere_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/Res
$<TARGET_FILE_DIR:d3d12_sphere_test>/Res
)
add_custom_command(TARGET d3d12_sphere_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/compare_ppm.py
$<TARGET_FILE_DIR:d3d12_sphere_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/run_integration_test.py
$<TARGET_FILE_DIR:d3d12_sphere_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
$<TARGET_FILE_DIR:d3d12_sphere_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
$<TARGET_FILE_DIR:d3d12_sphere_test>/
)
add_test(NAME d3d12_sphere_test
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:d3d12_sphere_test>/run_integration_test.py
$<TARGET_FILE:d3d12_sphere_test>
sphere.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:d3d12_sphere_test>
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -1,32 +0,0 @@
struct Vertex {
float4 pos : POSITION;
float4 texcoord : TEXCOORD0;
};
struct VSOut {
float4 pos : SV_POSITION;
float4 texcoord : TEXCOORD0;
};
Texture2D gDiffuseTexture : register(t0);
SamplerState gSampler : register(s0);
cbuffer MatrixBuffer : register(b0) {
float4x4 gProjectionMatrix;
float4x4 gViewMatrix;
float4x4 gModelMatrix;
float4x4 gIT_ModelMatrix;
};
VSOut MainVS(Vertex v) {
VSOut o;
float4 positionWS = mul(gModelMatrix, v.pos);
float4 positionVS = mul(gViewMatrix, positionWS);
o.pos = mul(gProjectionMatrix, positionVS);
o.texcoord = v.texcoord;
return o;
}
float4 MainPS(VSOut i) : SV_TARGET {
return gDiffuseTexture.Sample(gSampler, float2(i.texcoord.x, i.texcoord.y));
}

View File

@@ -1,785 +0,0 @@
#include <windows.h>
#include <filesystem>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <vector>
#include <d3d12.h>
#include <dxgi1_4.h>
#include "XCEngine/RHI/RHIEnums.h"
#include "XCEngine/RHI/RHITypes.h"
#include "XCEngine/RHI/RHIBuffer.h"
#include "XCEngine/RHI/RHIDescriptorPool.h"
#include "XCEngine/RHI/RHIDescriptorSet.h"
#include "XCEngine/RHI/RHIPipelineLayout.h"
#include "XCEngine/RHI/RHIPipelineState.h"
#include "XCEngine/RHI/RHIResourceView.h"
#include "XCEngine/RHI/RHISampler.h"
#include "XCEngine/RHI/RHITexture.h"
#include "XCEngine/RHI/D3D12/D3D12Device.h"
#include "XCEngine/RHI/D3D12/D3D12CommandQueue.h"
#include "XCEngine/RHI/D3D12/D3D12CommandAllocator.h"
#include "XCEngine/RHI/D3D12/D3D12CommandList.h"
#include "XCEngine/RHI/D3D12/D3D12DescriptorHeap.h"
#include "XCEngine/RHI/D3D12/D3D12SwapChain.h"
#include "XCEngine/RHI/D3D12/D3D12Texture.h"
#include "XCEngine/RHI/D3D12/D3D12ResourceView.h"
#include "XCEngine/RHI/D3D12/D3D12Screenshot.h"
#include "XCEngine/Core/Math/Matrix4.h"
#include "XCEngine/Core/Math/Vector3.h"
#include "XCEngine/Debug/Logger.h"
#include "XCEngine/Debug/ConsoleLogSink.h"
#include "XCEngine/Debug/FileLogSink.h"
#include "XCEngine/Debug/RenderDocCapture.h"
#include "XCEngine/Core/Containers/String.h"
#include "third_party/stb/stb_image.h"
using namespace XCEngine::RHI;
using namespace XCEngine::Debug;
using namespace XCEngine::Containers;
using namespace XCEngine::Math;
#pragma comment(lib,"d3d12.lib")
#pragma comment(lib,"dxgi.lib")
#pragma comment(lib,"dxguid.lib")
#pragma comment(lib,"d3dcompiler.lib")
#pragma comment(lib,"winmm.lib")
namespace {
struct Vertex {
float pos[4];
float uv[2];
};
struct MatrixBufferData {
Matrix4x4 projection;
Matrix4x4 view;
Matrix4x4 model;
};
constexpr float kSphereRadius = 1.0f;
constexpr int kSphereSegments = 32;
constexpr float kPi = 3.14159265358979323846f;
const char kSphereHlsl[] = R"(
Texture2D gDiffuseTexture : register(t0);
SamplerState gSampler : register(s0);
cbuffer MatrixBuffer : register(b0) {
float4x4 gProjectionMatrix;
float4x4 gViewMatrix;
float4x4 gModelMatrix;
};
struct VSInput {
float4 position : POSITION;
float2 texcoord : TEXCOORD0;
};
struct PSInput {
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
PSInput MainVS(VSInput input) {
PSInput output;
float4 positionWS = mul(gModelMatrix, input.position);
float4 positionVS = mul(gViewMatrix, positionWS);
output.position = mul(gProjectionMatrix, positionVS);
output.texcoord = input.texcoord;
return output;
}
float4 MainPS(PSInput input) : SV_TARGET {
return gDiffuseTexture.Sample(gSampler, input.texcoord);
}
)";
D3D12Device gDevice;
D3D12CommandQueue gCommandQueue;
D3D12SwapChain gSwapChain;
D3D12CommandAllocator gCommandAllocator;
D3D12CommandList gCommandList;
D3D12Texture gDepthStencil;
D3D12DescriptorHeap gRTVHeap;
D3D12DescriptorHeap gDSVHeap;
D3D12ResourceView gRTVs[2];
D3D12ResourceView gDSV;
std::vector<Vertex> gVertices;
std::vector<uint32_t> gIndices;
RHIBuffer* gVertexBuffer = nullptr;
RHIResourceView* gVertexBufferView = nullptr;
RHIBuffer* gIndexBuffer = nullptr;
RHIResourceView* gIndexBufferView = nullptr;
RHITexture* gTexture = nullptr;
RHIResourceView* gTextureView = nullptr;
RHISampler* gSampler = nullptr;
RHIDescriptorPool* gConstantPool = nullptr;
RHIDescriptorSet* gConstantSet = nullptr;
RHIDescriptorPool* gTexturePool = nullptr;
RHIDescriptorSet* gTextureSet = nullptr;
RHIDescriptorPool* gSamplerPool = nullptr;
RHIDescriptorSet* gSamplerSet = nullptr;
RHIPipelineLayout* gPipelineLayout = nullptr;
RHIPipelineState* gPipelineState = nullptr;
UINT gRTVDescriptorSize = 0;
UINT gDSVDescriptorSize = 0;
int gCurrentRTIndex = 0;
HWND gHWND = nullptr;
int gWidth = 1280;
int gHeight = 720;
template <typename T>
void ShutdownAndDelete(T*& object) {
if (object != nullptr) {
object->Shutdown();
delete object;
object = nullptr;
}
}
void Log(const char* format, ...) {
char buffer[1024];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
Logger::Get().Debug(LogCategory::Rendering, String(buffer));
}
std::filesystem::path GetExecutableDirectory() {
char exePath[MAX_PATH] = {};
const DWORD length = GetModuleFileNameA(nullptr, exePath, MAX_PATH);
if (length == 0 || length >= MAX_PATH) {
return std::filesystem::current_path();
}
return std::filesystem::path(exePath).parent_path();
}
std::filesystem::path ResolveRuntimePath(const char* relativePath) {
return GetExecutableDirectory() / relativePath;
}
void GenerateSphere(std::vector<Vertex>& vertices, std::vector<uint32_t>& indices, float radius, int segments) {
vertices.clear();
indices.clear();
segments = segments < 3 ? 3 : segments;
for (int lat = 0; lat <= segments; ++lat) {
const float phi = kPi * static_cast<float>(lat) / static_cast<float>(segments);
const float sinPhi = sinf(phi);
const float cosPhi = cosf(phi);
for (int lon = 0; lon <= segments; ++lon) {
const float theta = (kPi * 2.0f) * static_cast<float>(lon) / static_cast<float>(segments);
const float sinTheta = sinf(theta);
const float cosTheta = cosf(theta);
Vertex vertex = {};
vertex.pos[0] = radius * sinPhi * cosTheta;
vertex.pos[1] = radius * cosPhi;
vertex.pos[2] = radius * sinPhi * sinTheta;
vertex.pos[3] = 1.0f;
vertex.uv[0] = static_cast<float>(lon) / static_cast<float>(segments);
vertex.uv[1] = static_cast<float>(lat) / static_cast<float>(segments);
vertices.push_back(vertex);
}
}
for (int lat = 0; lat < segments; ++lat) {
for (int lon = 0; lon < segments; ++lon) {
const uint32_t topLeft = static_cast<uint32_t>(lat * (segments + 1) + lon);
const uint32_t topRight = topLeft + 1;
const uint32_t bottomLeft = static_cast<uint32_t>((lat + 1) * (segments + 1) + lon);
const uint32_t bottomRight = bottomLeft + 1;
indices.push_back(topLeft);
indices.push_back(bottomLeft);
indices.push_back(topRight);
indices.push_back(topRight);
indices.push_back(bottomLeft);
indices.push_back(bottomRight);
}
}
}
MatrixBufferData CreateMatrixBufferData() {
const float aspect = 1280.0f / 720.0f;
const Matrix4x4 projection = Matrix4x4::Perspective(45.0f * 3.141592f / 180.0f, aspect, 0.1f, 1000.0f);
const Matrix4x4 view = Matrix4x4::Identity();
const Matrix4x4 model = Matrix4x4::Translation(Vector3(0.0f, 0.0f, 5.0f));
MatrixBufferData data = {};
data.projection = projection.Transpose();
data.view = view.Transpose();
data.model = model.Transpose();
return data;
}
bool LoadTexture() {
const std::filesystem::path texturePath = ResolveRuntimePath("Res/Image/earth.png");
const std::string texturePathString = texturePath.string();
stbi_set_flip_vertically_on_load(0);
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* pixels = stbi_load(texturePathString.c_str(), &width, &height, &channels, STBI_rgb_alpha);
if (pixels == nullptr) {
Log("[ERROR] Failed to load texture: %s", texturePathString.c_str());
return false;
}
TextureDesc textureDesc = {};
textureDesc.width = static_cast<uint32_t>(width);
textureDesc.height = static_cast<uint32_t>(height);
textureDesc.depth = 1;
textureDesc.mipLevels = 1;
textureDesc.arraySize = 1;
textureDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
textureDesc.textureType = static_cast<uint32_t>(TextureType::Texture2D);
textureDesc.sampleCount = 1;
textureDesc.sampleQuality = 0;
textureDesc.flags = 0;
gTexture = gDevice.CreateTexture(
textureDesc,
pixels,
static_cast<size_t>(width) * static_cast<size_t>(height) * 4,
static_cast<uint32_t>(width) * 4);
stbi_image_free(pixels);
if (gTexture == nullptr) {
Log("[ERROR] Failed to create RHI texture");
return false;
}
ResourceViewDesc textureViewDesc = {};
textureViewDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
textureViewDesc.dimension = ResourceViewDimension::Texture2D;
textureViewDesc.mipLevel = 0;
gTextureView = gDevice.CreateShaderResourceView(gTexture, textureViewDesc);
if (gTextureView == nullptr) {
Log("[ERROR] Failed to create texture SRV");
return false;
}
SamplerDesc samplerDesc = {};
samplerDesc.filter = static_cast<uint32_t>(FilterMode::Linear);
samplerDesc.addressU = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressV = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressW = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.mipLodBias = 0.0f;
samplerDesc.maxAnisotropy = 1;
samplerDesc.comparisonFunc = static_cast<uint32_t>(ComparisonFunc::Always);
samplerDesc.borderColorR = 0.0f;
samplerDesc.borderColorG = 0.0f;
samplerDesc.borderColorB = 0.0f;
samplerDesc.borderColorA = 0.0f;
samplerDesc.minLod = 0.0f;
samplerDesc.maxLod = 1000.0f;
gSampler = gDevice.CreateSampler(samplerDesc);
if (gSampler == nullptr) {
Log("[ERROR] Failed to create sampler");
return false;
}
DescriptorPoolDesc constantPoolDesc = {};
constantPoolDesc.type = DescriptorHeapType::CBV_SRV_UAV;
constantPoolDesc.descriptorCount = 1;
constantPoolDesc.shaderVisible = false;
gConstantPool = gDevice.CreateDescriptorPool(constantPoolDesc);
if (gConstantPool == nullptr) {
Log("[ERROR] Failed to create constant descriptor pool");
return false;
}
DescriptorSetLayoutBinding constantBinding = {};
constantBinding.binding = 0;
constantBinding.type = static_cast<uint32_t>(DescriptorType::CBV);
constantBinding.count = 1;
DescriptorSetLayoutDesc constantLayoutDesc = {};
constantLayoutDesc.bindings = &constantBinding;
constantLayoutDesc.bindingCount = 1;
gConstantSet = gConstantPool->AllocateSet(constantLayoutDesc);
if (gConstantSet == nullptr) {
Log("[ERROR] Failed to allocate constant descriptor set");
return false;
}
const MatrixBufferData matrixData = CreateMatrixBufferData();
gConstantSet->WriteConstant(0, &matrixData, sizeof(matrixData));
DescriptorPoolDesc texturePoolDesc = {};
texturePoolDesc.type = DescriptorHeapType::CBV_SRV_UAV;
texturePoolDesc.descriptorCount = 1;
texturePoolDesc.shaderVisible = true;
gTexturePool = gDevice.CreateDescriptorPool(texturePoolDesc);
if (gTexturePool == nullptr) {
Log("[ERROR] Failed to create texture descriptor pool");
return false;
}
DescriptorSetLayoutBinding textureBinding = {};
textureBinding.binding = 0;
textureBinding.type = static_cast<uint32_t>(DescriptorType::SRV);
textureBinding.count = 1;
DescriptorSetLayoutDesc textureLayoutDesc = {};
textureLayoutDesc.bindings = &textureBinding;
textureLayoutDesc.bindingCount = 1;
gTextureSet = gTexturePool->AllocateSet(textureLayoutDesc);
if (gTextureSet == nullptr) {
Log("[ERROR] Failed to allocate texture descriptor set");
return false;
}
gTextureSet->Update(0, gTextureView);
DescriptorPoolDesc samplerPoolDesc = {};
samplerPoolDesc.type = DescriptorHeapType::Sampler;
samplerPoolDesc.descriptorCount = 1;
samplerPoolDesc.shaderVisible = true;
gSamplerPool = gDevice.CreateDescriptorPool(samplerPoolDesc);
if (gSamplerPool == nullptr) {
Log("[ERROR] Failed to create sampler descriptor pool");
return false;
}
DescriptorSetLayoutBinding samplerBinding = {};
samplerBinding.binding = 0;
samplerBinding.type = static_cast<uint32_t>(DescriptorType::Sampler);
samplerBinding.count = 1;
DescriptorSetLayoutDesc samplerLayoutDesc = {};
samplerLayoutDesc.bindings = &samplerBinding;
samplerLayoutDesc.bindingCount = 1;
gSamplerSet = gSamplerPool->AllocateSet(samplerLayoutDesc);
if (gSamplerSet == nullptr) {
Log("[ERROR] Failed to allocate sampler descriptor set");
return false;
}
gSamplerSet->UpdateSampler(0, gSampler);
return true;
}
GraphicsPipelineDesc CreateSpherePipelineDesc() {
GraphicsPipelineDesc desc = {};
desc.pipelineLayout = gPipelineLayout;
desc.topologyType = static_cast<uint32_t>(PrimitiveTopologyType::Triangle);
desc.renderTargetFormats[0] = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
desc.depthStencilFormat = static_cast<uint32_t>(Format::D24_UNorm_S8_UInt);
desc.sampleCount = 1;
desc.rasterizerState.fillMode = static_cast<uint32_t>(FillMode::Solid);
desc.rasterizerState.cullMode = static_cast<uint32_t>(CullMode::None);
desc.rasterizerState.frontFace = static_cast<uint32_t>(FrontFace::CounterClockwise);
desc.rasterizerState.depthClipEnable = true;
desc.depthStencilState.depthTestEnable = true;
desc.depthStencilState.depthWriteEnable = true;
desc.depthStencilState.depthFunc = static_cast<uint32_t>(ComparisonFunc::Less);
desc.depthStencilState.stencilEnable = false;
InputElementDesc position = {};
position.semanticName = "POSITION";
position.semanticIndex = 0;
position.format = static_cast<uint32_t>(Format::R32G32B32A32_Float);
position.inputSlot = 0;
position.alignedByteOffset = 0;
desc.inputLayout.elements.push_back(position);
InputElementDesc texcoord = {};
texcoord.semanticName = "TEXCOORD";
texcoord.semanticIndex = 0;
texcoord.format = static_cast<uint32_t>(Format::R32G32_Float);
texcoord.inputSlot = 0;
texcoord.alignedByteOffset = sizeof(float) * 4;
desc.inputLayout.elements.push_back(texcoord);
desc.vertexShader.source.assign(kSphereHlsl, kSphereHlsl + strlen(kSphereHlsl));
desc.vertexShader.sourceLanguage = ShaderLanguage::HLSL;
desc.vertexShader.entryPoint = L"MainVS";
desc.vertexShader.profile = L"vs_5_0";
desc.fragmentShader.source.assign(kSphereHlsl, kSphereHlsl + strlen(kSphereHlsl));
desc.fragmentShader.sourceLanguage = ShaderLanguage::HLSL;
desc.fragmentShader.entryPoint = L"MainPS";
desc.fragmentShader.profile = L"ps_5_0";
return desc;
}
bool InitializeSphereResources() {
GenerateSphere(gVertices, gIndices, kSphereRadius, kSphereSegments);
if (gVertices.empty() || gIndices.empty()) {
Log("[ERROR] Failed to generate sphere geometry");
return false;
}
BufferDesc vertexBufferDesc = {};
vertexBufferDesc.size = static_cast<uint64_t>(gVertices.size() * sizeof(Vertex));
vertexBufferDesc.stride = sizeof(Vertex);
vertexBufferDesc.bufferType = static_cast<uint32_t>(BufferType::Vertex);
gVertexBuffer = gDevice.CreateBuffer(vertexBufferDesc);
if (gVertexBuffer == nullptr) {
Log("[ERROR] Failed to create vertex buffer");
return false;
}
gVertexBuffer->SetData(gVertices.data(), gVertices.size() * sizeof(Vertex));
gVertexBuffer->SetStride(sizeof(Vertex));
gVertexBuffer->SetBufferType(BufferType::Vertex);
ResourceViewDesc vertexViewDesc = {};
vertexViewDesc.dimension = ResourceViewDimension::Buffer;
vertexViewDesc.structureByteStride = sizeof(Vertex);
gVertexBufferView = gDevice.CreateVertexBufferView(gVertexBuffer, vertexViewDesc);
if (gVertexBufferView == nullptr) {
Log("[ERROR] Failed to create vertex buffer view");
return false;
}
BufferDesc indexBufferDesc = {};
indexBufferDesc.size = static_cast<uint64_t>(gIndices.size() * sizeof(uint32_t));
indexBufferDesc.stride = sizeof(uint32_t);
indexBufferDesc.bufferType = static_cast<uint32_t>(BufferType::Index);
gIndexBuffer = gDevice.CreateBuffer(indexBufferDesc);
if (gIndexBuffer == nullptr) {
Log("[ERROR] Failed to create index buffer");
return false;
}
gIndexBuffer->SetData(gIndices.data(), gIndices.size() * sizeof(uint32_t));
gIndexBuffer->SetStride(sizeof(uint32_t));
gIndexBuffer->SetBufferType(BufferType::Index);
ResourceViewDesc indexViewDesc = {};
indexViewDesc.dimension = ResourceViewDimension::Buffer;
indexViewDesc.format = static_cast<uint32_t>(Format::R32_UInt);
gIndexBufferView = gDevice.CreateIndexBufferView(gIndexBuffer, indexViewDesc);
if (gIndexBufferView == nullptr) {
Log("[ERROR] Failed to create index buffer view");
return false;
}
if (!LoadTexture()) {
return false;
}
RHIPipelineLayoutDesc pipelineLayoutDesc = {};
pipelineLayoutDesc.constantBufferCount = 1;
pipelineLayoutDesc.textureCount = 1;
pipelineLayoutDesc.samplerCount = 1;
gPipelineLayout = gDevice.CreatePipelineLayout(pipelineLayoutDesc);
if (gPipelineLayout == nullptr) {
Log("[ERROR] Failed to create pipeline layout");
return false;
}
GraphicsPipelineDesc pipelineDesc = CreateSpherePipelineDesc();
gPipelineState = gDevice.CreatePipelineState(pipelineDesc);
if (gPipelineState == nullptr || !gPipelineState->IsValid()) {
Log("[ERROR] Failed to create pipeline state");
return false;
}
Log("[INFO] Sphere resources initialized successfully");
return true;
}
void ShutdownSphereResources() {
ShutdownAndDelete(gPipelineState);
ShutdownAndDelete(gPipelineLayout);
ShutdownAndDelete(gConstantSet);
ShutdownAndDelete(gTextureSet);
ShutdownAndDelete(gSamplerSet);
ShutdownAndDelete(gConstantPool);
ShutdownAndDelete(gTexturePool);
ShutdownAndDelete(gSamplerPool);
ShutdownAndDelete(gSampler);
ShutdownAndDelete(gTextureView);
ShutdownAndDelete(gTexture);
ShutdownAndDelete(gVertexBufferView);
ShutdownAndDelete(gIndexBufferView);
ShutdownAndDelete(gVertexBuffer);
ShutdownAndDelete(gIndexBuffer);
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
bool InitD3D12() {
RHIDeviceDesc deviceDesc = {};
deviceDesc.adapterIndex = 0;
deviceDesc.enableDebugLayer = false;
deviceDesc.enableGPUValidation = false;
if (!gDevice.Initialize(deviceDesc)) {
Log("[ERROR] Failed to initialize D3D12 device");
return false;
}
ID3D12Device* device = gDevice.GetDevice();
IDXGIFactory4* factory = gDevice.GetFactory();
if (!gCommandQueue.Initialize(device, CommandQueueType::Direct)) {
Log("[ERROR] Failed to initialize command queue");
return false;
}
if (!gSwapChain.Initialize(factory, gCommandQueue.GetCommandQueue(), gHWND, gWidth, gHeight, 2)) {
Log("[ERROR] Failed to initialize swap chain");
return false;
}
if (!gDepthStencil.InitializeDepthStencil(device, gWidth, gHeight)) {
Log("[ERROR] Failed to initialize depth stencil");
return false;
}
if (!gRTVHeap.Initialize(device, DescriptorHeapType::RTV, 2)) {
Log("[ERROR] Failed to initialize RTV heap");
return false;
}
gRTVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::RTV);
if (!gDSVHeap.Initialize(device, DescriptorHeapType::DSV, 1)) {
Log("[ERROR] Failed to initialize DSV heap");
return false;
}
gDSVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::DSV);
for (int i = 0; i < 2; ++i) {
D3D12Texture& backBuffer = gSwapChain.GetBackBuffer(i);
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = D3D12ResourceView::CreateRenderTargetDesc(
Format::R8G8B8A8_UNorm,
D3D12_RTV_DIMENSION_TEXTURE2D);
gRTVs[i].InitializeAsRenderTarget(device, backBuffer.GetResource(), &rtvDesc, &gRTVHeap, i);
}
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = D3D12ResourceView::CreateDepthStencilDesc(
Format::D24_UNorm_S8_UInt,
D3D12_DSV_DIMENSION_TEXTURE2D);
gDSV.InitializeAsDepthStencil(device, gDepthStencil.GetResource(), &dsvDesc, &gDSVHeap, 0);
if (!gCommandAllocator.Initialize(device, CommandQueueType::Direct)) {
Log("[ERROR] Failed to initialize command allocator");
return false;
}
if (!gCommandList.Initialize(device, CommandQueueType::Direct, gCommandAllocator.GetCommandAllocator())) {
Log("[ERROR] Failed to initialize command list");
return false;
}
if (!InitializeSphereResources()) {
Log("[ERROR] Failed to initialize sphere resources");
return false;
}
Log("[INFO] D3D12 initialized successfully");
return true;
}
void WaitForGPU() {
gCommandQueue.WaitForIdle();
}
void ExecuteCommandList() {
gCommandList.Close();
void* commandLists[] = { &gCommandList };
gCommandQueue.ExecuteCommandLists(1, commandLists);
}
void BeginRender() {
gCurrentRTIndex = gSwapChain.GetCurrentBackBufferIndex();
D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex);
gCommandList.TransitionBarrier(currentBackBuffer.GetResource(), ResourceStates::Present, ResourceStates::RenderTarget);
CPUDescriptorHandle rtvCpuHandle = gRTVHeap.GetCPUDescriptorHandle(gCurrentRTIndex);
CPUDescriptorHandle dsvCpuHandle = gDSVHeap.GetCPUDescriptorHandle(0);
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr };
D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = { dsvCpuHandle.ptr };
gCommandList.SetRenderTargetsHandle(1, &rtvHandle, &dsvHandle);
Viewport viewport = { 0.0f, 0.0f, static_cast<float>(gWidth), static_cast<float>(gHeight), 0.0f, 1.0f };
Rect scissorRect = { 0, 0, gWidth, gHeight };
gCommandList.SetViewport(viewport);
gCommandList.SetScissorRect(scissorRect);
const float clearColor[] = { 0.0f, 0.0f, 1.0f, 1.0f };
gCommandList.ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
gCommandList.ClearDepthStencilView(
dsvHandle,
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,
1.0f,
0,
0,
nullptr);
}
void EndRender() {
D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex);
gCommandList.TransitionBarrier(currentBackBuffer.GetResource(), ResourceStates::RenderTarget, ResourceStates::Present);
}
void RenderSphere() {
RHIDescriptorSet* descriptorSets[] = { gConstantSet, gTextureSet, gSamplerSet };
RHIResourceView* vertexBuffers[] = { gVertexBufferView };
const uint64_t offsets[] = { 0 };
const uint32_t strides[] = { sizeof(Vertex) };
gCommandList.SetPipelineState(gPipelineState);
gCommandList.SetGraphicsDescriptorSets(0, 3, descriptorSets, gPipelineLayout);
gCommandList.SetPrimitiveTopology(PrimitiveTopology::TriangleList);
gCommandList.SetVertexBuffers(0, 1, vertexBuffers, offsets, strides);
gCommandList.SetIndexBuffer(gIndexBufferView, 0);
gCommandList.DrawIndexed(static_cast<uint32_t>(gIndices.size()));
}
} // namespace
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
Logger::Get().Initialize();
Logger::Get().AddSink(std::make_unique<ConsoleLogSink>());
Logger::Get().AddSink(std::make_unique<FileLogSink>("sphere_test.log"));
Logger::Get().SetMinimumLevel(LogLevel::Debug);
Log("[INFO] D3D12 Sphere Test Starting");
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(WNDCLASSEXW);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"D3D12SphereTest";
if (!RegisterClassExW(&wc)) {
Log("[ERROR] Failed to register window class");
return -1;
}
RECT rect = { 0, 0, gWidth, gHeight };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
gHWND = CreateWindowExW(
0,
L"D3D12SphereTest",
L"D3D12 Sphere Test",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
rect.right - rect.left,
rect.bottom - rect.top,
NULL,
NULL,
hInstance,
NULL);
if (!gHWND) {
Log("[ERROR] Failed to create window");
return -1;
}
RenderDocCapture::Get().Initialize(nullptr, gHWND);
RenderDocCapture::Get().SetCaptureFilePath(".\\sphere_frame30");
if (!InitD3D12()) {
Log("[ERROR] Failed to initialize D3D12");
return -1;
}
RenderDocCapture::Get().SetDevice(gDevice.GetDevice());
ShowWindow(gHWND, nShowCmd);
UpdateWindow(gHWND);
MSG msg = {};
int frameCount = 0;
const int targetFrameCount = 30;
while (true) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
if (frameCount > 0) {
gCommandQueue.WaitForPreviousFrame();
}
gCommandAllocator.Reset();
gCommandList.Reset();
BeginRender();
RenderSphere();
frameCount++;
if (frameCount >= targetFrameCount) {
Log("[INFO] Reached target frame count %d - taking screenshot!", targetFrameCount);
ExecuteCommandList();
if (RenderDocCapture::Get().EndCapture()) {
Log("[INFO] RenderDoc capture ended");
}
WaitForGPU();
const bool screenshotResult = D3D12Screenshot::Capture(
gDevice,
gCommandQueue,
gSwapChain.GetBackBuffer(gCurrentRTIndex),
"sphere.ppm");
if (screenshotResult) {
Log("[INFO] Screenshot saved to sphere.ppm");
} else {
Log("[ERROR] Screenshot failed");
}
break;
}
if (frameCount == targetFrameCount - 1) {
if (RenderDocCapture::Get().BeginCapture("D3D12_Sphere_Test")) {
Log("[INFO] RenderDoc capture started");
}
}
EndRender();
ExecuteCommandList();
gSwapChain.Present(0, 0);
}
}
ShutdownSphereResources();
gCommandList.Shutdown();
gCommandAllocator.Shutdown();
gSwapChain.Shutdown();
gDevice.Shutdown();
RenderDocCapture::Get().Shutdown();
Logger::Get().Shutdown();
return 0;
}

View File

@@ -1,60 +0,0 @@
cmake_minimum_required(VERSION 3.15)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
project(d3d12_triangle_test)
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
add_executable(d3d12_triangle_test
WIN32
main.cpp
)
target_include_directories(d3d12_triangle_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${ENGINE_ROOT_DIR}/include
)
target_compile_definitions(d3d12_triangle_test PRIVATE
UNICODE
_UNICODE
)
target_link_libraries(d3d12_triangle_test PRIVATE
d3d12
dxgi
d3dcompiler
winmm
XCEngine
)
add_custom_command(TARGET d3d12_triangle_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/Res
$<TARGET_FILE_DIR:d3d12_triangle_test>/Res
)
add_custom_command(TARGET d3d12_triangle_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/compare_ppm.py
$<TARGET_FILE_DIR:d3d12_triangle_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/D3D12/integration/run_integration_test.py
$<TARGET_FILE_DIR:d3d12_triangle_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
$<TARGET_FILE_DIR:d3d12_triangle_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
$<TARGET_FILE_DIR:d3d12_triangle_test>/
)
add_test(NAME d3d12_triangle_test
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:d3d12_triangle_test>/run_integration_test.py
$<TARGET_FILE:d3d12_triangle_test>
triangle.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
0
WORKING_DIRECTORY $<TARGET_FILE_DIR:d3d12_triangle_test>
)

View File

@@ -1,20 +0,0 @@
struct Vertex {
float4 pos : POSITION;
float4 col : COLOR;
};
struct VSOut {
float4 pos : SV_POSITION;
float4 col : COLOR;
};
VSOut MainVS(Vertex v) {
VSOut o;
o.pos = v.pos;
o.col = v.col;
return o;
}
float4 MainPS(VSOut i) : SV_TARGET {
return i.col;
}

View File

@@ -1,378 +0,0 @@
#include <windows.h>
#include <d3d12.h>
#include <dxgi1_4.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdarg.h>
#include <string>
#include "XCEngine/RHI/RHIEnums.h"
#include "XCEngine/RHI/RHITypes.h"
#include "XCEngine/RHI/D3D12/D3D12Device.h"
#include "XCEngine/RHI/D3D12/D3D12CommandQueue.h"
#include "XCEngine/RHI/D3D12/D3D12CommandAllocator.h"
#include "XCEngine/RHI/D3D12/D3D12CommandList.h"
#include "XCEngine/RHI/D3D12/D3D12DescriptorHeap.h"
#include "XCEngine/RHI/D3D12/D3D12Fence.h"
#include "XCEngine/RHI/D3D12/D3D12SwapChain.h"
#include "XCEngine/RHI/D3D12/D3D12Buffer.h"
#include "XCEngine/RHI/D3D12/D3D12Texture.h"
#include "XCEngine/RHI/D3D12/D3D12ResourceView.h"
#include "XCEngine/RHI/D3D12/D3D12Shader.h"
#include "XCEngine/RHI/D3D12/D3D12RootSignature.h"
#include "XCEngine/RHI/D3D12/D3D12PipelineState.h"
#include "XCEngine/RHI/D3D12/D3D12Screenshot.h"
#include "XCEngine/Debug/Logger.h"
#include "XCEngine/Debug/ConsoleLogSink.h"
#include "XCEngine/Debug/FileLogSink.h"
#include "XCEngine/Debug/RenderDocCapture.h"
#include "XCEngine/Core/Containers/String.h"
using namespace XCEngine::RHI;
using namespace XCEngine::Debug;
using namespace XCEngine::Containers;
#pragma comment(lib,"d3d12.lib")
#pragma comment(lib,"dxgi.lib")
#pragma comment(lib,"dxguid.lib")
#pragma comment(lib,"d3dcompiler.lib")
#pragma comment(lib,"winmm.lib")
D3D12Device gDevice;
D3D12CommandQueue gCommandQueue;
D3D12SwapChain gSwapChain;
D3D12CommandAllocator gCommandAllocator;
D3D12CommandList gCommandList;
D3D12Texture gDepthStencil;
D3D12DescriptorHeap gRTVHeap;
D3D12DescriptorHeap gDSVHeap;
D3D12ResourceView gRTVs[2];
D3D12ResourceView gDSV;
D3D12Shader gVertexShader;
D3D12Shader gPixelShader;
D3D12RootSignature gRootSignature;
D3D12PipelineState gPipelineState;
D3D12Buffer gVertexBuffer;
UINT gRTVDescriptorSize = 0;
UINT gDSVDescriptorSize = 0;
int gCurrentRTIndex = 0;
HWND gHWND = nullptr;
int gWidth = 1280;
int gHeight = 720;
void Log(const char* format, ...) {
char buffer[1024];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
Logger::Get().Debug(LogCategory::Rendering, String(buffer));
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
bool InitializeUploadBuffer(D3D12Buffer& buffer, ID3D12Device* device, const void* data, uint64_t size, uint32_t stride, BufferType type) {
if (!buffer.Initialize(device, size, D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_HEAP_TYPE_UPLOAD)) {
return false;
}
buffer.SetData(data, static_cast<size_t>(size));
buffer.SetStride(stride);
buffer.SetBufferType(type);
return true;
}
bool InitD3D12() {
RHIDeviceDesc deviceDesc;
deviceDesc.adapterIndex = 0;
deviceDesc.enableDebugLayer = false;
deviceDesc.enableGPUValidation = false;
if (!gDevice.Initialize(deviceDesc)) {
Log("[ERROR] Failed to initialize D3D12 device");
return false;
}
ID3D12Device* device = gDevice.GetDevice();
IDXGIFactory4* factory = gDevice.GetFactory();
if (!gCommandQueue.Initialize(device, CommandQueueType::Direct)) {
Log("[ERROR] Failed to initialize command queue");
return false;
}
if (!gSwapChain.Initialize(factory, gCommandQueue.GetCommandQueue(), gHWND, gWidth, gHeight, 2)) {
Log("[ERROR] Failed to initialize swap chain");
return false;
}
gDepthStencil.InitializeDepthStencil(device, gWidth, gHeight);
gRTVHeap.Initialize(device, DescriptorHeapType::RTV, 2);
gRTVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::RTV);
gDSVHeap.Initialize(device, DescriptorHeapType::DSV, 1);
gDSVDescriptorSize = gDevice.GetDescriptorHandleIncrementSize(DescriptorHeapType::DSV);
for (int i = 0; i < 2; i++) {
D3D12Texture& backBuffer = gSwapChain.GetBackBuffer(i);
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = D3D12ResourceView::CreateRenderTargetDesc(Format::R8G8B8A8_UNorm, D3D12_RTV_DIMENSION_TEXTURE2D);
gRTVs[i].InitializeAsRenderTarget(device, backBuffer.GetResource(), &rtvDesc, &gRTVHeap, i);
}
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = D3D12ResourceView::CreateDepthStencilDesc(Format::D24_UNorm_S8_UInt, D3D12_DSV_DIMENSION_TEXTURE2D);
gDSV.InitializeAsDepthStencil(device, gDepthStencil.GetResource(), &dsvDesc, &gDSVHeap, 0);
gCommandAllocator.Initialize(device, CommandQueueType::Direct);
gCommandList.Initialize(device, CommandQueueType::Direct, gCommandAllocator.GetCommandAllocator());
if (!gVertexShader.CompileFromFile(L"Res/Shader/triangle.hlsl", "MainVS", "vs_5_1")) {
Log("[ERROR] Failed to compile vertex shader");
return false;
}
Log("[INFO] Vertex shader compiled, bytecode size: %zu", gVertexShader.GetBytecodeSize());
if (!gPixelShader.CompileFromFile(L"Res/Shader/triangle.hlsl", "MainPS", "ps_5_1")) {
Log("[ERROR] Failed to compile pixel shader");
return false;
}
Log("[INFO] Pixel shader compiled, bytecode size: %zu", gPixelShader.GetBytecodeSize());
D3D12_ROOT_SIGNATURE_DESC rsDesc = D3D12RootSignature::CreateDesc(nullptr, 0);
if (!gRootSignature.Initialize(device, rsDesc)) {
Log("[ERROR] Failed to initialize root signature");
return false;
}
D3D12_INPUT_ELEMENT_DESC inputElements[] = {
D3D12PipelineState::CreateInputElement("POSITION", 0, Format::R32G32B32A32_Float, 0, 0),
D3D12PipelineState::CreateInputElement("COLOR", 0, Format::R32G32B32A32_Float, 0, 16),
};
D3D12_SHADER_BYTECODE emptyGs = {};
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.pRootSignature = gRootSignature.GetRootSignature();
psoDesc.VS = gVertexShader.GetD3D12Bytecode();
psoDesc.PS = gPixelShader.GetD3D12Bytecode();
psoDesc.GS = emptyGs;
psoDesc.InputLayout.NumElements = 2;
psoDesc.InputLayout.pInputElementDescs = inputElements;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.DSVFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
psoDesc.SampleDesc.Count = 1;
psoDesc.SampleDesc.Quality = 0;
psoDesc.SampleMask = 0xffffffff;
psoDesc.NumRenderTargets = 1;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
psoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
psoDesc.RasterizerState.FrontCounterClockwise = FALSE;
psoDesc.RasterizerState.DepthClipEnable = TRUE;
psoDesc.DepthStencilState.DepthEnable = FALSE;
psoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
psoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
psoDesc.BlendState.RenderTarget[0].BlendEnable = FALSE;
psoDesc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_ONE;
psoDesc.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_ZERO;
psoDesc.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD;
psoDesc.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_ONE;
psoDesc.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ZERO;
psoDesc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD;
psoDesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
if (!gPipelineState.Initialize(device, psoDesc)) {
Log("[ERROR] Failed to initialize pipeline state");
return false;
}
struct Vertex {
float pos[4];
float col[4];
};
Vertex vertices[] = {
{ { 0.0f, 0.5f, 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
{ { -0.5f, -0.5f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
{ { 0.5f, -0.5f, 0.0f, 1.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } },
};
if (!InitializeUploadBuffer(gVertexBuffer, device, vertices, sizeof(vertices), sizeof(Vertex), BufferType::Vertex)) {
Log("[ERROR] Failed to initialize vertex buffer");
return false;
}
Log("[INFO] D3D12 initialized successfully");
return true;
}
void WaitForGPU() {
gCommandQueue.WaitForIdle();
}
void ExecuteCommandList() {
gCommandList.Close();
void* commandLists[] = { &gCommandList };
gCommandQueue.ExecuteCommandLists(1, commandLists);
}
void BeginRender() {
gCurrentRTIndex = gSwapChain.GetCurrentBackBufferIndex();
D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex);
gCommandList.TransitionBarrier(currentBackBuffer.GetResource(),
ResourceStates::Present, ResourceStates::RenderTarget);
CPUDescriptorHandle rtvCpuHandle = gRTVHeap.GetCPUDescriptorHandle(gCurrentRTIndex);
CPUDescriptorHandle dsvCpuHandle = gDSVHeap.GetCPUDescriptorHandle(0);
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr };
D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = { dsvCpuHandle.ptr };
gCommandList.SetRenderTargetsHandle(1, &rtvHandle, &dsvHandle);
Viewport viewport = { 0.0f, 0.0f, (float)gWidth, (float)gHeight, 0.0f, 1.0f };
Rect scissorRect = { 0, 0, gWidth, gHeight };
gCommandList.SetViewport(viewport);
gCommandList.SetScissorRect(scissorRect);
float clearColor[] = { 0.0f, 0.0f, 1.0f, 1.0f };
gCommandList.ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
gCommandList.ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
}
void EndRender() {
D3D12Texture& currentBackBuffer = gSwapChain.GetBackBuffer(gCurrentRTIndex);
gCommandList.TransitionBarrier(currentBackBuffer.GetResource(),
ResourceStates::RenderTarget, ResourceStates::Present);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
Logger::Get().Initialize();
Logger::Get().AddSink(std::make_unique<ConsoleLogSink>());
Logger::Get().SetMinimumLevel(LogLevel::Debug);
Log("[INFO] D3D12 Triangle Test Starting");
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"D3D12TriangleTest";
if (!RegisterClassEx(&wc)) {
MessageBox(NULL, L"Failed to register window class", L"Error", MB_OK);
return -1;
}
RECT rect = { 0, 0, gWidth, gHeight };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
gHWND = CreateWindowEx(0, L"D3D12TriangleTest", L"D3D12 Triangle Test",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
NULL, NULL, hInstance, NULL);
if (!gHWND) {
MessageBox(NULL, L"Failed to create window", L"Error", MB_OK);
return -1;
}
RenderDocCapture::Get().Initialize(nullptr, gHWND);
RenderDocCapture::Get().SetCaptureFilePath(".\\triangle_frame30");
if (!InitD3D12()) {
MessageBox(NULL, L"Failed to initialize D3D12", L"Error", MB_OK);
return -1;
}
RenderDocCapture::Get().SetDevice(gDevice.GetDevice());
ShowWindow(gHWND, nShowCmd);
UpdateWindow(gHWND);
MSG msg = {};
int frameCount = 0;
const int targetFrameCount = 30;
while (true) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
if (frameCount > 0) {
gCommandQueue.WaitForPreviousFrame();
}
gCommandAllocator.Reset();
gCommandList.Reset();
BeginRender();
gCommandList.SetPipelineState(gPipelineState.GetPipelineState());
gCommandList.SetRootSignature(gRootSignature.GetRootSignature());
gCommandList.SetPrimitiveTopology(PrimitiveTopology::TriangleList);
gCommandList.SetVertexBuffer(0, gVertexBuffer.GetResource(), 0, gVertexBuffer.GetStride());
gCommandList.Draw(3, 1, 0, 0);
frameCount++;
if (frameCount >= targetFrameCount) {
Log("[INFO] Reached target frame count %d - taking screenshot!", targetFrameCount);
ExecuteCommandList();
if (RenderDocCapture::Get().EndCapture()) {
Log("[INFO] RenderDoc capture ended");
}
WaitForGPU();
Log("[INFO] GPU idle, taking screenshot...");
bool screenshotResult = D3D12Screenshot::Capture(
gDevice,
gCommandQueue,
gSwapChain.GetBackBuffer(gCurrentRTIndex),
"triangle.ppm"
);
if (screenshotResult) {
Log("[INFO] Screenshot saved to triangle.ppm");
} else {
Log("[ERROR] Screenshot failed");
}
break;
}
if (frameCount == targetFrameCount - 1) {
if (RenderDocCapture::Get().BeginCapture("D3D12_Triangle_Test")) {
Log("[INFO] RenderDoc capture started");
}
}
EndRender();
ExecuteCommandList();
gSwapChain.Present(0, 0);
}
}
gCommandList.Shutdown();
gCommandAllocator.Shutdown();
gSwapChain.Shutdown();
gDevice.Shutdown();
RenderDocCapture::Get().Shutdown();
Logger::Get().Shutdown();
Log("[INFO] D3D12 Triangle Test Finished");
return 0;
}

View File

@@ -1,48 +0,0 @@
cmake_minimum_required(VERSION 3.15)
get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE)
find_package(GTest REQUIRED)
set(TEST_SOURCES
fixtures/D3D12TestFixture.cpp
test_device.cpp
test_fence.cpp
test_command_queue.cpp
test_command_allocator.cpp
test_command_list.cpp
test_buffer.cpp
test_texture.cpp
test_descriptor_heap.cpp
test_shader.cpp
test_root_signature.cpp
test_pipeline_state.cpp
test_views.cpp
test_swap_chain.cpp
test_backend_specific.cpp
)
add_executable(rhi_d3d12_tests ${TEST_SOURCES})
target_link_libraries(rhi_d3d12_tests PRIVATE
d3d12
dxgi
d3dcompiler
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(rhi_d3d12_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/fixtures
${PROJECT_ROOT_DIR}/engine/include
${PROJECT_ROOT_DIR}/engine/src
)
if(MSVC)
target_compile_options(rhi_d3d12_tests PRIVATE /FS)
endif()
enable_testing()
include(GoogleTest)
gtest_discover_tests(rhi_d3d12_tests)

View File

@@ -1,77 +0,0 @@
#include "D3D12TestFixture.h"
#include <memory>
namespace XCEngine {
namespace RHI {
void D3D12TestFixture::SetUpTestSuite() {
}
void D3D12TestFixture::TearDownTestSuite() {
}
void D3D12TestFixture::SetUp() {
m_device = std::make_unique<D3D12Device>();
RHIDeviceDesc desc;
desc.enableDebugLayer = false;
desc.enableGPUValidation = false;
if (!m_device->Initialize(desc)) {
GTEST_SKIP() << "Failed to initialize D3D12Device";
return;
}
m_commandQueue = std::make_unique<D3D12CommandQueue>();
if (!m_commandQueue->Initialize(m_device->GetDevice(), CommandQueueType::Direct)) {
GTEST_SKIP() << "Failed to create command queue";
return;
}
m_commandAllocator = std::make_unique<D3D12CommandAllocator>();
if (!m_commandAllocator->Initialize(m_device->GetDevice(), CommandQueueType::Direct)) {
GTEST_SKIP() << "Failed to create command allocator";
return;
}
m_commandList = std::make_unique<D3D12CommandList>();
if (!m_commandList->Initialize(m_device->GetDevice(), CommandQueueType::Direct, m_commandAllocator->GetCommandAllocator())) {
GTEST_SKIP() << "Failed to create command list";
return;
}
}
void D3D12TestFixture::TearDown() {
if (m_commandQueue) {
WaitForGPU();
}
if (m_commandList) {
m_commandList->Shutdown();
m_commandList.reset();
}
if (m_commandAllocator) {
m_commandAllocator->Shutdown();
m_commandAllocator.reset();
}
if (m_commandQueue) {
m_commandQueue->Shutdown();
m_commandQueue.reset();
}
if (m_device) {
m_device->Shutdown();
m_device.reset();
}
}
void D3D12TestFixture::WaitForGPU() {
if (!m_commandQueue) return;
auto fence = std::make_unique<D3D12Fence>();
if (fence->Initialize(m_device->GetDevice(), 0)) {
m_commandQueue->Signal(fence.get(), 1);
fence->Wait(1);
}
}
} // namespace RHI
} // namespace XCEngine

View File

@@ -1,38 +0,0 @@
#pragma once
#include <gtest/gtest.h>
#include <wrl/client.h>
#include "XCEngine/RHI/D3D12/D3D12Device.h"
#include "XCEngine/RHI/D3D12/D3D12CommandQueue.h"
#include "XCEngine/RHI/D3D12/D3D12CommandAllocator.h"
#include "XCEngine/RHI/D3D12/D3D12CommandList.h"
#include "XCEngine/RHI/D3D12/D3D12Fence.h"
namespace XCEngine {
namespace RHI {
class D3D12TestFixture : public ::testing::Test {
protected:
static void SetUpTestSuite();
static void TearDownTestSuite();
void SetUp() override;
void TearDown() override;
D3D12Device* GetDevice() { return m_device.get(); }
D3D12CommandQueue* GetCommandQueue() { return m_commandQueue.get(); }
D3D12CommandList* GetCommandList() { return m_commandList.get(); }
D3D12CommandAllocator* GetCommandAllocator() { return m_commandAllocator.get(); }
void WaitForGPU();
private:
std::unique_ptr<D3D12Device> m_device;
std::unique_ptr<D3D12CommandQueue> m_commandQueue;
std::unique_ptr<D3D12CommandAllocator> m_commandAllocator;
std::unique_ptr<D3D12CommandList> m_commandList;
};
} // namespace RHI
} // namespace XCEngine

View File

@@ -1,248 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12DescriptorSet.h"
#include "XCEngine/RHI/D3D12/D3D12PipelineLayout.h"
#include "XCEngine/RHI/RHIDescriptorPool.h"
#include "XCEngine/RHI/RHIEnums.h"
#include "XCEngine/RHI/RHIPipelineLayout.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, DescriptorSet_MixedBindings_AssignDescriptorIndicesByType) {
DescriptorPoolDesc poolDesc = {};
poolDesc.type = DescriptorHeapType::CBV_SRV_UAV;
poolDesc.descriptorCount = 4;
poolDesc.shaderVisible = true;
RHIDescriptorPool* pool = GetDevice()->CreateDescriptorPool(poolDesc);
ASSERT_NE(pool, nullptr);
DescriptorSetLayoutBinding bindings[3] = {};
bindings[0].binding = 2;
bindings[0].type = static_cast<uint32_t>(DescriptorType::UAV);
bindings[0].count = 1;
bindings[1].binding = 0;
bindings[1].type = static_cast<uint32_t>(DescriptorType::CBV);
bindings[1].count = 1;
bindings[2].binding = 1;
bindings[2].type = static_cast<uint32_t>(DescriptorType::SRV);
bindings[2].count = 1;
DescriptorSetLayoutDesc layoutDesc = {};
layoutDesc.bindings = bindings;
layoutDesc.bindingCount = 3;
RHIDescriptorSet* firstSet = pool->AllocateSet(layoutDesc);
RHIDescriptorSet* secondSet = pool->AllocateSet(layoutDesc);
ASSERT_NE(firstSet, nullptr);
ASSERT_NE(secondSet, nullptr);
auto* firstD3D12Set = static_cast<D3D12DescriptorSet*>(firstSet);
auto* secondD3D12Set = static_cast<D3D12DescriptorSet*>(secondSet);
EXPECT_EQ(firstD3D12Set->GetCount(), 2u);
EXPECT_EQ(firstD3D12Set->GetDescriptorIndexForBinding(1), 0u);
EXPECT_EQ(firstD3D12Set->GetDescriptorIndexForBinding(2), 1u);
EXPECT_EQ(firstD3D12Set->GetDescriptorIndexForBinding(0), UINT32_MAX);
EXPECT_EQ(firstD3D12Set->GetOffset(), 0u);
EXPECT_EQ(secondD3D12Set->GetOffset(), 2u);
firstSet->Shutdown();
delete firstSet;
secondSet->Shutdown();
delete secondSet;
pool->Shutdown();
delete pool;
}
TEST_F(D3D12TestFixture, DescriptorSet_MultipleConstantBuffersUploadIndependently) {
DescriptorPoolDesc poolDesc = {};
poolDesc.type = DescriptorHeapType::CBV_SRV_UAV;
poolDesc.descriptorCount = 1;
poolDesc.shaderVisible = false;
RHIDescriptorPool* pool = GetDevice()->CreateDescriptorPool(poolDesc);
ASSERT_NE(pool, nullptr);
DescriptorSetLayoutBinding bindings[2] = {};
bindings[0].binding = 0;
bindings[0].type = static_cast<uint32_t>(DescriptorType::CBV);
bindings[0].count = 1;
bindings[1].binding = 1;
bindings[1].type = static_cast<uint32_t>(DescriptorType::CBV);
bindings[1].count = 1;
DescriptorSetLayoutDesc layoutDesc = {};
layoutDesc.bindings = bindings;
layoutDesc.bindingCount = 2;
RHIDescriptorSet* set = pool->AllocateSet(layoutDesc);
ASSERT_NE(set, nullptr);
auto* d3d12Set = static_cast<D3D12DescriptorSet*>(set);
const float firstData[16] = { 1.0f, 0.0f, 0.0f, 0.0f };
const float secondData[16] = { 2.0f, 0.0f, 0.0f, 0.0f };
d3d12Set->WriteConstant(0, firstData, sizeof(firstData));
d3d12Set->WriteConstant(1, secondData, sizeof(secondData));
ASSERT_TRUE(d3d12Set->UploadConstantBuffer(0));
ASSERT_TRUE(d3d12Set->UploadConstantBuffer(1));
const D3D12_GPU_VIRTUAL_ADDRESS firstAddress = d3d12Set->GetConstantBufferGPUAddress(0);
const D3D12_GPU_VIRTUAL_ADDRESS secondAddress = d3d12Set->GetConstantBufferGPUAddress(1);
EXPECT_NE(firstAddress, 0u);
EXPECT_NE(secondAddress, 0u);
EXPECT_NE(firstAddress, secondAddress);
set->Shutdown();
delete set;
pool->Shutdown();
delete pool;
}
TEST_F(D3D12TestFixture, PipelineLayout_TracksDistinctBindingClasses) {
RHIPipelineLayoutDesc desc = {};
desc.constantBufferCount = 2;
desc.textureCount = 1;
desc.uavCount = 1;
desc.samplerCount = 1;
RHIPipelineLayout* layout = GetDevice()->CreatePipelineLayout(desc);
ASSERT_NE(layout, nullptr);
auto* d3d12Layout = static_cast<D3D12PipelineLayout*>(layout);
EXPECT_TRUE(d3d12Layout->HasConstantBufferBinding(0));
EXPECT_TRUE(d3d12Layout->HasConstantBufferBinding(1));
EXPECT_FALSE(d3d12Layout->HasConstantBufferBinding(2));
EXPECT_TRUE(d3d12Layout->HasShaderResourceTable());
EXPECT_TRUE(d3d12Layout->HasUnorderedAccessTable());
EXPECT_TRUE(d3d12Layout->HasSamplerTable());
EXPECT_NE(
d3d12Layout->GetConstantBufferRootParameterIndex(0),
d3d12Layout->GetConstantBufferRootParameterIndex(1));
EXPECT_NE(
d3d12Layout->GetShaderResourceTableRootParameterIndex(),
d3d12Layout->GetUnorderedAccessTableRootParameterIndex());
EXPECT_NE(
d3d12Layout->GetUnorderedAccessTableRootParameterIndex(),
d3d12Layout->GetSamplerTableRootParameterIndex());
layout->Shutdown();
delete layout;
}
TEST_F(D3D12TestFixture, PipelineLayout_InfersBindingClassesFromSetLayouts) {
DescriptorSetLayoutBinding set0Bindings[1] = {};
set0Bindings[0].binding = 0;
set0Bindings[0].type = static_cast<uint32_t>(DescriptorType::CBV);
set0Bindings[0].count = 1;
DescriptorSetLayoutBinding set1Bindings[1] = {};
set1Bindings[0].binding = 0;
set1Bindings[0].type = static_cast<uint32_t>(DescriptorType::SRV);
set1Bindings[0].count = 1;
DescriptorSetLayoutBinding set2Bindings[1] = {};
set2Bindings[0].binding = 0;
set2Bindings[0].type = static_cast<uint32_t>(DescriptorType::UAV);
set2Bindings[0].count = 1;
DescriptorSetLayoutBinding set3Bindings[1] = {};
set3Bindings[0].binding = 0;
set3Bindings[0].type = static_cast<uint32_t>(DescriptorType::Sampler);
set3Bindings[0].count = 1;
DescriptorSetLayoutDesc setLayouts[4] = {};
setLayouts[0].bindings = set0Bindings;
setLayouts[0].bindingCount = 1;
setLayouts[1].bindings = set1Bindings;
setLayouts[1].bindingCount = 1;
setLayouts[2].bindings = set2Bindings;
setLayouts[2].bindingCount = 1;
setLayouts[3].bindings = set3Bindings;
setLayouts[3].bindingCount = 1;
RHIPipelineLayoutDesc desc = {};
desc.setLayouts = setLayouts;
desc.setLayoutCount = 4;
RHIPipelineLayout* layout = GetDevice()->CreatePipelineLayout(desc);
ASSERT_NE(layout, nullptr);
auto* d3d12Layout = static_cast<D3D12PipelineLayout*>(layout);
EXPECT_TRUE(d3d12Layout->UsesSetLayouts());
EXPECT_EQ(d3d12Layout->GetSetLayoutCount(), 4u);
EXPECT_TRUE(d3d12Layout->HasConstantBufferBinding(0, 0));
EXPECT_TRUE(d3d12Layout->HasShaderResourceTable(1));
EXPECT_TRUE(d3d12Layout->HasUnorderedAccessTable(2));
EXPECT_TRUE(d3d12Layout->HasSamplerTable(3));
EXPECT_EQ(d3d12Layout->GetDesc().constantBufferCount, 1u);
EXPECT_EQ(d3d12Layout->GetDesc().textureCount, 1u);
EXPECT_EQ(d3d12Layout->GetDesc().uavCount, 1u);
EXPECT_EQ(d3d12Layout->GetDesc().samplerCount, 1u);
layout->Shutdown();
delete layout;
}
TEST_F(D3D12TestFixture, PipelineLayout_SeparatesOverlappingBindingsAcrossSetSlots) {
DescriptorSetLayoutBinding set0Bindings[2] = {};
set0Bindings[0].binding = 0;
set0Bindings[0].type = static_cast<uint32_t>(DescriptorType::CBV);
set0Bindings[0].count = 1;
set0Bindings[1].binding = 0;
set0Bindings[1].type = static_cast<uint32_t>(DescriptorType::SRV);
set0Bindings[1].count = 1;
DescriptorSetLayoutBinding set1Bindings[2] = {};
set1Bindings[0].binding = 0;
set1Bindings[0].type = static_cast<uint32_t>(DescriptorType::CBV);
set1Bindings[0].count = 1;
set1Bindings[1].binding = 0;
set1Bindings[1].type = static_cast<uint32_t>(DescriptorType::SRV);
set1Bindings[1].count = 1;
DescriptorSetLayoutBinding set2Bindings[1] = {};
set2Bindings[0].binding = 0;
set2Bindings[0].type = static_cast<uint32_t>(DescriptorType::Sampler);
set2Bindings[0].count = 1;
DescriptorSetLayoutDesc setLayouts[3] = {};
setLayouts[0].bindings = set0Bindings;
setLayouts[0].bindingCount = 2;
setLayouts[1].bindings = set1Bindings;
setLayouts[1].bindingCount = 2;
setLayouts[2].bindings = set2Bindings;
setLayouts[2].bindingCount = 1;
RHIPipelineLayoutDesc desc = {};
desc.setLayouts = setLayouts;
desc.setLayoutCount = 3;
RHIPipelineLayout* layout = GetDevice()->CreatePipelineLayout(desc);
ASSERT_NE(layout, nullptr);
auto* d3d12Layout = static_cast<D3D12PipelineLayout*>(layout);
EXPECT_TRUE(d3d12Layout->UsesSetLayouts());
EXPECT_TRUE(d3d12Layout->HasConstantBufferBinding(0, 0));
EXPECT_TRUE(d3d12Layout->HasConstantBufferBinding(1, 0));
EXPECT_FALSE(d3d12Layout->HasConstantBufferBinding(2, 0));
EXPECT_NE(
d3d12Layout->GetConstantBufferRootParameterIndex(0, 0),
d3d12Layout->GetConstantBufferRootParameterIndex(1, 0));
EXPECT_TRUE(d3d12Layout->HasShaderResourceTable(0));
EXPECT_TRUE(d3d12Layout->HasShaderResourceTable(1));
EXPECT_FALSE(d3d12Layout->HasShaderResourceTable(2));
EXPECT_NE(
d3d12Layout->GetShaderResourceTableRootParameterIndex(0),
d3d12Layout->GetShaderResourceTableRootParameterIndex(1));
EXPECT_FALSE(d3d12Layout->HasSamplerTable(0));
EXPECT_FALSE(d3d12Layout->HasSamplerTable(1));
EXPECT_TRUE(d3d12Layout->HasSamplerTable(2));
layout->Shutdown();
delete layout;
}

View File

@@ -1,78 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12Buffer.h"
#include "XCEngine/RHI/RHIEnums.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, Buffer_Create_DefaultHeap) {
BufferDesc desc = {};
desc.size = 1024;
desc.stride = 0;
desc.bufferType = static_cast<uint32_t>(BufferType::Vertex);
desc.flags = 0;
RHIBuffer* buffer = GetDevice()->CreateBuffer(desc);
ASSERT_NE(buffer, nullptr);
auto* d3d12Buffer = static_cast<D3D12Buffer*>(buffer);
EXPECT_EQ(d3d12Buffer->GetSize(), 1024);
buffer->Shutdown();
delete buffer;
}
TEST_F(D3D12TestFixture, Buffer_Create_UploadHeap) {
BufferDesc desc = {};
desc.size = 2048;
desc.stride = 0;
desc.bufferType = static_cast<uint32_t>(BufferType::Constant);
desc.flags = 0;
RHIBuffer* buffer = GetDevice()->CreateBuffer(desc);
ASSERT_NE(buffer, nullptr);
buffer->Shutdown();
delete buffer;
}
TEST_F(D3D12TestFixture, Buffer_Get_GPUVirtualAddress) {
BufferDesc desc = {};
desc.size = 512;
desc.stride = 0;
desc.bufferType = static_cast<uint32_t>(BufferType::Vertex);
desc.flags = 0;
RHIBuffer* buffer = GetDevice()->CreateBuffer(desc);
ASSERT_NE(buffer, nullptr);
auto* d3d12Buffer = static_cast<D3D12Buffer*>(buffer);
EXPECT_NE(d3d12Buffer->GetGPUAddress(), 0);
buffer->Shutdown();
delete buffer;
}
TEST_F(D3D12TestFixture, Buffer_Map_Unmap) {
BufferDesc desc = {};
desc.size = 256;
desc.stride = 0;
desc.bufferType = static_cast<uint32_t>(BufferType::Constant);
desc.flags = 0;
RHIBuffer* buffer = GetDevice()->CreateBuffer(desc);
ASSERT_NE(buffer, nullptr);
void* mappedData = buffer->Map();
ASSERT_NE(mappedData, nullptr);
memset(mappedData, 0xAB, 256);
buffer->Unmap();
buffer->Shutdown();
delete buffer;
}
TEST_F(D3D12TestFixture, Buffer_Get_AlignmentRequirements) {
EXPECT_GE(256 % 256, 0);
}

View File

@@ -1,35 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12CommandAllocator.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, CommandAllocator_Reset_Basic) {
auto allocator = std::make_unique<D3D12CommandAllocator>();
ASSERT_TRUE(allocator->Initialize(GetDevice()->GetDevice(), CommandQueueType::Direct));
allocator->Reset();
EXPECT_TRUE(allocator->IsReady());
}
TEST_F(D3D12TestFixture, CommandAllocator_Reset_Multiple) {
auto allocator = std::make_unique<D3D12CommandAllocator>();
ASSERT_TRUE(allocator->Initialize(GetDevice()->GetDevice(), CommandQueueType::Direct));
for (int i = 0; i < 10; ++i) {
allocator->Reset();
EXPECT_TRUE(allocator->IsReady());
}
}
TEST_F(D3D12TestFixture, CommandAllocator_Create_DifferentTypes) {
CommandQueueType types[] = {
CommandQueueType::Direct,
CommandQueueType::Compute,
CommandQueueType::Copy
};
for (auto type : types) {
auto allocator = std::make_unique<D3D12CommandAllocator>();
ASSERT_TRUE(allocator->Initialize(GetDevice()->GetDevice(), type));
}
}

View File

@@ -1,26 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12Enums.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, CommandList_Close_Basic) {
GetCommandList()->Reset();
GetCommandList()->Close();
}
TEST_F(D3D12TestFixture, CommandList_Get_Desc) {
auto type = GetCommandList()->GetCommandList()->GetType();
EXPECT_EQ(type, D3D12_COMMAND_LIST_TYPE_DIRECT);
}
TEST_F(D3D12TestFixture, CommandList_Reset_AfterExecute_ReopensCommandList) {
GetCommandList()->Close();
void* commandLists[] = { GetCommandList() };
GetCommandQueue()->ExecuteCommandLists(1, commandLists);
GetCommandQueue()->WaitForPreviousFrame();
GetCommandList()->Reset();
EXPECT_TRUE(SUCCEEDED(GetCommandList()->GetCommandList()->Close()));
}

View File

@@ -1,18 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12CommandQueue.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, CommandQueue_Get_TimestampFrequency) {
ASSERT_NE(GetCommandQueue(), nullptr);
UINT64 frequency = GetCommandQueue()->GetTimestampFrequency();
EXPECT_GT(frequency, 0);
}
TEST_F(D3D12TestFixture, CommandQueue_Execute_EmptyCommandLists) {
GetCommandList()->Reset();
void* commandLists[] = { GetCommandList() };
GetCommandQueue()->ExecuteCommandLists(1, commandLists);
GetCommandList()->Close();
WaitForGPU();
}

View File

@@ -1,101 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12DescriptorHeap.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, DescriptorHeap_Wrapper_Initialize_RTV) {
D3D12DescriptorHeap heap;
bool result = heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::RTV, 4);
ASSERT_TRUE(result);
EXPECT_EQ(heap.GetDescriptorCount(), 4u);
EXPECT_EQ(heap.GetType(), DescriptorHeapType::RTV);
heap.Shutdown();
}
TEST_F(D3D12TestFixture, DescriptorHeap_Wrapper_GetCPUDescriptorHandle) {
D3D12DescriptorHeap heap;
ASSERT_TRUE(heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::RTV, 4));
uint32_t descriptorSize = heap.GetDescriptorSize();
CPUDescriptorHandle handle0 = heap.GetCPUDescriptorHandle(0);
CPUDescriptorHandle handle1 = heap.GetCPUDescriptorHandle(1);
CPUDescriptorHandle handle2 = heap.GetCPUDescriptorHandle(2);
EXPECT_EQ(handle1.ptr - handle0.ptr, descriptorSize);
EXPECT_EQ(handle2.ptr - handle1.ptr, descriptorSize);
heap.Shutdown();
}
TEST_F(D3D12TestFixture, DescriptorHeap_Wrapper_GetGPUDescriptorHandle) {
D3D12DescriptorHeap heap;
ASSERT_TRUE(heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::CBV_SRV_UAV, 4, true));
uint32_t descriptorSize = heap.GetDescriptorSize();
GPUDescriptorHandle handle0 = heap.GetGPUDescriptorHandle(0);
GPUDescriptorHandle handle1 = heap.GetGPUDescriptorHandle(1);
EXPECT_EQ(handle1.ptr - handle0.ptr, descriptorSize);
heap.Shutdown();
}
TEST_F(D3D12TestFixture, DescriptorHeap_Wrapper_GetDescriptorSize) {
D3D12DescriptorHeap rtvHeap;
ASSERT_TRUE(rtvHeap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::RTV, 4));
EXPECT_GT(rtvHeap.GetDescriptorSize(), 0u);
D3D12DescriptorHeap cbvHeap;
ASSERT_TRUE(cbvHeap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::CBV_SRV_UAV, 4));
EXPECT_GT(cbvHeap.GetDescriptorSize(), 0u);
}
TEST_F(D3D12TestFixture, DescriptorHeap_Wrapper_Shutdown) {
D3D12DescriptorHeap heap;
ASSERT_TRUE(heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::RTV, 2));
heap.Shutdown();
}
TEST_F(D3D12TestFixture, DescriptorHeap_Create_CBV_SRV_UAV) {
D3D12DescriptorHeap heap;
bool result = heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::CBV_SRV_UAV, 10, true);
ASSERT_TRUE(result);
EXPECT_EQ(heap.GetDescriptorCount(), 10u);
heap.Shutdown();
}
TEST_F(D3D12TestFixture, DescriptorHeap_Create_Sampler) {
D3D12DescriptorHeap heap;
bool result = heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::Sampler, 5, true);
ASSERT_TRUE(result);
EXPECT_EQ(heap.GetType(), DescriptorHeapType::Sampler);
heap.Shutdown();
}
TEST_F(D3D12TestFixture, DescriptorHeap_Create_RTV) {
D3D12DescriptorHeap heap;
bool result = heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::RTV, 4, false);
ASSERT_TRUE(result);
EXPECT_EQ(heap.GetType(), DescriptorHeapType::RTV);
heap.Shutdown();
}
TEST_F(D3D12TestFixture, DescriptorHeap_Create_DSV) {
D3D12DescriptorHeap heap;
bool result = heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::DSV, 2, false);
ASSERT_TRUE(result);
EXPECT_EQ(heap.GetType(), DescriptorHeapType::DSV);
heap.Shutdown();
}
TEST_F(D3D12TestFixture, DescriptorHeap_Get_HandleIncrementSize) {
UINT cbvSrvUavSize = GetDevice()->GetDescriptorHandleIncrementSize(DescriptorHeapType::CBV_SRV_UAV);
UINT samplerSize = GetDevice()->GetDescriptorHandleIncrementSize(DescriptorHeapType::Sampler);
UINT rtvSize = GetDevice()->GetDescriptorHandleIncrementSize(DescriptorHeapType::RTV);
UINT dsvSize = GetDevice()->GetDescriptorHandleIncrementSize(DescriptorHeapType::DSV);
EXPECT_GT(cbvSrvUavSize, 0);
EXPECT_GT(samplerSize, 0);
EXPECT_GT(rtvSize, 0);
EXPECT_GT(dsvSize, 0);
}

View File

@@ -1,64 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/RHIEnums.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, Device_Create_Success) {
ASSERT_NE(GetDevice(), nullptr);
ASSERT_NE(GetDevice()->GetDevice(), nullptr);
}
TEST_F(D3D12TestFixture, Device_Get_CommandQueue) {
ASSERT_NE(GetCommandQueue(), nullptr);
ASSERT_NE(GetCommandQueue()->GetCommandQueue(), nullptr);
}
TEST_F(D3D12TestFixture, Device_Get_FeatureLevel) {
D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevels = {};
static const D3D_FEATURE_LEVEL requestedLevels[] = { D3D_FEATURE_LEVEL_12_0 };
featureLevels.NumFeatureLevels = 1;
featureLevels.pFeatureLevelsRequested = requestedLevels;
bool result = GetDevice()->CheckFeatureSupport(D3D12_FEATURE_FEATURE_LEVELS, &featureLevels, sizeof(featureLevels));
ASSERT_TRUE(result);
EXPECT_EQ(featureLevels.MaxSupportedFeatureLevel, D3D_FEATURE_LEVEL_12_0);
}
TEST_F(D3D12TestFixture, Device_Get_DescriptorHandleIncrementSize) {
UINT cbvSrvUavSize = GetDevice()->GetDescriptorHandleIncrementSize(DescriptorHeapType::CBV_SRV_UAV);
EXPECT_GT(cbvSrvUavSize, 0);
UINT samplerSize = GetDevice()->GetDescriptorHandleIncrementSize(DescriptorHeapType::Sampler);
EXPECT_GT(samplerSize, 0);
UINT rtvSize = GetDevice()->GetDescriptorHandleIncrementSize(DescriptorHeapType::RTV);
EXPECT_GT(rtvSize, 0);
UINT dsvSize = GetDevice()->GetDescriptorHandleIncrementSize(DescriptorHeapType::DSV);
EXPECT_GT(dsvSize, 0);
}
TEST_F(D3D12TestFixture, Device_Get_ShaderModelSupport) {
D3D12_FEATURE_DATA_SHADER_MODEL shaderModel = {};
shaderModel.HighestShaderModel = D3D_SHADER_MODEL_6_0;
bool result = GetDevice()->CheckFeatureSupport(D3D12_FEATURE_SHADER_MODEL, &shaderModel, sizeof(shaderModel));
ASSERT_TRUE(result);
EXPECT_GE(shaderModel.HighestShaderModel, D3D_SHADER_MODEL_6_0);
}
TEST_F(D3D12TestFixture, Device_Get_ResourceBindingTier) {
D3D12_FEATURE_DATA_D3D12_OPTIONS options = {};
bool result = GetDevice()->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS, &options, sizeof(options));
ASSERT_TRUE(result);
EXPECT_GE(options.ResourceBindingTier, D3D12_RESOURCE_BINDING_TIER_1);
}
TEST_F(D3D12TestFixture, Device_Get_TiledResourcesTier) {
D3D12_FEATURE_DATA_D3D12_OPTIONS options = {};
bool result = GetDevice()->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS, &options, sizeof(options));
ASSERT_TRUE(result);
EXPECT_GE(options.TiledResourcesTier, D3D12_TILED_RESOURCES_TIER_NOT_SUPPORTED);
}

View File

@@ -1,181 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12Fence.h"
#include <Windows.h>
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, Fence_Create_Success) {
ASSERT_NE(GetDevice(), nullptr);
FenceDesc desc = {};
desc.initialValue = 0;
desc.flags = 0;
RHIFence* fence = GetDevice()->CreateFence(desc);
ASSERT_NE(fence, nullptr);
fence->Shutdown();
delete fence;
}
TEST_F(D3D12TestFixture, Fence_Get_CompletedValue_Initial) {
FenceDesc desc = {};
desc.initialValue = 0;
desc.flags = 0;
RHIFence* fence = GetDevice()->CreateFence(desc);
ASSERT_NE(fence, nullptr);
EXPECT_EQ(fence->GetCompletedValue(), 0);
fence->Shutdown();
delete fence;
}
TEST_F(D3D12TestFixture, Fence_Signal_Wait) {
FenceDesc desc = {};
desc.initialValue = 0;
desc.flags = 0;
RHIFence* fence = GetDevice()->CreateFence(desc);
ASSERT_NE(fence, nullptr);
const UINT64 fenceValue = 100;
GetCommandQueue()->Signal(fence, fenceValue);
fence->Wait(fenceValue);
EXPECT_EQ(fence->GetCompletedValue(), fenceValue);
fence->Shutdown();
delete fence;
}
TEST_F(D3D12TestFixture, Fence_Set_EventOnCompletion) {
FenceDesc desc = {};
desc.initialValue = 0;
desc.flags = 0;
auto* fence = new D3D12Fence();
ASSERT_TRUE(fence->Initialize(GetDevice()->GetDevice(), 200));
const UINT64 fenceValue = 200;
GetCommandQueue()->Signal(fence, fenceValue);
HANDLE eventHandle = CreateEvent(nullptr, FALSE, FALSE, nullptr);
ASSERT_NE(eventHandle, nullptr);
fence->GetFence()->SetEventOnCompletion(fenceValue, eventHandle);
DWORD waitResult = WaitForSingleObject(eventHandle, 1000);
EXPECT_EQ(waitResult, WAIT_OBJECT_0);
EXPECT_EQ(fence->GetCompletedValue(), fenceValue);
CloseHandle(eventHandle);
fence->Shutdown();
delete fence;
}
TEST_F(D3D12TestFixture, Fence_Signal_Multiple) {
FenceDesc desc = {};
desc.initialValue = 0;
desc.flags = 0;
RHIFence* fence = GetDevice()->CreateFence(desc);
ASSERT_NE(fence, nullptr);
const UINT64 value1 = 1;
const UINT64 value2 = 2;
const UINT64 value3 = 3;
GetCommandQueue()->Signal(fence, value1);
GetCommandQueue()->Signal(fence, value2);
GetCommandQueue()->Signal(fence, value3);
auto* d3d12fence = static_cast<D3D12Fence*>(fence);
HANDLE eventHandle = CreateEvent(nullptr, FALSE, FALSE, nullptr);
ASSERT_NE(eventHandle, nullptr);
d3d12fence->GetFence()->SetEventOnCompletion(value3, eventHandle);
DWORD waitResult = WaitForSingleObject(eventHandle, 1000);
EXPECT_EQ(waitResult, WAIT_OBJECT_0);
EXPECT_EQ(fence->GetCompletedValue(), value3);
CloseHandle(eventHandle);
fence->Shutdown();
delete fence;
}
TEST_F(D3D12TestFixture, Fence_Timeline_SignalIncrement) {
auto* fence = new D3D12Fence();
ASSERT_TRUE(fence->Initialize(GetDevice()->GetDevice(), 0));
fence->Signal(1);
fence->Wait(1);
EXPECT_GE(fence->GetCompletedValue(), 1u);
fence->Signal(5);
fence->Wait(5);
EXPECT_GE(fence->GetCompletedValue(), 5u);
fence->Signal(10);
fence->Wait(10);
EXPECT_GE(fence->GetCompletedValue(), 10u);
fence->Shutdown();
delete fence;
}
TEST_F(D3D12TestFixture, Fence_Timeline_SignalDecrement) {
auto* fence = new D3D12Fence();
ASSERT_TRUE(fence->Initialize(GetDevice()->GetDevice(), 0));
fence->Signal(5);
fence->Wait(5);
EXPECT_GE(fence->GetCompletedValue(), 5u);
fence->Signal(3);
fence->Wait(3);
EXPECT_GE(fence->GetCompletedValue(), 3u);
fence->Shutdown();
delete fence;
}
TEST_F(D3D12TestFixture, Fence_Timeline_MultipleSignals) {
auto* fence = new D3D12Fence();
ASSERT_TRUE(fence->Initialize(GetDevice()->GetDevice(), 0));
fence->Signal(10);
fence->Wait(10);
EXPECT_GE(fence->GetCompletedValue(), 10u);
fence->Signal(20);
fence->Wait(20);
EXPECT_GE(fence->GetCompletedValue(), 20u);
fence->Signal(30);
fence->Wait(30);
EXPECT_GE(fence->GetCompletedValue(), 30u);
fence->Shutdown();
delete fence;
}
TEST_F(D3D12TestFixture, Fence_Timeline_WaitSmallerThanCompleted) {
auto* fence = new D3D12Fence();
ASSERT_TRUE(fence->Initialize(GetDevice()->GetDevice(), 0));
fence->Signal(5);
fence->Wait(5);
fence->Wait(3);
EXPECT_GE(fence->GetCompletedValue(), 5u);
fence->Shutdown();
delete fence;
}

View File

@@ -1,41 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include <cstring>
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, PipelineState_Get_GraphicsPipelineDescDefaults) {
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = {};
psoDesc.pRootSignature = nullptr;
psoDesc.VS = {};
psoDesc.PS = {};
psoDesc.DS = {};
psoDesc.HS = {};
psoDesc.GS = {};
psoDesc.StreamOutput = {};
psoDesc.BlendState = {};
psoDesc.SampleMask = UINT_MAX;
psoDesc.RasterizerState = {};
psoDesc.DepthStencilState = {};
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.DSVFormat = DXGI_FORMAT_D32_FLOAT;
psoDesc.SampleDesc.Count = 1;
psoDesc.SampleDesc.Quality = 0;
EXPECT_EQ(psoDesc.PrimitiveTopologyType, D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE);
EXPECT_EQ(psoDesc.NumRenderTargets, 1);
}
TEST_F(D3D12TestFixture, PipelineState_Get_ComputePipelineDescDefaults) {
D3D12_COMPUTE_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.pRootSignature = nullptr;
psoDesc.CS = {};
psoDesc.NodeMask = 0;
psoDesc.CachedPSO = {};
psoDesc.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;
EXPECT_EQ(psoDesc.NodeMask, 0);
EXPECT_EQ(psoDesc.Flags, D3D12_PIPELINE_STATE_FLAG_NONE);
}

View File

@@ -1,30 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12RootSignature.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, RootSignature_Create_EmptyRootSignature) {
D3D12_ROOT_SIGNATURE_DESC rootSigDesc = {};
rootSigDesc.NumParameters = 0;
rootSigDesc.NumStaticSamplers = 0;
rootSigDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE;
D3D12RootSignature rootSig;
bool result = rootSig.Initialize(GetDevice()->GetDevice(), rootSigDesc);
ASSERT_TRUE(result);
}
TEST_F(D3D12TestFixture, RootSignature_Create_WithCBV) {
D3D12_ROOT_PARAMETER param = D3D12RootSignature::CreateCBV(0);
D3D12_ROOT_SIGNATURE_DESC rootSigDesc = {};
rootSigDesc.NumParameters = 1;
rootSigDesc.pParameters = &param;
rootSigDesc.NumStaticSamplers = 0;
rootSigDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE;
D3D12RootSignature rootSig;
bool result = rootSig.Initialize(GetDevice()->GetDevice(), rootSigDesc);
ASSERT_TRUE(result);
ASSERT_NE(rootSig.GetRootSignature(), nullptr);
}

View File

@@ -1,18 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, Shader_Get_VertexShaderProfile) {
const char* profile = "vs_6_0";
EXPECT_STREQ(profile, "vs_6_0");
}
TEST_F(D3D12TestFixture, Shader_Get_PixelShaderProfile) {
const char* profile = "ps_6_0";
EXPECT_STREQ(profile, "ps_6_0");
}
TEST_F(D3D12TestFixture, Shader_Get_ComputeShaderProfile) {
const char* profile = "cs_6_0";
EXPECT_STREQ(profile, "cs_6_0");
}

View File

@@ -1,213 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12SwapChain.h"
#include <windows.h>
using namespace XCEngine::RHI;
class SwapChainTestFixture : public ::testing::Test {
protected:
void SetUp() override {
HRESULT hr = D3D12CreateDevice(
nullptr,
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&mDevice)
);
if (FAILED(hr)) {
GTEST_SKIP() << "Failed to create D3D12 device";
return;
}
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
hr = mDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue));
if (FAILED(hr)) {
GTEST_SKIP() << "Failed to create command queue";
return;
}
hr = CreateDXGIFactory1(IID_PPV_ARGS(&mFactory));
if (FAILED(hr)) {
GTEST_SKIP() << "Failed to create DXGI factory";
return;
}
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpfnWndProc = DefWindowProc;
wc.hInstance = GetModuleHandle(nullptr);
wc.lpszClassName = "SwapChainTest";
RegisterClassEx(&wc);
mHWND = CreateWindowEx(
0,
"SwapChainTest",
"SwapChain Test",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
800,
600,
NULL,
NULL,
wc.hInstance,
NULL
);
if (!mHWND) {
GTEST_SKIP() << "Failed to create window";
return;
}
}
void TearDown() override {
if (mCommandQueue) {
WaitForGPU();
}
if (mHWND) {
DestroyWindow(mHWND);
}
mSwapChain.Shutdown();
mCommandQueue.Reset();
mDevice.Reset();
mFactory.Reset();
}
void WaitForGPU() {
if (!mCommandQueue || !mDevice) return;
ComPtr<ID3D12Fence> fence;
UINT64 fenceValue = 1;
HRESULT hr = mDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence));
if (SUCCEEDED(hr)) {
mCommandQueue->Signal(fence.Get(), fenceValue);
if (fence->GetCompletedValue() < fenceValue) {
HANDLE eventHandle = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (eventHandle) {
fence->SetEventOnCompletion(fenceValue, eventHandle);
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}
}
}
Microsoft::WRL::ComPtr<ID3D12Device> mDevice;
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
Microsoft::WRL::ComPtr<IDXGIFactory4> mFactory;
HWND mHWND = nullptr;
D3D12SwapChain mSwapChain;
};
TEST_F(SwapChainTestFixture, SwapChain_Initialize_FromFactory) {
bool result = mSwapChain.Initialize(
mFactory.Get(),
mCommandQueue.Get(),
mHWND,
800,
600,
2
);
ASSERT_TRUE(result);
}
TEST_F(SwapChainTestFixture, SwapChain_GetBackBuffer_ValidIndex) {
ASSERT_TRUE(mSwapChain.Initialize(
mFactory.Get(),
mCommandQueue.Get(),
mHWND,
800,
600,
2
));
D3D12Texture& backBuffer0 = mSwapChain.GetBackBuffer(0);
ID3D12Resource* resource0 = backBuffer0.GetResource();
ASSERT_NE(resource0, nullptr);
D3D12Texture& backBuffer1 = mSwapChain.GetBackBuffer(1);
ID3D12Resource* resource1 = backBuffer1.GetResource();
ASSERT_NE(resource1, nullptr);
ASSERT_NE(resource0, resource1);
}
TEST_F(SwapChainTestFixture, DISABLED_SwapChain_GetBackBuffer_InvalidIndex) {
ASSERT_TRUE(mSwapChain.Initialize(
mFactory.Get(),
mCommandQueue.Get(),
mHWND,
800,
600,
2
));
ASSERT_DEATH(mSwapChain.GetBackBuffer(99), "BackBuffer index out of range");
}
TEST_F(SwapChainTestFixture, SwapChain_GetCurrentBackBufferIndex) {
ASSERT_TRUE(mSwapChain.Initialize(
mFactory.Get(),
mCommandQueue.Get(),
mHWND,
800,
600,
2
));
uint32_t index = mSwapChain.GetCurrentBackBufferIndex();
EXPECT_LT(index, 2u);
}
TEST_F(SwapChainTestFixture, SwapChain_Present_DoesNotCrash) {
ASSERT_TRUE(mSwapChain.Initialize(
mFactory.Get(),
mCommandQueue.Get(),
mHWND,
800,
600,
2
));
ASSERT_NO_FATAL_FAILURE(mSwapChain.Present(0, 0));
}
TEST_F(SwapChainTestFixture, SwapChain_Resize_RecreatesBackBuffers) {
ASSERT_TRUE(mSwapChain.Initialize(
mFactory.Get(),
mCommandQueue.Get(),
mHWND,
800,
600,
2
));
ID3D12Resource* beforeResize = mSwapChain.GetBackBuffer(0).GetResource();
ASSERT_NE(beforeResize, nullptr);
EXPECT_EQ(mSwapChain.GetBackBuffer(0).GetWidth(), 800u);
EXPECT_EQ(mSwapChain.GetBackBuffer(0).GetHeight(), 600u);
ASSERT_NO_FATAL_FAILURE(mSwapChain.Resize(1024, 768));
ID3D12Resource* afterResize = mSwapChain.GetBackBuffer(0).GetResource();
ASSERT_NE(afterResize, nullptr);
EXPECT_EQ(mSwapChain.GetBackBuffer(0).GetWidth(), 1024u);
EXPECT_EQ(mSwapChain.GetBackBuffer(0).GetHeight(), 768u);
EXPECT_NE(beforeResize, afterResize);
}
TEST_F(SwapChainTestFixture, SwapChain_Shutdown_Cleanup) {
ASSERT_TRUE(mSwapChain.Initialize(
mFactory.Get(),
mCommandQueue.Get(),
mHWND,
800,
600,
2
));
ASSERT_NO_FATAL_FAILURE(mSwapChain.Shutdown());
}

View File

@@ -1,93 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/RHIEnums.h"
#include "XCEngine/RHI/RHITexture.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, Texture_Create_2D) {
TextureDesc desc = {};
desc.width = 256;
desc.height = 256;
desc.depth = 1;
desc.mipLevels = 1;
desc.arraySize = 1;
desc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
desc.textureType = static_cast<uint32_t>(TextureType::Texture2D);
desc.sampleCount = 1;
desc.sampleQuality = 0;
desc.flags = 0;
RHITexture* texture = GetDevice()->CreateTexture(desc);
ASSERT_NE(texture, nullptr);
EXPECT_EQ(texture->GetWidth(), 256);
EXPECT_EQ(texture->GetHeight(), 256);
EXPECT_EQ(texture->GetFormat(), Format::R8G8B8A8_UNorm);
texture->Shutdown();
delete texture;
}
TEST_F(D3D12TestFixture, Texture_Create_3D) {
TextureDesc desc = {};
desc.width = 64;
desc.height = 64;
desc.depth = 64;
desc.mipLevels = 1;
desc.arraySize = 1;
desc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
desc.textureType = static_cast<uint32_t>(TextureType::Texture3D);
desc.sampleCount = 1;
desc.sampleQuality = 0;
desc.flags = 0;
RHITexture* texture = GetDevice()->CreateTexture(desc);
ASSERT_NE(texture, nullptr);
EXPECT_EQ(texture->GetDepth(), 64);
texture->Shutdown();
delete texture;
}
TEST_F(D3D12TestFixture, Texture_Create_MultipleMips) {
TextureDesc desc = {};
desc.width = 512;
desc.height = 512;
desc.depth = 1;
desc.mipLevels = 5;
desc.arraySize = 1;
desc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
desc.textureType = static_cast<uint32_t>(TextureType::Texture2D);
desc.sampleCount = 1;
desc.sampleQuality = 0;
desc.flags = 0;
RHITexture* texture = GetDevice()->CreateTexture(desc);
ASSERT_NE(texture, nullptr);
EXPECT_EQ(texture->GetMipLevels(), 5);
texture->Shutdown();
delete texture;
}
TEST_F(D3D12TestFixture, Texture_Create_Array) {
TextureDesc desc = {};
desc.width = 128;
desc.height = 128;
desc.depth = 1;
desc.mipLevels = 1;
desc.arraySize = 4;
desc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
desc.textureType = static_cast<uint32_t>(TextureType::Texture2DArray);
desc.sampleCount = 1;
desc.sampleQuality = 0;
desc.flags = 0;
RHITexture* texture = GetDevice()->CreateTexture(desc);
ASSERT_NE(texture, nullptr);
texture->Shutdown();
delete texture;
}

View File

@@ -1,40 +0,0 @@
#include "fixtures/D3D12TestFixture.h"
#include "XCEngine/RHI/D3D12/D3D12DescriptorHeap.h"
using namespace XCEngine::RHI;
TEST_F(D3D12TestFixture, Views_Create_RTVDescriptorHeap) {
D3D12DescriptorHeap heap;
ASSERT_TRUE(heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::RTV, 1, false));
CPUDescriptorHandle handle = heap.GetCPUDescriptorHandle(0);
EXPECT_NE(handle.ptr, 0);
}
TEST_F(D3D12TestFixture, Views_Create_DSVDescriptorHeap) {
D3D12DescriptorHeap heap;
ASSERT_TRUE(heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::DSV, 1, false));
CPUDescriptorHandle handle = heap.GetCPUDescriptorHandle(0);
EXPECT_NE(handle.ptr, 0);
}
TEST_F(D3D12TestFixture, Views_Create_CBVDescriptorHeap) {
D3D12DescriptorHeap heap;
ASSERT_TRUE(heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::CBV_SRV_UAV, 1, true));
CPUDescriptorHandle handle = heap.GetCPUDescriptorHandle(0);
EXPECT_NE(handle.ptr, 0);
}
TEST_F(D3D12TestFixture, Views_Get_RTVDescriptorHandleIncrement) {
UINT rtvSize = GetDevice()->GetDescriptorHandleIncrementSize(DescriptorHeapType::RTV);
D3D12DescriptorHeap heap;
ASSERT_TRUE(heap.Initialize(GetDevice()->GetDevice(), DescriptorHeapType::RTV, 4, false));
CPUDescriptorHandle handle1 = heap.GetCPUDescriptorHandle(0);
CPUDescriptorHandle handle2 = heap.GetCPUDescriptorHandle(1);
EXPECT_EQ(handle2.ptr - handle1.ptr, rtvSize);
}

View File

@@ -1,9 +0,0 @@
cmake_minimum_required(VERSION 3.15)
project(OpenGLEngineTests)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(unit)
add_subdirectory(integration)

View File

@@ -1,251 +0,0 @@
# OpenGL 测试专项规范
本文档是 XCEngine 测试规范的 OpenGL 专项补充。
**前置阅读**: [tests/TEST_SPEC.md](../TEST_SPEC.md) - 通用测试规范
---
## 1. 概述
### 1.1 OpenGL 测试特点
| 特点 | 说明 |
|------|------|
| 平台依赖 | Windows WGL 实现Win32 原生 API |
| 窗口依赖 | 集成测试需要 GUI 窗口 |
| GPU 状态 | 测试间可能有 GPU 状态污染 |
| 无 GLFW | 已移除 GLFW 依赖,使用 Win32 WGL API |
### 1.2 测试层级
| 层级 | 位置 | 执行方式 | 框架 |
|------|------|----------|------|
| 单元测试 | `tests/RHI/OpenGL/unit/` | CTest | Google Test |
| 集成测试 | `tests/RHI/OpenGL/integration/` | CTest + Python | Python wrapper |
---
## 2. 单元测试规范
### 2.1 Fixture 设计
每个测试独立创建 OpenGL 上下文,避免 GPU 状态污染:
```cpp
class OpenGLTestFixture : public ::testing::Test {
protected:
void SetUp() override; // 创建窗口、初始化 OpenGL 上下文
void TearDown() override; // 清理资源、销毁上下文
HWND GetHWND();
HDC GetHDC();
HGLRC GetGLRC();
void MakeCurrent();
};
```
### 2.2 测试前缀对应
| 类名 | 测试前缀 |
|------|---------|
| OpenGLDevice | Device |
| OpenGLSwapChain | SwapChain |
| OpenGLBuffer | Buffer |
| OpenGLTexture | Texture |
| OpenGLShader | Shader |
| OpenGLCommandList | CommandList |
| OpenGLPipelineState | PipelineState |
| OpenGLRenderTargetView | RTV |
| OpenGLDepthStencilView | DSV |
| OpenGLFence | Fence |
| OpenGLSampler | Sampler |
| OpenGLVertexArray | VertexArray |
---
## 3. 集成测试规范
### 3.1 目录结构
每个集成测试独占一个子文件夹,资源相互隔离:
```
integration/
├── CMakeLists.txt # 构建配置
├── run_integration_test.py # 公共测试运行脚本
├── compare_ppm.py # PPM 图像比对脚本
├── minimal/ # 最小化测试
│ ├── main.cpp
│ ├── GT.ppm
│ ├── CMakeLists.txt
│ └── run.bat
└── ... # 其他测试
```
### 3.2 Python Wrapper
**位置**: `tests/RHI/OpenGL/integration/run_integration_test.py`
**职责**:
1. 启动 OpenGL exe
2. 等待进程完成
3. 检查输出文件 (PPM 截图)
4. 调用 `compare_ppm.py` 比对 Golden Image
5. 返回 0(成功)/1(失败)
### 3.3 CTest 注册格式
```cmake
add_test(NAME OpenGL_Minimal_Integration
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:OpenGL_Minimal>/run_integration_test.py
$<TARGET_FILE:OpenGL_Minimal>
minimal.ppm
${CMAKE_CURRENT_SOURCE_DIR}/minimal/GT.ppm
5
WORKING_DIRECTORY $<TARGET_FILE_DIR:OpenGL_Minimal>
)
```
### 3.4 Golden Image 规范
| 属性 | 值 |
|------|-----|
| 格式 | PPM (P6) |
| 命名 | `GT.ppm``GT_<test_name>.ppm` |
| 阈值 | 默认 5% |
| 存储位置 | `tests/RHI/OpenGL/integration/<test_name>/` |
### 3.5 Golden Image 生成流程
1. 在干净硬件环境运行集成测试
2. 截图保存为 `minimal.ppm`
3. 人工验证截图正确性
4. 提交到版本控制作为 GT
### 3.6 当前集成测试
| 测试名 | Golden Image | 状态 |
|--------|-------------|------|
| OpenGL_Minimal_Integration | `minimal/GT.ppm` | ✅ 通过 |
---
## 4. 测试执行
### 4.1 单元测试
```bash
# 方式 1: 使用统一脚本
python scripts/run_tests.py --unit-only
# 方式 2: 直接使用 CTest
cd build/tests/RHI/OpenGL/unit
ctest -C Debug --output-on-failure
```
### 4.2 集成测试
```bash
# 方式 1: 使用统一脚本
python scripts/run_tests.py --integration
# 方式 2: 直接使用 CTest
cd build/tests/RHI/OpenGL/integration
ctest -C Debug --output-on-failure
```
### 4.3 构建和测试
```bash
# 构建
cmake --build . --target OpenGL_Minimal --config Debug
# 运行测试
python scripts/run_tests.py --build
```
---
## 5. CI 集成
### 5.1 GitHub Actions 配置
```yaml
name: OpenGL Tests
on: [push, pull_request]
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build OpenGL Tests
run: cmake --build build --target OpenGL_Minimal --config Debug
- name: Run Unit Tests
run: cd build/tests/RHI/OpenGL/unit && ctest -C Debug --output-on-failure
- name: Run Integration Tests
run: python scripts/run_tests.py --integration
```
### 5.2 CI 模式
`--ci` 模式会跳过需要 GUI 的集成测试:
```bash
python scripts/run_tests.py --ci # 仅运行单元测试
```
---
## 6. 文件结构
```
tests/RHI/OpenGL/
├── CMakeLists.txt
├── TEST_SPEC.md # 本文档 (OpenGL 专项)
├── unit/
│ ├── CMakeLists.txt
│ ├── fixtures/
│ │ ├── OpenGLTestFixture.h
│ │ └── OpenGLTestFixture.cpp
│ └── test_device.cpp
└── integration/
├── CMakeLists.txt
├── run_integration_test.py # 公共脚本
├── compare_ppm.py # 公共脚本
└── minimal/ # 最小化测试
├── main.cpp
├── GT.ppm
├── CMakeLists.txt
└── run.bat
```
---
## 7. 已知问题
### 7.1 窗口尺寸问题
**问题**: `SetWindowPos` 的尺寸参数在 `WS_OVERLAPPEDWINDOW` 风格下是完整窗口尺寸(包含 chrome、边框而不是客户区尺寸。
**修复**: 使用 `AdjustWindowRect` 计算完整尺寸后再传给 `SetWindowPos`
---
## 8. 规范更新记录
| 版本 | 日期 | 变更 |
|------|------|------|
| 1.0 | 2026-03-20 | 初始版本,移除 GLFW 依赖后的重构版本 |
---
**规范版本**: 1.0
**最后更新**: 2026-03-20
**前置文档**: [tests/TEST_SPEC.md](../TEST_SPEC.md)

View File

@@ -1,12 +0,0 @@
cmake_minimum_required(VERSION 3.15)
project(OpenGL_Integration)
find_package(Python3 REQUIRED)
enable_testing()
add_subdirectory(minimal)
add_subdirectory(triangle)
add_subdirectory(quad)
add_subdirectory(sphere)

View File

@@ -1,75 +0,0 @@
import sys
import os
def read_ppm(filename):
with open(filename, "rb") as f:
header = f.readline()
if header != b"P6\n":
raise ValueError(f"Not a P6 PPM file: {filename}")
while True:
line = f.readline()
if not line.startswith(b"#"):
break
dims = line.split()
width, height = int(dims[0]), int(dims[1])
line = f.readline()
maxval = int(line.strip())
data = f.read()
return width, height, data
def compare_ppm(file1, file2, threshold):
w1, h1, d1 = read_ppm(file1)
w2, h2, d2 = read_ppm(file2)
if w1 != w2 or h1 != h2:
print(f"ERROR: Size mismatch - {file1}: {w1}x{h1}, {file2}: {w2}x{h2}")
return False
total_pixels = w1 * h1
diff_count = 0
for i in range(len(d1)):
diff = abs(d1[i] - d2[i])
if diff > threshold:
diff_count += 1
diff_percent = (diff_count / (total_pixels * 3)) * 100
print(f"Image 1: {file1} ({w1}x{h1})")
print(f"Image 2: {file2} ({w2}x{h2})")
print(f"Threshold: {threshold}")
print(f"Different pixels: {diff_count} / {total_pixels * 3} ({diff_percent:.2f}%)")
if diff_percent <= 1.0:
print("PASS: Images match!")
return True
else:
print("FAIL: Images differ!")
return False
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Usage: python compare_ppm.py <file1.ppm> <file2.ppm> <threshold>")
sys.exit(1)
file1 = sys.argv[1]
file2 = sys.argv[2]
threshold = int(sys.argv[3])
if not os.path.exists(file1):
print(f"ERROR: File not found: {file1}")
sys.exit(1)
if not os.path.exists(file2):
print(f"ERROR: File not found: {file2}")
sys.exit(1)
result = compare_ppm(file1, file2, threshold)
sys.exit(0 if result else 1)

View File

@@ -1,54 +0,0 @@
cmake_minimum_required(VERSION 3.15)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
project(opengl_minimal_test)
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/mvs/OpenGL/package)
add_executable(opengl_minimal_test
WIN32
main.cpp
${PACKAGE_DIR}/src/glad.c
)
target_include_directories(opengl_minimal_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${ENGINE_ROOT_DIR}/include
${PACKAGE_DIR}/include
)
target_link_libraries(opengl_minimal_test PRIVATE
opengl32
XCEngine
)
target_compile_definitions(opengl_minimal_test PRIVATE
UNICODE
_UNICODE
)
add_custom_command(TARGET opengl_minimal_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/OpenGL/integration/run_integration_test.py
$<TARGET_FILE_DIR:opengl_minimal_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/OpenGL/integration/compare_ppm.py
$<TARGET_FILE_DIR:opengl_minimal_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
$<TARGET_FILE_DIR:opengl_minimal_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
$<TARGET_FILE_DIR:opengl_minimal_test>/
)
add_test(NAME opengl_minimal_test
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:opengl_minimal_test>/run_integration_test.py
$<TARGET_FILE:opengl_minimal_test>
minimal.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
5
WORKING_DIRECTORY $<TARGET_FILE_DIR:opengl_minimal_test>
)

View File

@@ -1,151 +0,0 @@
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <GL/gl.h>
#include "XCEngine/RHI/OpenGL/OpenGLDevice.h"
#include "XCEngine/RHI/OpenGL/OpenGLSwapChain.h"
#include "XCEngine/RHI/OpenGL/OpenGLCommandList.h"
#include "XCEngine/RHI/OpenGL/OpenGLScreenshot.h"
#include "XCEngine/Debug/Logger.h"
#include "XCEngine/Debug/ConsoleLogSink.h"
#include "XCEngine/Debug/RenderDocCapture.h"
#include "XCEngine/Core/Containers/String.h"
#pragma comment(lib, "opengl32.lib")
using namespace XCEngine::RHI;
using namespace XCEngine::Debug;
using namespace XCEngine::Containers;
static const int gWidth = 1280;
static const int gHeight = 720;
void Log(const char* format, ...) {
char buffer[1024];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
Logger::Get().Debug(LogCategory::Rendering, String(buffer));
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
_putenv_s("RENDERDOC_CAPTUREINTERACTIVE", "0");
_putenv_s("RENDERDOC_CAPTUREFRAMESTART", "0");
Logger::Get().Initialize();
Logger::Get().AddSink(std::make_unique<ConsoleLogSink>());
Logger::Get().SetMinimumLevel(LogLevel::Debug);
Log("[INFO] OpenGL Integration Test Starting");
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(WNDCLASSEXW);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"XCEngine_OpenGL_Test";
if (!RegisterClassExW(&wc)) {
Log("[ERROR] Failed to register window class");
return -1;
}
RECT rect = { 0, 0, gWidth, gHeight };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
HWND hwnd = CreateWindowExW(
0,
L"XCEngine_OpenGL_Test",
L"OpenGL Integration Test",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
NULL, NULL, hInstance, NULL
);
if (!hwnd) {
Log("[ERROR] Failed to create window");
return -1;
}
RenderDocCapture::Get().Initialize(nullptr, hwnd);
RenderDocCapture::Get().SetCaptureFilePath(".\\minimal_frame30");
OpenGLDevice device;
if (!device.InitializeWithExistingWindow(hwnd)) {
Log("[ERROR] Failed to initialize OpenGL device");
return -1;
}
RenderDocCapture::Get().SetDevice(device.GetNativeContext());
ShowWindow(hwnd, nShowCmd);
UpdateWindow(hwnd);
Log("[INFO] OpenGL Device: %S", device.GetDeviceInfo().renderer.c_str());
Log("[INFO] OpenGL Version: %S", device.GetDeviceInfo().version.c_str());
OpenGLSwapChain swapChain;
swapChain.Initialize(&device, hwnd, gWidth, gHeight);
OpenGLCommandList commandList;
MSG msg = {};
int frameCount = 0;
const int captureStartFrame = 29;
const int captureEndFrame = 30;
while (true) {
if (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
} else {
device.MakeContextCurrent();
commandList.SetViewport(0, 0, gWidth, gHeight);
commandList.Clear(1.0f, 0.0f, 0.0f, 1.0f, 1 | 2);
swapChain.Present(0, 0);
frameCount++;
if (frameCount >= captureEndFrame) {
RenderDocCapture::Get().EndCapture();
Log("[INFO] RenderDoc capture ended at frame %d", frameCount);
break;
}
if (frameCount == captureStartFrame) {
RenderDocCapture::Get().BeginCapture("OpenGL_Minimal_Test");
Log("[INFO] RenderDoc capture started at frame %d", frameCount);
}
}
}
Log("[INFO] Taking screenshot!");
OpenGLScreenshot::Capture(device, swapChain, "minimal.ppm");
RenderDocCapture::Get().Shutdown();
swapChain.Shutdown();
device.Shutdown();
Logger::Get().Shutdown();
Log("[INFO] OpenGL Integration Test Finished");
return 0;
}

View File

@@ -1,60 +0,0 @@
cmake_minimum_required(VERSION 3.15)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
project(opengl_quad_test)
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/mvs/OpenGL/package)
add_executable(opengl_quad_test
WIN32
main.cpp
${PACKAGE_DIR}/src/glad.c
)
target_include_directories(opengl_quad_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${ENGINE_ROOT_DIR}/include
${PACKAGE_DIR}/include
)
target_link_libraries(opengl_quad_test PRIVATE
opengl32
XCEngine
)
target_compile_definitions(opengl_quad_test PRIVATE
UNICODE
_UNICODE
)
add_custom_command(TARGET opengl_quad_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/Res
$<TARGET_FILE_DIR:opengl_quad_test>/Res
)
add_custom_command(TARGET opengl_quad_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/OpenGL/integration/run_integration_test.py
$<TARGET_FILE_DIR:opengl_quad_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/OpenGL/integration/compare_ppm.py
$<TARGET_FILE_DIR:opengl_quad_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
$<TARGET_FILE_DIR:opengl_quad_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
$<TARGET_FILE_DIR:opengl_quad_test>/
)
add_test(NAME opengl_quad_test
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:opengl_quad_test>/run_integration_test.py
$<TARGET_FILE:opengl_quad_test>
quad.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
5
WORKING_DIRECTORY $<TARGET_FILE_DIR:opengl_quad_test>
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -1,10 +0,0 @@
#version 460
in vec2 vTexcoord;
out vec4 fragColor;
uniform sampler2D uTexture;
void main() {
fragColor = texture(uTexture, vTexcoord);
}

View File

@@ -1,11 +0,0 @@
#version 460
layout(location = 0) in vec4 aPosition;
layout(location = 1) in vec2 aTexcoord;
out vec2 vTexcoord;
void main() {
gl_Position = aPosition;
vTexcoord = aTexcoord;
}

View File

@@ -1,251 +0,0 @@
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <glad/glad.h>
#include "XCEngine/RHI/OpenGL/OpenGLDevice.h"
#include "XCEngine/RHI/OpenGL/OpenGLSwapChain.h"
#include "XCEngine/RHI/OpenGL/OpenGLCommandList.h"
#include "XCEngine/RHI/OpenGL/OpenGLBuffer.h"
#include "XCEngine/RHI/OpenGL/OpenGLVertexArray.h"
#include "XCEngine/RHI/OpenGL/OpenGLShader.h"
#include "XCEngine/RHI/OpenGL/OpenGLPipelineState.h"
#include "XCEngine/RHI/OpenGL/OpenGLTexture.h"
#include "XCEngine/RHI/OpenGL/OpenGLSampler.h"
#include "XCEngine/RHI/OpenGL/OpenGLScreenshot.h"
#include "XCEngine/Debug/Logger.h"
#include "XCEngine/Debug/ConsoleLogSink.h"
#include "XCEngine/Debug/RenderDocCapture.h"
#include "XCEngine/Core/Containers/String.h"
#pragma comment(lib, "opengl32.lib")
using namespace XCEngine::RHI;
using namespace XCEngine::Debug;
using namespace XCEngine::Containers;
static const int gWidth = 1280;
static const int gHeight = 720;
void Log(const char* format, ...) {
char buffer[1024];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
Logger::Get().Debug(LogCategory::Rendering, String(buffer));
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
Logger::Get().Initialize();
Logger::Get().AddSink(std::make_unique<ConsoleLogSink>());
Logger::Get().SetMinimumLevel(LogLevel::Debug);
Log("[INFO] OpenGL Quad Test Starting");
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(WNDCLASSEXW);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"XCEngine_OpenGL_Quad";
if (!RegisterClassExW(&wc)) {
Log("[ERROR] Failed to register window class");
return -1;
}
RECT rect = { 0, 0, gWidth, gHeight };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
HWND hwnd = CreateWindowExW(
0,
L"XCEngine_OpenGL_Quad",
L"OpenGL Quad Test",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
NULL, NULL, hInstance, NULL
);
if (!hwnd) {
Log("[ERROR] Failed to create window");
return -1;
}
RenderDocCapture::Get().Initialize(nullptr, hwnd);
RenderDocCapture::Get().SetCaptureFilePath(".\\quad_frame30");
OpenGLDevice device;
if (!device.InitializeWithExistingWindow(hwnd)) {
Log("[ERROR] Failed to initialize OpenGL device");
return -1;
}
RenderDocCapture::Get().SetDevice(device.GetNativeContext());
ShowWindow(hwnd, nShowCmd);
UpdateWindow(hwnd);
Log("[INFO] OpenGL Device: %S", device.GetDeviceInfo().renderer.c_str());
Log("[INFO] OpenGL Version: %S", device.GetDeviceInfo().version.c_str());
OpenGLSwapChain swapChain;
swapChain.Initialize(&device, hwnd, gWidth, gHeight);
OpenGLCommandList commandList;
struct Vertex {
float pos[4];
float texcoord[2];
};
Vertex vertices[] = {
{ { -0.5f, -0.5f, 0.0f, 1.0f }, { 0.0f, 1.0f } },
{ { -0.5f, 0.5f, 0.0f, 1.0f }, { 0.0f, 0.0f } },
{ { 0.5f, -0.5f, 0.0f, 1.0f }, { 1.0f, 1.0f } },
{ { 0.5f, 0.5f, 0.0f, 1.0f }, { 1.0f, 0.0f } },
};
OpenGLBuffer vertexBuffer;
if (!vertexBuffer.InitializeVertexBuffer(vertices, sizeof(vertices))) {
Log("[ERROR] Failed to initialize vertex buffer");
return -1;
}
vertexBuffer.SetStride(sizeof(Vertex));
OpenGLVertexArray vertexArray;
vertexArray.Initialize();
VertexAttribute posAttr = {};
posAttr.index = 0;
posAttr.count = 4;
posAttr.type = VertexAttributeType::Float;
posAttr.normalized = VertexAttributeNormalized::False;
posAttr.stride = sizeof(Vertex);
posAttr.offset = 0;
vertexArray.AddVertexBuffer(vertexBuffer.GetID(), posAttr);
VertexAttribute texAttr = {};
texAttr.index = 1;
texAttr.count = 2;
texAttr.type = VertexAttributeType::Float;
texAttr.normalized = VertexAttributeNormalized::False;
texAttr.stride = sizeof(Vertex);
texAttr.offset = sizeof(float) * 4;
vertexArray.AddVertexBuffer(vertexBuffer.GetID(), texAttr);
OpenGLShader shader;
if (!shader.CompileFromFile("Res/Shader/quad.vert", "Res/Shader/quad.frag")) {
Log("[ERROR] Failed to compile shaders");
return -1;
}
Log("[INFO] Shaders compiled successfully");
commandList.SetShader(&shader);
commandList.SetUniformInt("uTexture", 0);
OpenGLPipelineState pipelineState;
OpenGLRasterizerState rasterizerState;
rasterizerState.cullFaceEnable = false;
pipelineState.SetRasterizerState(rasterizerState);
OpenGLDepthStencilState depthStencilState;
depthStencilState.depthTestEnable = false;
depthStencilState.depthWriteEnable = false;
pipelineState.SetDepthStencilState(depthStencilState);
ViewportState viewportState = { 0.0f, 0.0f, (float)gWidth, (float)gHeight, 0.0f, 1.0f };
pipelineState.SetViewport(viewportState);
pipelineState.AttachShader(shader.GetID());
pipelineState.Apply();
OpenGLTexture texture;
if (!texture.LoadFromFile("Res/Image/earth.png")) {
Log("[ERROR] Failed to load texture");
return -1;
}
Log("[INFO] Texture loaded successfully");
OpenGLSampler sampler;
OpenGLSamplerDesc samplerDesc = {};
samplerDesc.minFilter = SamplerFilter::Linear;
samplerDesc.magFilter = SamplerFilter::Linear;
samplerDesc.wrapS = SamplerWrapMode::ClampToEdge;
samplerDesc.wrapT = SamplerWrapMode::ClampToEdge;
sampler.Initialize(samplerDesc);
MSG msg = {};
int frameCount = 0;
const int captureStartFrame = 29;
const int captureEndFrame = 30;
while (true) {
if (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
} else {
device.MakeContextCurrent();
commandList.SetViewport(0, 0, gWidth, gHeight);
commandList.Clear(0.0f, 0.0f, 1.0f, 1.0f, 1);
pipelineState.Bind();
vertexArray.Bind();
texture.Bind(0);
sampler.Bind(0);
commandList.Draw(PrimitiveType::TriangleStrip, 4, 0);
frameCount++;
if (frameCount >= captureEndFrame) {
RenderDocCapture::Get().EndCapture();
Log("[INFO] RenderDoc capture ended at frame %d", frameCount);
Log("[INFO] Capture complete - taking screenshot!");
OpenGLScreenshot::Capture(device, swapChain, "quad.ppm");
break;
}
swapChain.Present(0, 0);
if (frameCount == captureStartFrame) {
RenderDocCapture::Get().BeginCapture("OpenGL_Quad_Test");
Log("[INFO] RenderDoc capture started at frame %d", frameCount);
}
}
}
sampler.Shutdown();
texture.Shutdown();
vertexArray.Shutdown();
vertexBuffer.Shutdown();
shader.Shutdown();
pipelineState.Shutdown();
swapChain.Shutdown();
device.Shutdown();
RenderDocCapture::Get().Shutdown();
Logger::Get().Shutdown();
Log("[INFO] OpenGL Quad Test Finished");
return 0;
}

View File

@@ -1,124 +0,0 @@
import sys
import os
import subprocess
import time
import shutil
def run_integration_test(exe_path, output_ppm, gt_ppm, threshold, timeout=120):
"""
Run a D3D12 integration test and compare output with golden template.
Args:
exe_path: Path to the test executable
output_ppm: Filename of the output screenshot
gt_ppm: Path to the golden template PPM file
threshold: Pixel difference threshold for comparison
timeout: Maximum time to wait for test completion (seconds)
Returns:
0 on success, non-zero on failure
"""
exe_dir = os.path.dirname(os.path.abspath(exe_path))
output_path = os.path.join(exe_dir, output_ppm)
print(f"[Integration Test] Starting: {exe_path}")
print(f"[Integration Test] Working directory: {exe_dir}")
print(f"[Integration Test] Expected output: {output_path}")
if not os.path.exists(exe_path):
print(f"[Integration Test] ERROR: Executable not found: {exe_path}")
return 1
if not os.path.exists(gt_ppm):
print(f"[Integration Test] ERROR: Golden template not found: {gt_ppm}")
return 1
if os.path.exists(output_path):
print(f"[Integration Test] Removing old output: {output_path}")
os.remove(output_path)
try:
print(f"[Integration Test] Launching process...")
start_time = time.time()
process = subprocess.Popen(
[exe_path],
cwd=exe_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == "nt" else 0,
)
returncode = None
while time.time() - start_time < timeout:
returncode = process.poll()
if returncode is not None:
break
time.sleep(0.5)
if returncode is None:
print(f"[Integration Test] ERROR: Process timed out after {timeout}s")
process.kill()
return 1
elapsed = time.time() - start_time
print(
f"[Integration Test] Process finished in {elapsed:.1f}s with exit code: {returncode}"
)
if returncode != 0:
print(f"[Integration Test] ERROR: Process returned non-zero exit code")
stdout, stderr = process.communicate(timeout=5)
if stdout:
print(
f"[Integration Test] STDOUT:\n{stdout.decode('utf-8', errors='replace')}"
)
if stderr:
print(
f"[Integration Test] STDERR:\n{stderr.decode('utf-8', errors='replace')}"
)
return 1
except Exception as e:
print(f"[Integration Test] ERROR: Failed to run process: {e}")
return 1
if not os.path.exists(output_path):
print(f"[Integration Test] ERROR: Output file not created: {output_path}")
return 1
print(f"[Integration Test] Running image comparison...")
script_dir = os.path.dirname(os.path.abspath(__file__))
compare_script = os.path.join(script_dir, "compare_ppm.py")
try:
result = subprocess.run(
[sys.executable, compare_script, output_path, gt_ppm, str(threshold)],
cwd=exe_dir,
capture_output=True,
text=True,
)
print(result.stdout)
if result.stderr:
print(f"[Integration Test] Comparison STDERR: {result.stderr}")
return result.returncode
except Exception as e:
print(f"[Integration Test] ERROR: Failed to run comparison: {e}")
return 1
if __name__ == "__main__":
if len(sys.argv) != 5:
print(
"Usage: run_integration_test.py <exe_path> <output_ppm> <gt_ppm> <threshold>"
)
sys.exit(1)
exe_path = sys.argv[1]
output_ppm = sys.argv[2]
gt_ppm = sys.argv[3]
threshold = int(sys.argv[4])
exit_code = run_integration_test(exe_path, output_ppm, gt_ppm, threshold)
sys.exit(exit_code)

View File

@@ -1,60 +0,0 @@
cmake_minimum_required(VERSION 3.15)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
project(opengl_sphere_test)
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/mvs/OpenGL/package)
add_executable(opengl_sphere_test
WIN32
main.cpp
${PACKAGE_DIR}/src/glad.c
)
target_include_directories(opengl_sphere_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${ENGINE_ROOT_DIR}/include
${PACKAGE_DIR}/include
)
target_link_libraries(opengl_sphere_test PRIVATE
opengl32
XCEngine
)
target_compile_definitions(opengl_sphere_test PRIVATE
UNICODE
_UNICODE
)
add_custom_command(TARGET opengl_sphere_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/Res
$<TARGET_FILE_DIR:opengl_sphere_test>/Res
)
add_custom_command(TARGET opengl_sphere_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/OpenGL/integration/run_integration_test.py
$<TARGET_FILE_DIR:opengl_sphere_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/tests/RHI/OpenGL/integration/compare_ppm.py
$<TARGET_FILE_DIR:opengl_sphere_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
$<TARGET_FILE_DIR:opengl_sphere_test>/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
$<TARGET_FILE_DIR:opengl_sphere_test>/
)
add_test(NAME opengl_sphere_test
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:opengl_sphere_test>/run_integration_test.py
$<TARGET_FILE:opengl_sphere_test>
sphere.ppm
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
5
WORKING_DIRECTORY $<TARGET_FILE_DIR:opengl_sphere_test>
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -1,10 +0,0 @@
#version 460
in vec2 vTexcoord;
out vec4 fragColor;
uniform sampler2D uTexture;
void main() {
fragColor = texture(uTexture, vTexcoord);
}

View File

@@ -1,17 +0,0 @@
#version 460
layout(location = 0) in vec4 aPosition;
layout(location = 1) in vec2 aTexcoord;
out vec2 vTexcoord;
uniform mat4 gModelMatrix;
uniform mat4 gViewMatrix;
uniform mat4 gProjectionMatrix;
void main() {
vec4 positionWS = gModelMatrix * aPosition;
vec4 positionVS = gViewMatrix * positionWS;
gl_Position = gProjectionMatrix * positionVS;
vTexcoord = aTexcoord;
}

Some files were not shown because too many files have changed in this diff Show More