tests: remove legacy test tree
This commit is contained in:
@@ -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:"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
)
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
@@ -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.
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB |
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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.
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB |
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 = ¶m;
|
||||
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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
)
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
@@ -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.
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB |
@@ -1,10 +0,0 @@
|
||||
#version 460
|
||||
|
||||
in vec2 vTexcoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform sampler2D uTexture;
|
||||
|
||||
void main() {
|
||||
fragColor = texture(uTexture, vTexcoord);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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.
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB |
@@ -1,10 +0,0 @@
|
||||
#version 460
|
||||
|
||||
in vec2 vTexcoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform sampler2D uTexture;
|
||||
|
||||
void main() {
|
||||
fragColor = texture(uTexture, vTexcoord);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user