diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 681434af9..d8395b6cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,8 +18,11 @@ env: jobs: build: strategy: + # Linux runs alongside Windows/macOS, but the engine's own Linux support is + # not yet verified -- keep a failing Linux leg from cancelling the others. + fail-fast: false matrix: - os: ['windows-2022', 'macOS-latest'] + os: ['windows-2022', 'macOS-latest', 'ubuntu-latest'] runs-on: ${{ matrix.os }} @@ -38,6 +41,14 @@ jobs: uses: ilammy/msvc-dev-cmd@v1 if: runner.os == 'Windows' + # Qt's prebuilt Qt6::Gui transitively requires WrapOpenGL, so CMake's FindOpenGL needs the GL dev + # files (libOpenGL.so, libGLX.so, GL/gl.h). The runners only ship the runtime libraries by default. + - name: Setup Linux Dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgl1-mesa-dev libglu1-mesa-dev + if: runner.os == 'Linux' + - name: Checkout Repo uses: actions/checkout@v4 @@ -62,6 +73,15 @@ jobs: - name: Config Conan Remote run: conan remote add explosion https://conan.kindem.online/artifactory/api/conan/conan + # xorg/system (pulled in transitively by Qt on Linux) declares its X11 dev packages as system + # requirements; let Conan install them via apt instead of erroring out in the default 'check' mode. + - name: Allow Conan System Package Install + run: | + conf="$(conan config home)/global.conf" + echo "tools.system.package_manager:mode=install" >> "$conf" + echo "tools.system.package_manager:sudo=True" >> "$conf" + if: runner.os == 'Linux' + # Register the in-repo recipes in the local cache so the engine's 'conan install --build=missing' # resolves them from this checkout: unchanged recipes hash to the revision already published on the # remote (binaries are downloaded), changed ones get built locally, making recipe PRs self-contained. diff --git a/.github/workflows/publish-conan-recipes.yml b/.github/workflows/publish-conan-recipes.yml index 4fcf6b26f..09d3b73b5 100644 --- a/.github/workflows/publish-conan-recipes.yml +++ b/.github/workflows/publish-conan-recipes.yml @@ -33,6 +33,8 @@ jobs: cppstd: '17' - os: macOS-latest cppstd: gnu17 + - os: ubuntu-latest + cppstd: gnu17 runs-on: ${{ matrix.os }} @@ -45,6 +47,20 @@ jobs: uses: ilammy/msvc-dev-cmd@v1 if: runner.os == 'Windows' + # The Linux leg runs Qt's test_package, a headless QWebEngineView smoke test, which needs: + # - GL dev files (libOpenGL.so, libGLX.so, GL/gl.h) for CMake's FindOpenGL at configure time, since + # Qt6::Gui transitively requires WrapOpenGL; runners ship only the GL runtime libraries. + # - QtWebEngine's Chromium runtime libs (NSS/NSPR, ALSA, libxkbfile) so libQt6WebEngineCore loads. + # The headless run env (QT_QPA_PLATFORM=offscreen, QTWEBENGINE_DISABLE_SANDBOX=1) is set by the qt + # recipe's test_package itself, so it is not configured here. + - name: Setup Linux Dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libgl1-mesa-dev libglu1-mesa-dev \ + libnss3 libnspr4 libxkbfile1 libasound2t64 + if: runner.os == 'Linux' + - name: Checkout Repo uses: actions/checkout@v4 diff --git a/CMake/Common.cmake b/CMake/Common.cmake index 731ab792c..e871dd0ed 100644 --- a/CMake/Common.cmake +++ b/CMake/Common.cmake @@ -6,6 +6,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_UNITY_BUILD ${USE_UNITY_BUILD}) set(CMAKE_EXPORT_COMPILE_COMMANDS ${EXPORT_COMPILE_COMMANDS}) +# Static libraries such as Common get linked into the engine's shared libraries, so on ELF platforms every object must be +# position-independent or the shared link fails with "relocation ... can not be used when making a shared object". +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + if (${CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT}) set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/Installed CACHE PATH "" FORCE) endif() diff --git a/CMakeLists.txt b/CMakeLists.txt index e2d1bb8c3..f9a742e68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,9 +15,13 @@ set(SUB_PROJECT_VERSION_PATCH 1 CACHE STRING "" FORCE) set(SUB_PROJECT_CMAKE_LIBS "ThirdParty/Registry.cmake" CACHE STRING "" FORCE) set(USE_CONAN ON CACHE BOOL "" FORCE) -set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG "Release" CACHE STRING "" FORCE) -set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO "Release" CACHE STRING "" FORCE) -set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL "Release" CACHE STRING "" FORCE) +# Conan builds its dependencies as Release, so map the other configs onto Release imported locations. The trailing +# empty element keeps the configuration-less IMPORTED_LOCATION as a fallback; system find-modules such as FindOpenGL +# create UNKNOWN imported targets (OpenGL::OpenGL / OpenGL::GLX on Linux) that only set the suffix-less location, and +# without this fallback CMake reports "IMPORTED_LOCATION not set ... configuration Debug" at generate time. +set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG "Release;" CACHE STRING "" FORCE) +set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO "Release;" CACHE STRING "" FORCE) +set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL "Release;" CACHE STRING "" FORCE) add_compile_definitions(BUILD_EDITOR=$) diff --git a/Editor/CMakeLists.txt b/Editor/CMakeLists.txt index 02d6c5e5f..c25b9a9e0 100644 --- a/Editor/CMakeLists.txt +++ b/Editor/CMakeLists.txt @@ -124,7 +124,11 @@ exp_add_resources_copy_command( # ---- end shaders ----------------------------------------------------------------------------------- # ---- begin web project ----------------------------------------------------------------------------- -find_program(npm_executable NAMES npm.cmd npm REQUIRED NO_CACHE) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + find_program(npm_executable NAMES npm.cmd npm REQUIRED NO_CACHE) +else () + find_program(npm_executable NAMES npm REQUIRED NO_CACHE) +endif () message("Perform web project npm install......") execute_process( COMMAND ${CMAKE_COMMAND} -E env ${npm_executable} install --no-fund --registry=https://registry.npmmirror.com diff --git a/Editor/Include/Editor/Qt/JsonSerialization.h b/Editor/Include/Editor/Qt/JsonSerialization.h index a8e296a21..9434dea47 100644 --- a/Editor/Include/Editor/Qt/JsonSerialization.h +++ b/Editor/Include/Editor/Qt/JsonSerialization.h @@ -175,7 +175,7 @@ namespace Editor { struct QtJsonSerializer { static void QtJsonSerialize(QJsonValue& outJsonValue, int64_t inValue) { - outJsonValue = inValue; + outJsonValue = static_cast(inValue); } static void QtJsonDeserialize(const QJsonValue& inJsonValue, int64_t& outValue) @@ -191,7 +191,7 @@ namespace Editor { struct QtJsonSerializer { static void QtJsonSerialize(QJsonValue& outJsonValue, uint64_t inValue) { - outJsonValue = static_cast(inValue); + outJsonValue = static_cast(inValue); } static void QtJsonDeserialize(const QJsonValue& inJsonValue, uint64_t& outValue) diff --git a/Engine/Source/Common/Include/Common/Delegate.h b/Engine/Source/Common/Include/Common/Delegate.h index 02d585656..e938752e2 100644 --- a/Engine/Source/Common/Include/Common/Delegate.h +++ b/Engine/Source/Common/Include/Common/Delegate.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include diff --git a/Engine/Source/Common/Include/Common/Math/Quaternion.h b/Engine/Source/Common/Include/Common/Math/Quaternion.h index 241d02756..87852748c 100644 --- a/Engine/Source/Common/Include/Common/Math/Quaternion.h +++ b/Engine/Source/Common/Include/Common/Math/Quaternion.h @@ -84,7 +84,7 @@ namespace Common { Quaternion& operator+=(const Quaternion& rhs); Quaternion& operator-=(const Quaternion& rhs); - Quaternion& operator*=(T rhs) const; + Quaternion& operator*=(T rhs); Quaternion& operator*=(const Quaternion& rhs); Quaternion& operator/=(T rhs); @@ -97,6 +97,9 @@ namespace Common { // when axis faced to us, ccw as positive direction Vec RotateVector(const Vec& inVector) const; Mat GetRotationMatrix() const; + + template + Quaternion CastTo() const; }; template @@ -541,7 +544,7 @@ namespace Common { } template - Quaternion& Quaternion::operator*=(T rhs) const + Quaternion& Quaternion::operator*=(T rhs) { this->w *= rhs; this->x *= rhs; @@ -553,7 +556,7 @@ namespace Common { template Quaternion& Quaternion::operator*=(const Quaternion& rhs) { - *this = this * rhs; + *this = *this * rhs; return *this; } @@ -610,7 +613,7 @@ namespace Common { template T Quaternion::Dot(const Quaternion& rhs) const { - return this->w * rhs.w * this->x * rhs.x + this->y * rhs.y + this->z * rhs.z; + return this->w * rhs.w + this->x * rhs.x + this->y * rhs.y + this->z * rhs.z; } template @@ -643,4 +646,15 @@ namespace Common { ); } + template + template + Quaternion Quaternion::CastTo() const + { + Quaternion result; + result.w = static_cast(this->w); + result.x = static_cast(this->x); + result.y = static_cast(this->y); + result.z = static_cast(this->z); + return result; + } } diff --git a/Engine/Source/Common/Include/Common/Math/Rect.h b/Engine/Source/Common/Include/Common/Math/Rect.h index a8035371e..4974b0dd7 100644 --- a/Engine/Source/Common/Include/Common/Math/Rect.h +++ b/Engine/Source/Common/Include/Common/Math/Rect.h @@ -219,7 +219,7 @@ namespace Common { template T Rect::Size() const { - Vec diagonal = this->max - this->min; + Vec diagonal = this->max - this->min; return diagonal.Model() / T(2); } diff --git a/Engine/Source/Common/Include/Common/Math/Transform.h b/Engine/Source/Common/Include/Common/Math/Transform.h index 84c64d08d..d3fb18e4c 100644 --- a/Engine/Source/Common/Include/Common/Math/Transform.h +++ b/Engine/Source/Common/Include/Common/Math/Transform.h @@ -434,7 +434,7 @@ namespace Common { template Vec Transform::TransformPosition(const Vec& inPosition) const { - Mat posColMat = Mat::FromColVecs(inPosition); + Mat posColMat = Mat::FromColVecs(Vec(inPosition)); return (GetTransformMatrix() * posColMat).Col(0); } diff --git a/Engine/Source/Common/Include/Common/Math/Vector.h b/Engine/Source/Common/Include/Common/Math/Vector.h index 60f94a589..47ee96481 100644 --- a/Engine/Source/Common/Include/Common/Math/Vector.h +++ b/Engine/Source/Common/Include/Common/Math/Vector.h @@ -128,7 +128,7 @@ namespace Common { static const Vec unit; static const Vec negaUnitX; static const Vec negaUnitY; - static const Vec negaUintZ; + static const Vec negaUnitZ; static const Vec negaUnitW; static const Vec negaUnit; }; @@ -492,7 +492,7 @@ namespace Common { const Vec VecConsts::negaUnitY = Vec(0, -1, 0, 0); template - const Vec VecConsts::negaUintZ = Vec(0, 0, -1, 0); + const Vec VecConsts::negaUnitZ = Vec(0, 0, -1, 0); template const Vec VecConsts::negaUnitW = Vec(0, 0, 0, -1); diff --git a/Engine/Source/Common/Include/Common/Math/View.h b/Engine/Source/Common/Include/Common/Math/View.h index ad6cad9b0..17c4f79c3 100644 --- a/Engine/Source/Common/Include/Common/Math/View.h +++ b/Engine/Source/Common/Include/Common/Math/View.h @@ -73,7 +73,7 @@ namespace Common { template ViewTransform ViewTransform::LookAt(const Vec& inPosition, const Vec& inTargetPosition, const Vec& inUpDirection) { - return Transform::LookAt(inPosition, inTargetPosition, inUpDirection); + return ViewTransform(Transform::LookAt(inPosition, inTargetPosition, inUpDirection)); } template diff --git a/Engine/Source/Common/Include/Common/Platform.h b/Engine/Source/Common/Include/Common/Platform.h index 110b73e3d..a98c64bd6 100644 --- a/Engine/Source/Common/Include/Common/Platform.h +++ b/Engine/Source/Common/Include/Common/Platform.h @@ -6,6 +6,12 @@ #include +// GCC and Clang in GNU mode predefine the bare macro `linux` as 1, which collides with the linux enumerators below. +// Drop it so the lowercase enumerator stays valid; the canonical `__linux__` is unaffected. +#ifdef linux +#undef linux +#endif + namespace Common { enum class DevelopmentPlatform { windows, diff --git a/Engine/Source/Common/Include/Common/Serialization.h b/Engine/Source/Common/Include/Common/Serialization.h index ac48aa5a4..897231f70 100644 --- a/Engine/Source/Common/Include/Common/Serialization.h +++ b/Engine/Source/Common/Include/Common/Serialization.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include diff --git a/Engine/Source/Common/Src/Concurrent.cpp b/Engine/Source/Common/Src/Concurrent.cpp index 34dfbcb70..c0c3b0883 100644 --- a/Engine/Source/Common/Src/Concurrent.cpp +++ b/Engine/Source/Common/Src/Concurrent.cpp @@ -26,7 +26,10 @@ namespace Common { #elif PLATFORM_MACOS pthread_setname_np(name.c_str()); #else - pthread_setname_np(thread.native_handle(), name.c_str()); + // SetThreadName always runs inside the freshly spawned thread, so name the current thread via pthread_self(); + // reading thread.native_handle() here would race with the std::thread move-assignment in the constructor and + // could observe a null handle, crashing pthread_setname_np. + pthread_setname_np(pthread_self(), name.c_str()); #endif } diff --git a/Engine/Source/Common/Src/Math/Color.cpp b/Engine/Source/Common/Src/Math/Color.cpp index a8fc829e8..f6bc58244 100644 --- a/Engine/Source/Common/Src/Math/Color.cpp +++ b/Engine/Source/Common/Src/Math/Color.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -59,7 +60,11 @@ namespace Common { std::string Color::ToHexString() const { std::stringstream result; - result << "0x" << std::hex << r << std::hex << g << std::hex << b << std::hex << a; + result << "0x" << std::hex << std::setfill('0') + << std::setw(2) << static_cast(r) + << std::setw(2) << static_cast(g) + << std::setw(2) << static_cast(b) + << std::setw(2) << static_cast(a); return result.str(); } @@ -136,10 +141,10 @@ namespace Common { } const Color ColorConsts::white = Color(255, 255, 255, 255); - const Color ColorConsts::black = Color(0, 0, 0, 1); - const Color ColorConsts::red = Color(1, 0, 0, 1); - const Color ColorConsts::green = Color(0, 1, 0, 1); - const Color ColorConsts::blue = Color(0, 0, 1, 1); + const Color ColorConsts::black = Color(0, 0, 0, 255); + const Color ColorConsts::red = Color(255, 0, 0, 255); + const Color ColorConsts::green = Color(0, 255, 0, 255); + const Color ColorConsts::blue = Color(0, 0, 255, 255); const LinearColor LinearColorConsts::white = LinearColor(1.0f, 1.0f, 1.0f, 1.0f); const LinearColor LinearColorConsts::black = LinearColor(0.0f, 0.0f, 0.0f, 1.0f); diff --git a/Engine/Source/Common/Src/String.cpp b/Engine/Source/Common/Src/String.cpp index 07321173d..b0a82f8a5 100644 --- a/Engine/Source/Common/Src/String.cpp +++ b/Engine/Source/Common/Src/String.cpp @@ -2,6 +2,7 @@ // Created by johnk on 2024/4/14. // +#include #include #include #include diff --git a/Engine/Source/Common/Test/MathTest.cpp b/Engine/Source/Common/Test/MathTest.cpp index e745be6ae..d757df483 100644 --- a/Engine/Source/Common/Test/MathTest.cpp +++ b/Engine/Source/Common/Test/MathTest.cpp @@ -19,6 +19,8 @@ using namespace Common; +// ==================================== Common ==================================== + TEST(MathTest, CommonTest) { ASSERT_EQ(DivideAndRoundUp(7, 3), 3); @@ -26,6 +28,16 @@ TEST(MathTest, CommonTest) ASSERT_EQ(DivideAndRoundUp(70, 9), 8); } +TEST(MathTest, CompareNumberTest) +{ + ASSERT_TRUE(CompareNumber(1.0f, 1.0f + epsilon / 2.0f)); + ASSERT_FALSE(CompareNumber(1.0f, 1.1f)); + ASSERT_TRUE(CompareNumber(5, 5)); + ASSERT_FALSE(CompareNumber(5, 6)); +} + +// ==================================== Half ==================================== + TEST(MathTest, HFloatTest) // NOLINT { const HFloat v0 = 1.0f; @@ -54,6 +66,47 @@ TEST(MathTest, HFloatTest) // NOLINT ASSERT_TRUE(v5 == HFloat(std::sqrt(v6))); } +TEST(MathTest, HFloatComparisonTest) // NOLINT +{ + const HFloat a = 1.0f; + const HFloat b = 2.0f; + ASSERT_TRUE(a < b); + ASSERT_TRUE(b > a); + ASSERT_TRUE(a <= HFloat(1.0f)); + ASSERT_TRUE(a >= HFloat(1.0f)); + ASSERT_FALSE(a > b); + ASSERT_TRUE(a != b); + + ASSERT_TRUE((HFloat(1.0f) + HFloat(2.0f)) == 3.0f); + ASSERT_TRUE((HFloat(6.0f) - HFloat(2.0f)) == 4.0f); + ASSERT_TRUE((HFloat(2.0f) * HFloat(3.0f)) == 6.0f); + ASSERT_TRUE((HFloat(6.0f) / HFloat(2.0f)) == 3.0f); + + HFloat c = 1.0f; + c += HFloat(2.0f); + c -= 1.0f; + c *= HFloat(3.0f); + ASSERT_TRUE(c == 6.0f); + c /= 2.0f; + ASSERT_TRUE(c == 3.0f); +} + +TEST(MathTest, HFloatEdgeCaseTest) // NOLINT +{ + ASSERT_TRUE(HFloat(0.0f) == 0.0f); + ASSERT_TRUE(HFloat(-2.5f) == -2.5f); + ASSERT_TRUE(HFloat(-2.5f) < HFloat(0.0f)); + ASSERT_TRUE(HFloat(0.5f) == 0.5f); + ASSERT_TRUE(HFloat(0.25f) == 0.25f); + + // values beyond the half range are clamped to the largest representable finite half (~65504) + const float large = static_cast(HFloat(100000.0f)); + ASSERT_GT(large, 60000.0f); + ASSERT_LT(large, 70000.0f); +} + +// ==================================== Vector ==================================== + TEST(MathTest, FVec1Test) { const FVec1 v0; @@ -241,6 +294,25 @@ TEST(MathTest, HVec4Test) ASSERT_TRUE(v1 == HVec4(1.0f, 0.0f, -1.0f, -2.0f)); } +TEST(MathTest, VecScalarCompareTest) +{ + const FVec3 v0(2.0f); + ASSERT_TRUE(v0 == 2.0f); + ASSERT_TRUE(v0 != 3.0f); + ASSERT_FALSE(FVec3(1, 2, 3) == 1.0f); +} + +TEST(MathTest, VecCastToTest) +{ + const FVec3 v0(1.5f, 2.5f, 3.5f); + const IVec3 v1 = v0.CastTo(); + ASSERT_TRUE(v1 == IVec3(1, 2, 3)); + + const IVec2 v2(1, 2); + const FVec2 v3 = v2.CastTo(); + ASSERT_TRUE(v3 == FVec2(1.0f, 2.0f)); +} + TEST(MathTest, SubVecTest) { const auto v0 = FVec4(1, 2, 3, 4); @@ -301,6 +373,19 @@ TEST(MathTest, VecConstsTest) ASSERT_TRUE(IVec3(0, 0, 1) == IVec3Consts::unitZ); } +TEST(MathTest, VecConstsFullTest) +{ + ASSERT_TRUE(FVec4Consts::negaUnitZ == FVec4(0, 0, -1, 0)); + ASSERT_TRUE(FVec4Consts::unitW == FVec4(0, 0, 0, 1)); + ASSERT_TRUE(FVec4Consts::negaUnit == FVec4(-1, -1, -1, -1)); + ASSERT_TRUE(FVec3Consts::unit == FVec3(1, 1, 1)); + ASSERT_TRUE(FVec2Consts::negaUnitX == FVec2(-1, 0)); + ASSERT_TRUE(IVec1Consts::negaUnit == IVec1(-1)); + ASSERT_TRUE(IVec1Consts::unit == IVec1(1)); +} + +// ==================================== Matrix ==================================== + TEST(MathTest, IMat2x3Test) { IMat2x3 v0(1, 2, 3, 4, 5, 6); @@ -351,22 +436,6 @@ TEST(MathTest, SubMatrixTest) ASSERT_TRUE(m1.Row(2) == FVec3(7, 8, 9)); } -TEST(MathTest, MatExtractionTest) -{ - const FMat4x4 m = { - 0, -2, 0, 7, - 4, 0, 0, 5, - 0, 0, 3, 3, - 7, 5, 3, 1 - }; - - const FTransform trans = FTransform(m); - - ASSERT_TRUE(trans.translation == FVec3(7.0f, 5.0f, 3.0f)); - ASSERT_TRUE(trans.scale == FVec3(4.0f, 2.0f, 3.0f)); - ASSERT_TRUE(trans.rotation == FQuat(0.7071067f, .0f, .0f, .7071067f)); -} - TEST(MathTest, MatViewTest) { const FMat3x3 v0( @@ -427,6 +496,27 @@ TEST(MathTest, MatSetTest) // NOLINT ASSERT_TRUE(v0.Row(1) == FVec4(6, 7, 8, 9)); } +TEST(MathTest, MatFromVecsTest) +{ + const FMat2x3 m0 = FMat2x3::FromRowVecs(FVec3(1, 2, 3), FVec3(4, 5, 6)); + ASSERT_TRUE(m0.Row(0) == FVec3(1, 2, 3)); + ASSERT_TRUE(m0.Row(1) == FVec3(4, 5, 6)); + + const FMat2x3 m1 = FMat2x3::FromColVecs(FVec2(1, 4), FVec2(2, 5), FVec2(3, 6)); + ASSERT_TRUE(m1.Col(0) == FVec2(1, 4)); + ASSERT_TRUE(m1 == m0); +} + +TEST(MathTest, MatCastToTest) +{ + const FMat2x2 m0(1.5f, 2.5f, 3.5f, 4.5f); + const IMat2x2 m1 = m0.CastTo(); + ASSERT_EQ(m1[0], 1); + ASSERT_EQ(m1[1], 2); + ASSERT_EQ(m1[2], 3); + ASSERT_EQ(m1[3], 4); +} + TEST(MathTest, MatMulTest) { const FMat3x4 m0( @@ -508,6 +598,55 @@ TEST(MathTest, MatrixDetInverseTest) ASSERT_TRUE(im2.Col(0) == col2); } +TEST(MathTest, MatCanInverseTest) +{ + const FMat2x2 m0(1, 2, 3, 4); + ASSERT_TRUE(m0.CanInverse()); + + const FMat2x2 m1(1, 2, 2, 4); + ASSERT_FALSE(m1.CanInverse()); +} + +TEST(MathTest, MatScalarArithmeticTest) +{ + FMat2x2 m0(1, 2, 3, 4); + m0 += 1.0f; + ASSERT_TRUE(m0 == FMat2x2(2, 3, 4, 5)); + m0 -= 1.0f; + ASSERT_TRUE(m0 == FMat2x2(1, 2, 3, 4)); + m0 *= 2.0f; + ASSERT_TRUE(m0 == FMat2x2(2, 4, 6, 8)); + m0 /= 2.0f; + ASSERT_TRUE(m0 == FMat2x2(1, 2, 3, 4)); + + const FMat2x2 a(1, 2, 3, 4); + const FMat2x2 b(4, 3, 2, 1); + ASSERT_TRUE(a != b); + ASSERT_TRUE(a != 0.0f); + ASSERT_TRUE((a + b) == FMat2x2(5, 5, 5, 5)); + ASSERT_TRUE((b - a) == FMat2x2(3, 1, -1, -3)); + ASSERT_TRUE((a * 2.0f) == FMat2x2(2, 4, 6, 8)); + ASSERT_TRUE((a / 2.0f) == FMat2x2(0.5f, 1.0f, 1.5f, 2.0f)); +} + +TEST(MathTest, MatExtractionTest) +{ + const FMat4x4 m = { + 0, -2, 0, 7, + 4, 0, 0, 5, + 0, 0, 3, 3, + 7, 5, 3, 1 + }; + + const FTransform trans = FTransform(m); + + ASSERT_TRUE(trans.translation == FVec3(7.0f, 5.0f, 3.0f)); + ASSERT_TRUE(trans.scale == FVec3(4.0f, 2.0f, 3.0f)); + ASSERT_TRUE(trans.rotation == FQuat(0.7071067f, .0f, .0f, .7071067f)); +} + +// ==================================== Quaternion ==================================== + TEST(MathTest, AngleAndRadianTest) { const FAngle v0(90.0f); @@ -517,6 +656,20 @@ TEST(MathTest, AngleAndRadianTest) ASSERT_TRUE(v1.ToAngle() == 45.0f); } +TEST(MathTest, AngleRadianConvertTest) +{ + const FAngle a0(90.0f); + const FRadian r0(a0); + ASSERT_TRUE(r0 == FRadian(pi / 2.0f)); + + const FRadian r1(pi); + const FAngle a1(r1); + ASSERT_TRUE(a1 == FAngle(180.0f)); + + ASSERT_TRUE(FAngle(45.0f) == FAngle(45.0f)); + ASSERT_FALSE(FRadian(1.0f) == FRadian(2.0f)); +} + TEST(MathTest, QuaternionBasicTest) { const FQuat v0(1, 2, 3, 4); @@ -525,6 +678,46 @@ TEST(MathTest, QuaternionBasicTest) ASSERT_TRUE((v0 * 2 + v1 - v2) == FQuat(3, 6, 8, 11)); } +TEST(MathTest, QuaternionArithmeticTest) +{ + FQuat v0(1, 2, 3, 4); + v0 += FQuat(1, 1, 1, 1); + ASSERT_TRUE(v0 == FQuat(2, 3, 4, 5)); + v0 -= FQuat(1, 1, 1, 1); + ASSERT_TRUE(v0 == FQuat(1, 2, 3, 4)); + v0 *= 2.0f; + ASSERT_TRUE(v0 == FQuat(2, 4, 6, 8)); + v0 /= 2.0f; + ASSERT_TRUE(v0 == FQuat(1, 2, 3, 4)); + + ASSERT_TRUE((FQuat(2, 4, 6, 8) / 2.0f) == FQuat(1, 2, 3, 4)); + + FQuat v1(1, 2, 3, 4); + v1 *= FQuatConsts::identity; + ASSERT_TRUE(v1 == FQuat(1, 2, 3, 4)); + + const FQuat r0(FVec3Consts::unitZ, 90); + const FQuat r1(FVec3Consts::unitY, 45); + FQuat r2 = r0; + r2 *= r1; + ASSERT_TRUE(r2 == (r0 * r1)); +} + +TEST(MathTest, QuaternionPropertiesTest) +{ + const FQuat v0(1, 2, 3, 4); + ASSERT_TRUE(v0.ImaginaryPart() == FVec3(2, 3, 4)); + ASSERT_FLOAT_EQ(v0.Model(), std::sqrt(30.0f)); + ASSERT_TRUE(v0.Negatived() == FQuat(-1, -2, -3, -4)); + ASSERT_TRUE(v0.Conjugated() == FQuat(1, -2, -3, -4)); + + const FQuat v1(2, 0, 0, 0); + ASSERT_TRUE(v1.Normalized() == FQuat(1, 0, 0, 0)); + + const FQuat v2(2, 3, 4, 5); + ASSERT_FLOAT_EQ(v0.Dot(v2), 1.0f * 2 + 2 * 3 + 3 * 4 + 4 * 5); +} + TEST(MathTest, QuaternionRotationTest) { const FQuat v0(FVec3(0, 0, 1), 90); @@ -538,6 +731,17 @@ TEST(MathTest, QuaternionRotationTest) ASSERT_TRUE(v1r0 == FVec3(0, 0, -1)); } +TEST(MathTest, QuaternionRadianTest) +{ + const FQuat v0(FVec3Consts::unitZ, FRadian(pi / 2.0f)); + const FQuat v1(FVec3Consts::unitZ, 90.0f); + ASSERT_TRUE(v0 == v1); + + const FQuat v2 = FQuat::FromEulerZYX(FRadian(0.0f), FRadian(0.0f), FRadian(pi / 2.0f)); + const FQuat v3 = FQuat::FromEulerZYX(0.0f, 0.0f, 90.0f); + ASSERT_TRUE(v2 == v3); +} + TEST(MathTest, EulerRotationTest) { const FQuat v0 = FQuat::FromEulerZYX(0, 0, 90); @@ -566,6 +770,21 @@ TEST(MathTest, QuaternionToRotationMatrixTest) ASSERT_TRUE(v1r0 == FVec3(-1, 0, 0)); } +TEST(MathTest, QuatConstsTest) +{ + ASSERT_TRUE(FQuatConsts::zero == FQuat(0, 0, 0, 0)); + ASSERT_TRUE(FQuatConsts::identity == FQuat(1, 0, 0, 0)); +} + +TEST(MathTest, QuaternionCastToTest) +{ + const FQuat v0(1.0f, 2.0f, 3.0f, 4.0f); + const DQuat v1 = v0.CastTo(); + ASSERT_TRUE(v1 == DQuat(1.0, 2.0, 3.0, 4.0)); +} + +// ==================================== Transform ==================================== + TEST(MathTest, TransformTest) { FVec3 x(1, 0, 0); @@ -591,6 +810,90 @@ TEST(MathTest, TransformTest) } +TEST(MathTest, TransformOperatorsTest) +{ + const FTransform base; + const FQuat rot(FVec3Consts::unitZ, 90); + + const FTransform v0 = base + FVec3(1, 2, 3); + ASSERT_TRUE(v0.translation == FVec3(1, 2, 3)); + ASSERT_TRUE(v0.scale == FVec3(1, 1, 1)); + + const FTransform v1 = base * FVec3(2, 3, 4); + ASSERT_TRUE(v1.scale == FVec3(2, 3, 4)); + + const FTransform v2 = base | rot; + ASSERT_TRUE(v2.rotation == rot); + + FTransform v3 = base; + v3 += FVec3(1, 1, 1); + v3 *= FVec3(2, 2, 2); + v3 |= rot; + ASSERT_TRUE(v3.translation == FVec3(1, 1, 1)); + ASSERT_TRUE(v3.scale == FVec3(2, 2, 2)); + ASSERT_TRUE(v3.rotation == rot); + + FTransform v4 = base; + v4.Translate(FVec3(5, 0, 0)).Scale(FVec3(2, 2, 2)); + ASSERT_TRUE(v4.translation == FVec3(5, 0, 0)); + ASSERT_TRUE(v4.scale == FVec3(2, 2, 2)); +} + +TEST(MathTest, TransformMatrixTest) +{ + const FTransform v0(FVec3(2, 3, 4), FQuatConsts::identity, FVec3(5, 6, 7)); + + const FMat4x4 tm = v0.GetTranslationMatrix(); + ASSERT_TRUE(tm.Col(3) == FVec4(5, 6, 7, 1)); + + const FMat4x4 sm = v0.GetScaleMatrix(); + ASSERT_FLOAT_EQ(sm.At(0, 0), 2.0f); + ASSERT_FLOAT_EQ(sm.At(1, 1), 3.0f); + ASSERT_FLOAT_EQ(sm.At(2, 2), 4.0f); + + ASSERT_TRUE(v0.GetRotationMatrix() == FMat4x4Consts::identity); + + const FVec3 p0 = v0.TransformPosition(FVec3(1, 1, 1)); + ASSERT_TRUE(p0 == FVec3(7, 9, 11)); + + const FVec4 p1 = (v0.GetTransformMatrixNoScale() * FVec4(1, 1, 1, 1)); + const FVec3 p1Xyz = p1.SubVec<0, 1, 2>(); + ASSERT_TRUE(p1Xyz == FVec3(6, 7, 8)); +} + +TEST(MathTest, TransformPositionVec4Test) +{ + const FTransform v0(FVec3(2, 2, 2), FQuatConsts::identity, FVec3(1, 1, 1)); + const FVec4 p0 = v0.TransformPosition(FVec4(1, 1, 1, 1)); + ASSERT_TRUE(p0 == FVec4(3, 3, 3, 1)); +} + +TEST(MathTest, TransformLookAtTest) +{ + FTransform v0; + v0.MoveAndLookTo(FVec3(1, 2, 3), FVec3(4, 2, 3)); + ASSERT_TRUE(v0.translation == FVec3(1, 2, 3)); + + const FTransform v1 = FTransform::LookAt(FVec3(1, 2, 3), FVec3(4, 2, 3)); + ASSERT_TRUE(v0 == v1); + + FTransform v2(FQuatConsts::identity, FVec3(0, 0, 0)); + v2.LookTo(FVec3(1, 0, 0)); + ASSERT_TRUE(v2.translation == FVec3(0, 0, 0)); + ASSERT_TRUE(v2.rotation == FQuat(0.5f, 0.5f, 0.5f, 0.5f)); +} + +TEST(MathTest, TransformCastToTest) +{ + const FTransform v0(FVec3(2, 3, 4), FQuat(1, 0, 0, 0), FVec3(5, 6, 7)); + const DTransform v1 = v0.CastTo(); + ASSERT_TRUE(v1.scale == DVec3(2, 3, 4)); + ASSERT_TRUE(v1.translation == DVec3(5, 6, 7)); + ASSERT_TRUE(v1.rotation == DQuat(1, 0, 0, 0)); +} + +// ==================================== Rect ==================================== + TEST(MathTest, RectTest) { const FRect rect0(0.0f, 0.0f, 2.0f, 1.0f); @@ -604,6 +907,29 @@ TEST(MathTest, RectTest) ASSERT_TRUE(rect0.Distance(rect2) == 2.0f); } +TEST(MathTest, RectGeometryTest) +{ + const FRect v0 = FRect::FromMinExtent(1.0f, 2.0f, 4.0f, 6.0f); + ASSERT_TRUE(v0.min == FVec2(1, 2)); + ASSERT_TRUE(v0.max == FVec2(5, 8)); + ASSERT_TRUE(v0.Extent() == FVec2(4, 6)); + ASSERT_FLOAT_EQ(v0.ExtentX(), 4.0f); + ASSERT_FLOAT_EQ(v0.ExtentY(), 6.0f); + ASSERT_TRUE(v0.Center() == FVec2(3, 5)); + ASSERT_FLOAT_EQ(v0.CenterX(), 3.0f); + ASSERT_FLOAT_EQ(v0.CenterY(), 5.0f); + ASSERT_FLOAT_EQ(v0.Size(), std::sqrt(4.0f * 4 + 6 * 6) / 2.0f); + + const FRect v1 = FRect::FromMinExtent(FVec2(0, 0), FVec2(2, 2)); + ASSERT_TRUE(v1.max == FVec2(2, 2)); + + const IRect v2 = v0.CastTo(); + ASSERT_TRUE(v2.min == IVec2(1, 2)); + ASSERT_TRUE(v2.max == IVec2(5, 8)); +} + +// ==================================== Box ==================================== + TEST(MathTest, BoxTest) { const FBox box0(FVec3(0.0f), FVec3(1.0f)); @@ -616,6 +942,35 @@ TEST(MathTest, BoxTest) ASSERT_TRUE(!box0.Intersect(box2)); } +TEST(MathTest, BoxGeometryTest) +{ + const FBox v0 = FBox::FromMinExtent(1.0f, 2.0f, 3.0f, 2.0f, 4.0f, 6.0f); + ASSERT_TRUE(v0.min == FVec3(1, 2, 3)); + ASSERT_TRUE(v0.max == FVec3(3, 6, 9)); + ASSERT_TRUE(v0.Extent() == FVec3(2, 4, 6)); + ASSERT_FLOAT_EQ(v0.ExtentX(), 2.0f); + ASSERT_FLOAT_EQ(v0.ExtentY(), 4.0f); + ASSERT_FLOAT_EQ(v0.ExtentZ(), 6.0f); + ASSERT_TRUE(v0.Center() == FVec3(2, 4, 6)); + ASSERT_FLOAT_EQ(v0.CenterX(), 2.0f); + ASSERT_FLOAT_EQ(v0.CenterY(), 4.0f); + ASSERT_FLOAT_EQ(v0.CenterZ(), 6.0f); + ASSERT_FLOAT_EQ(v0.Size(), std::sqrt(2.0f * 2 + 4 * 4 + 6 * 6) / 2.0f); + + const FBox v1(FVec3(0), FVec3(2)); + const FBox v2(FVec3(10), FVec3(12)); + ASSERT_FLOAT_EQ(v1.Distance(v2), std::sqrt(300.0f)); + + const FBox v3 = FBox::FromMinExtent(FVec3(0), FVec3(5)); + ASSERT_TRUE(v3.max == FVec3(5)); + + const IBox v4 = v0.CastTo(); + ASSERT_TRUE(v4.min == IVec3(1, 2, 3)); + ASSERT_TRUE(v4.max == IVec3(3, 6, 9)); +} + +// ==================================== Sphere ==================================== + TEST(MathTest, SphereTest) { const FSphere sphere0(FVec3(0.0f), 1.0f); @@ -629,6 +984,106 @@ TEST(MathTest, SphereTest) ASSERT_TRUE(sphere0.Distance(sphere1) == 0.5f); } +TEST(MathTest, SphereGeometryTest) +{ + const FSphere v0(FVec3(0), 1.0f); + const FSphere v1(FVec3(3, 0, 0), 2.0f); + ASSERT_FLOAT_EQ(v0.Distance(v1), 3.0f); + ASSERT_FALSE(v0.Inside(FVec3(2, 0, 0))); + ASSERT_TRUE(v0.Inside(FVec3(0.5f, 0, 0))); + + const FSphere v2(FVec3(1, 2, 3), 1.0f); + const DSphere v3 = v2.CastTo(); + ASSERT_TRUE(v3.center == DVec3(1, 2, 3)); + ASSERT_TRUE(CompareNumber(v3.radius, 1.0)); +} + +// ==================================== Color ==================================== + +TEST(MathTest, ColorConversionTest) +{ + const Color v0(255, 128, 0, 255); + const LinearColor v1 = v0.ToLinearColor(); + ASSERT_TRUE(v1 == LinearColor(1.0f, 128.0f / 255.0f, 0.0f, 1.0f)); + ASSERT_TRUE(v1.ToColor() == v0); + + ASSERT_TRUE(Color(LinearColor(1.0f, 0.0f, 0.0f, 1.0f)) == Color(255, 0, 0, 255)); + ASSERT_TRUE(LinearColor(Color(0, 255, 0, 255)) == LinearColor(0.0f, 1.0f, 0.0f, 1.0f)); + + ASSERT_EQ(Color(255, 128, 0, 255).ToHexString(), "0xff8000ff"); + ASSERT_EQ(Color(0, 0, 0, 0).ToHexString(), "0x00000000"); + + ASSERT_TRUE(ColorConsts::white == Color(255, 255, 255, 255)); + ASSERT_TRUE(ColorConsts::black == Color(0, 0, 0, 255)); + ASSERT_TRUE(ColorConsts::red == Color(255, 0, 0, 255)); + ASSERT_TRUE(ColorConsts::green == Color(0, 255, 0, 255)); + ASSERT_TRUE(ColorConsts::blue == Color(0, 0, 255, 255)); + ASSERT_TRUE(LinearColorConsts::white == LinearColor(1.0f, 1.0f, 1.0f, 1.0f)); + ASSERT_TRUE(LinearColorConsts::black == LinearColor(0.0f, 0.0f, 0.0f, 1.0f)); +} + +// ==================================== View ==================================== + +TEST(MathTest, ViewMatrixTest) +{ + const FViewTransform v0; + const FMat4x4 vm0 = v0.GetViewMatrix(); + ASSERT_TRUE((vm0 * FVec4(2, 3, 5, 1)) == FVec4(3, 5, 2, 1)); + + const FViewTransform v1 = FViewTransform::LookAt(FVec3(3, 4, 5), FVec3(0, 0, 0), FVec3Consts::unitZ); + const FMat4x4 vm1 = v1.GetViewMatrix(); + + const FVec4 camInView = vm1 * FVec4(3, 4, 5, 1); + ASSERT_NEAR(camInView.x, 0.0f, 1e-4f); + ASSERT_NEAR(camInView.y, 0.0f, 1e-4f); + ASSERT_NEAR(camInView.z, 0.0f, 1e-4f); + + const FVec4 targetInView = vm1 * FVec4(0, 0, 0, 1); + const FVec3 targetXyz = targetInView.SubVec<0, 1, 2>(); + ASSERT_NEAR(targetXyz.Model(), std::sqrt(50.0f), 1e-4f); +} + +// ==================================== Projection ==================================== + +TEST(MathTest, OrthoProjectionMatrixTest) +{ + const FReversedZOrthoProjection v0(4.0f, 2.0f, 1.0f, 11.0f); + const FMat4x4 m0 = v0.GetProjectionMatrix(); + ASSERT_NEAR((m0 * FVec4(0, 0, 1.0f, 1)).z, 1.0f, 1e-4f); + ASSERT_NEAR((m0 * FVec4(0, 0, 11.0f, 1)).z, 0.0f, 1e-4f); + ASSERT_NEAR((m0 * FVec4(2.0f, 0, 1.0f, 1)).x, 1.0f, 1e-4f); + ASSERT_NEAR((m0 * FVec4(0, 1.0f, 1.0f, 1)).y, 1.0f, 1e-4f); + + const FReversedZOrthoProjection v1(4.0f, 2.0f, 1.0f); + const FMat4x4 m1 = v1.GetProjectionMatrix(); + ASSERT_NEAR((m1 * FVec4(0, 0, 1.0f, 1)).z, 1.0f, 1e-4f); + + ASSERT_TRUE(v0 == FReversedZOrthoProjection(4.0f, 2.0f, 1.0f, 11.0f)); + ASSERT_FALSE(v0 == v1); +} + +TEST(MathTest, PerspectiveProjectionMatrixTest) +{ + const FReversedZPerspectiveProjection v0(90.0f, 2.0f, 2.0f, 1.0f, 11.0f); + const FMat4x4 m0 = v0.GetProjectionMatrix(); + ASSERT_NEAR(m0.At(1, 1), 1.0f, 1e-4f); + + const FVec4 nearClip = m0 * FVec4(0, 0, 1.0f, 1); + ASSERT_NEAR(nearClip.z / nearClip.w, 1.0f, 1e-4f); + const FVec4 farClip = m0 * FVec4(0, 0, 11.0f, 1); + ASSERT_NEAR(farClip.z / farClip.w, 0.0f, 1e-4f); + + const FReversedZPerspectiveProjection v1(90.0f, 2.0f, 2.0f, 1.0f, std::nullopt); + const FMat4x4 m1 = v1.GetProjectionMatrix(); + const FVec4 nearClipInf = m1 * FVec4(0, 0, 1.0f, 1); + ASSERT_NEAR(nearClipInf.z / nearClipInf.w, 1.0f, 1e-4f); + + ASSERT_TRUE(v0 == FReversedZPerspectiveProjection(90.0f, 2.0f, 2.0f, 1.0f, 11.0f)); + ASSERT_FALSE(v0 == FReversedZPerspectiveProjection(60.0f, 2.0f, 2.0f, 1.0f, 11.0f)); +} + +// ==================================== Serialization / String / Json ==================================== + TEST(MathTest, SerializationTest) { // half diff --git a/Engine/Source/Runtime/Include/Runtime/ECS.h b/Engine/Source/Runtime/Include/Runtime/ECS.h index 776a01469..542fbb494 100644 --- a/Engine/Source/Runtime/Include/Runtime/ECS.h +++ b/Engine/Source/Runtime/Include/Runtime/ECS.h @@ -499,7 +499,7 @@ namespace Runtime { Runtime::RuntimeView RuntimeView(const RuntimeFilter& inFilter); Runtime::ConstRuntimeView RuntimeView(const RuntimeFilter& inFilter) const; Runtime::ConstRuntimeView ConstRuntimeView(const RuntimeFilter& inFilter) const; - EventsObserverDyn EventsObserverDyn(CompClass inClass); + Runtime::EventsObserverDyn EventsObserverDyn(CompClass inClass); // global component static template G& GEmplace(Args&&... inArgs); @@ -530,7 +530,7 @@ namespace Runtime { size_t GCompCount() const; // comp observer - Observer Observer(); + Runtime::Observer Observer(); // serialization void Save(ECArchive& outArchive) const; diff --git a/Engine/Source/Runtime/Include/Runtime/System/Scene.h b/Engine/Source/Runtime/Include/Runtime/System/Scene.h index 5bf203d3d..8527705de 100644 --- a/Engine/Source/Runtime/Include/Runtime/System/Scene.h +++ b/Engine/Source/Runtime/Include/Runtime/System/Scene.h @@ -65,7 +65,7 @@ namespace Runtime::Internal { namespace Runtime::Internal { template <> - static void UpdateSceneProxyContent(Render::LightSceneProxy& outSceneProxy, const DirectionalLight& inComponent) + inline void UpdateSceneProxyContent(Render::LightSceneProxy& outSceneProxy, const DirectionalLight& inComponent) { outSceneProxy.type = Render::LightType::directional; outSceneProxy.color = inComponent.color; @@ -73,7 +73,7 @@ namespace Runtime::Internal { } template <> - static void UpdateSceneProxyContent(Render::LightSceneProxy& outSceneProxy, const PointLight& inComponent) + inline void UpdateSceneProxyContent(Render::LightSceneProxy& outSceneProxy, const PointLight& inComponent) { outSceneProxy.type = Render::LightType::point; outSceneProxy.color = inComponent.color; @@ -82,7 +82,7 @@ namespace Runtime::Internal { } template <> - static void UpdateSceneProxyContent(Render::LightSceneProxy& outSceneProxy, const SpotLight& inComponent) + inline void UpdateSceneProxyContent(Render::LightSceneProxy& outSceneProxy, const SpotLight& inComponent) { outSceneProxy.type = Render::LightType::spot; outSceneProxy.color = inComponent.color; diff --git a/Engine/Source/Runtime/Include/Runtime/World.h b/Engine/Source/Runtime/Include/Runtime/World.h index 6162fadf0..b229f5e24 100644 --- a/Engine/Source/Runtime/Include/Runtime/World.h +++ b/Engine/Source/Runtime/Include/Runtime/World.h @@ -28,7 +28,7 @@ namespace Runtime { World(std::string inName, Client* inClient, PlayType inPlayType); void SetSystemGraph(const SystemGraph& inSystemGraph); - PlayStatus PlayStatus() const; + Runtime::PlayStatus PlayStatus() const; bool Stopped() const; bool Playing() const; bool Paused() const; diff --git a/Engine/Source/Runtime/Src/ECS.cpp b/Engine/Source/Runtime/Src/ECS.cpp index 7335ee8c5..55a4afac5 100644 --- a/Engine/Source/Runtime/Src/ECS.cpp +++ b/Engine/Source/Runtime/Src/ECS.cpp @@ -763,7 +763,7 @@ namespace Runtime { return Runtime::ConstRuntimeView { *this, inFilter }; } - EventsObserverDyn ECRegistry::EventsObserverDyn(CompClass inClass) + Runtime::EventsObserverDyn ECRegistry::EventsObserverDyn(CompClass inClass) { return Runtime::EventsObserverDyn { *this, inClass }; } @@ -795,7 +795,7 @@ namespace Runtime { iter->second.onRemove.Broadcast(*this, inEntity); } - Observer ECRegistry::Observer() + Runtime::Observer ECRegistry::Observer() { return Runtime::Observer { *this }; } diff --git a/Engine/Source/Runtime/Src/World.cpp b/Engine/Source/Runtime/Src/World.cpp index d8ef17039..9fa201ae0 100644 --- a/Engine/Source/Runtime/Src/World.cpp +++ b/Engine/Source/Runtime/Src/World.cpp @@ -29,7 +29,7 @@ namespace Runtime { systemGraph = inSystemGraph; } - PlayStatus World::PlayStatus() const + Runtime::PlayStatus World::PlayStatus() const { return playStatus; } diff --git a/ThirdParty/ConanRecipes/README.md b/ThirdParty/ConanRecipes/README.md index 74c331d8f..084b79c76 100644 --- a/ThirdParty/ConanRecipes/README.md +++ b/ThirdParty/ConanRecipes/README.md @@ -30,8 +30,8 @@ To build every recipe at once, use the `build_recipes.py` helper. It walks each recipe directory, picks the latest version (the top-most entry in `conandata.yml`) and builds them one-by-one in dependency order. Each recipe lists the platforms it supports under a `platforms` key in its `conandata.yml` -(currently `Windows-x86_64` and/or `Macos-armv8`); recipes that do not target -the current host are skipped. If any recipe fails the script stops immediately +(currently `Windows-x86_64`, `Macos-armv8` and/or `Linux-x86_64`); recipes that +do not target the current host are skipped. If any recipe fails the script stops immediately and prints a summary. ```shell @@ -52,6 +52,13 @@ python build_recipes.py --upload \ python build_recipes.py --export-only ``` +## Windows notes + +Some recipes build code that needs ATL (e.g. `atlbase.h`), which a stock Visual +Studio C++ installation does not ship. If a build fails with a missing-ATL +error, open the Visual Studio Installer and add the **C++ ATL** component +(Modify -> Individual components -> "C++ ATL for latest build tools"). + ## CI Recipe changes are validated and published automatically: @@ -63,8 +70,8 @@ Recipe changes are validated and published automatically: the job. A PR can therefore change recipes and engine code together and be validated atomically. - After a push to `master` that touches `ThirdParty/ConanRecipes`, the - `Publish Conan Recipes` workflow builds the changed recipes on Windows - and macOS and uploads them to the remote (credentials come from the + `Publish Conan Recipes` workflow builds the changed recipes on Windows, + macOS and Linux and uploads them to the remote (credentials come from the `CONAN_REMOTE_USER` / `CONAN_REMOTE_PASSWORD` repository secrets). It can also be re-run manually via `workflow_dispatch` if an upload failed. diff --git a/ThirdParty/ConanRecipes/assimp/conandata.yml b/ThirdParty/ConanRecipes/assimp/conandata.yml index 535c0fad9..1e15c3580 100644 --- a/ThirdParty/ConanRecipes/assimp/conandata.yml +++ b/ThirdParty/ConanRecipes/assimp/conandata.yml @@ -1,6 +1,7 @@ platforms: - Windows-x86_64 - Macos-armv8 + - Linux-x86_64 sources: "6.0.2-exp": url: "https://github.com/assimp/assimp/archive/refs/tags/v6.0.2.tar.gz" diff --git a/ThirdParty/ConanRecipes/assimp/conanfile.py b/ThirdParty/ConanRecipes/assimp/conanfile.py index a48a9206f..8f8ed35e2 100644 --- a/ThirdParty/ConanRecipes/assimp/conanfile.py +++ b/ThirdParty/ConanRecipes/assimp/conanfile.py @@ -24,7 +24,7 @@ def validate(self): check_min_cppstd(self, 17) def requirements(self): - self.requires("zlib/1.3.1") + self.requires("zlib/[>=1.3.1 <2]") def build_requirements(self): self.tool_requires("ninja/[>=1.12]") diff --git a/ThirdParty/ConanRecipes/build_recipes.py b/ThirdParty/ConanRecipes/build_recipes.py index 46296bc85..a5f433ee9 100644 --- a/ThirdParty/ConanRecipes/build_recipes.py +++ b/ThirdParty/ConanRecipes/build_recipes.py @@ -10,11 +10,19 @@ every recipe, so a subsequent 'conan install --build=missing' resolves recipes from the local cache and builds whatever the remote has no binaries for. CI uses this to validate recipe changes together with the engine build. + +Before building each recipe the script exports it, resolves the package id for the +current profile, then asks both the local cache and the remote whether that exact +recipe-revision + package-id binary already exists. A recipe found on the remote is +skipped with no download, rebuild, or no-op re-upload; one found only in the local +cache skips the rebuild but is still uploaded so the local binary reaches the remote. +Pass --no-skip-existing to force every supported recipe through 'conan create'. """ from __future__ import annotations import argparse +import json import platform import re import subprocess @@ -25,7 +33,7 @@ import yaml -SUPPORTED_PLATFORMS = ("Windows-x86_64", "Macos-armv8") +SUPPORTED_PLATFORMS = ("Windows-x86_64", "Macos-armv8", "Linux-x86_64") def current_platform() -> str: @@ -35,6 +43,8 @@ def current_platform() -> str: return "Windows-x86_64" if system == "Darwin" and machine in ("arm64", "aarch64"): return "Macos-armv8" + if system == "Linux" and machine in ("amd64", "x86_64"): + return "Linux-x86_64" sys.exit( f"error: unsupported host {system}/{platform.machine()}; " f"only {', '.join(SUPPORTED_PLATFORMS)} are supported" @@ -116,6 +126,85 @@ def run(cmd: list[str], cwd: Path | None = None, redact: set[str] | None = None) return subprocess.run(cmd, cwd=str(cwd) if cwd else None).returncode +def run_capture(cmd: list[str], cwd: Path | None = None) -> tuple[int, str]: + print(f"\n$ {' '.join(cmd)}", flush=True) + proc = subprocess.run(cmd, cwd=str(cwd) if cwd else None, stdout=subprocess.PIPE, text=True) + return proc.returncode, proc.stdout + + +def graph_recipe_node(graph_json: str, reference: str) -> dict | None: + try: + nodes = json.loads(graph_json).get("graph", {}).get("nodes", {}) + except (json.JSONDecodeError, AttributeError): + return None + for node in nodes.values(): + if (node.get("ref") or "").split("#", 1)[0] == reference: + return node + return None + + +def list_has_binary(list_json: str, reference: str, package_id: str) -> bool: + # 'conan list' reports each scope (the local cache or a remote) as either an {"error": ...} payload + # when the reference is absent, or a revisions -> packages tree when it is present; the binary exists + # there only if our package id shows up under some revision of that tree. + name_version = reference.split("#", 1)[0] + try: + data = json.loads(list_json) + except json.JSONDecodeError: + return False + for scope in data.values(): + if not isinstance(scope, dict) or "error" in scope: + continue + revisions = (scope.get(name_version) or {}).get("revisions") or {} + for revision in revisions.values(): + if package_id in (revision.get("packages") or {}): + return True + return False + + +def conan_list_has(args: argparse.Namespace, reference: str, package_id: str, remote: str | None) -> bool: + cmd = [args.conan, "list", f"{reference}:{package_id}", "--format=json"] + if remote: + cmd += ["-r", remote] + code, out = run_capture(cmd, cwd=args.recipes_root) + if code != 0: + return False + return list_has_binary(out, reference, package_id) + + +def package_status(args: argparse.Namespace, recipe: Recipe, create_extra: list[str]) -> str: + # Export so the local recipe revision (a hash of the recipe's contents) lands in the cache, then + # resolve the package id for the current profile via 'conan graph info'. With both in hand, query the + # local cache and (when a remote is configured) the remote for that exact recipe-revision + + # package-id binary -- 'conan graph info' alone cannot tell the two apart, since a cache hit masks the + # remote. Returns "remote" if published, "local" if only cached locally, else "build". Any + # uncertainty -- a failed export/query, unparseable output, a missing id -- yields "build", so the + # pre-check only ever short-circuits a sure hit, never a guess. + export = [args.conan, "export", f"{recipe.dir_name}/conanfile.py", "--version", recipe.version] + if run(export, cwd=args.recipes_root) != 0: + return "build" + + info = [args.conan, "graph", "info", f"--requires={recipe.reference}", "--format=json"] + if args.remote: + info += ["-r", args.remote] + info += create_extra + code, out = run_capture(info, cwd=args.recipes_root) + if code != 0: + return "build" + + node = graph_recipe_node(out, recipe.reference) + reference = (node or {}).get("ref") or "" + package_id = (node or {}).get("package_id") + if "#" not in reference or not package_id: + return "build" + + if args.remote and conan_list_has(args, reference, package_id, args.remote): + return "remote" + if conan_list_has(args, reference, package_id, None): + return "local" + return "build" + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Build and optionally upload all Conan recipes.", @@ -155,6 +244,12 @@ def parse_args() -> argparse.Namespace: action="store_true", help="only 'conan export' every version of every recipe; no build, no platform filter", ) + parser.add_argument( + "--skip-existing", + action=argparse.BooleanOptionalAction, + default=True, + help="before building, query the remote and skip any recipe whose binary is already published", + ) upload = parser.add_argument_group("upload") upload.add_argument( @@ -193,6 +288,8 @@ def export_all(args: argparse.Namespace, recipes: list[Recipe]): def build_all(args: argparse.Namespace, recipes: list[Recipe], host: str): built: list[Recipe] = [] skipped: list[tuple[Recipe, str]] = [] + local: list[Recipe] = [] + present: list[Recipe] = [] create_extra: list[str] = [] for profile in args.profile: @@ -202,7 +299,7 @@ def build_all(args: argparse.Namespace, recipes: list[Recipe], host: str): for index, recipe in enumerate(recipes, start=1): header = f"[{index}/{len(recipes)}] {recipe.name}" if recipe.version is None: - fail(args, built, skipped, recipe, + fail(args, built, skipped, local, present, recipe, f"could not determine latest version from {recipe.conandata}") if not recipe.supports(host): reason = f"not built on {host} (platforms: {recipe.platforms or 'none'})" @@ -210,6 +307,18 @@ def build_all(args: argparse.Namespace, recipes: list[Recipe], host: str): skipped.append((recipe, reason)) continue + if args.skip_existing: + status = package_status(args, recipe, create_extra) + if status == "remote": + print(f"\n=== {header} -- {recipe.reference} already on remote, skipping ===", flush=True) + present.append(recipe) + continue + if status == "local": + print(f"\n=== {header} -- {recipe.reference} only in local cache, skipping build ===", + flush=True) + local.append(recipe) + continue + print(f"\n=== {header} -- building {recipe.reference} ===", flush=True) start = time.time() cmd = [ @@ -220,25 +329,23 @@ def build_all(args: argparse.Namespace, recipes: list[Recipe], host: str): code = run(cmd, cwd=args.recipes_root) elapsed = time.time() - start if code != 0: - fail(args, built, skipped, recipe, + fail(args, built, skipped, local, present, recipe, f"'conan create' exited with code {code} after {elapsed:.0f}s") print(f"--- {recipe.reference} built in {elapsed:.0f}s ---", flush=True) built.append(recipe) - return built, skipped + return built, skipped, local, present -def fail(args, built, skipped, recipe: Recipe, message: str): +def fail(args, built, skipped, local, present, recipe: Recipe, message: str): print(f"\n!!! BUILD FAILED: {recipe.name} -- {message}", file=sys.stderr, flush=True) - print_summary(built, skipped, failed=recipe) + print_summary(built, skipped, local, present, failed=recipe) if args.upload: print("\nUpload skipped: not all recipes built successfully.", flush=True) sys.exit(1) -def upload_all(args: argparse.Namespace, built: list[Recipe]): - print("\n=== uploading packages ===", flush=True) - +def configure_remote(args: argparse.Namespace): if args.remote_url: if run([args.conan, "remote", "add", "--force", args.remote, args.remote_url]) != 0: sys.exit("error: failed to register remote") @@ -252,26 +359,33 @@ def upload_all(args: argparse.Namespace, built: list[Recipe]): if run(login, redact=redact) != 0: sys.exit("error: failed to log in to remote") - for recipe in built: + +def upload_all(args: argparse.Namespace, recipes: list[Recipe]): + print("\n=== uploading packages ===", flush=True) + for recipe in recipes: print(f"\n--- uploading {recipe.reference} ---", flush=True) if run([args.conan, "upload", recipe.reference, "-r", args.remote, "--confirm"]) != 0: sys.exit(f"error: failed to upload {recipe.reference}") - print(f"\nUploaded {len(built)} package(s) to '{args.remote}'.", flush=True) + print(f"\nUploaded {len(recipes)} package(s) to '{args.remote}'.", flush=True) -def print_summary(built, skipped, failed: Recipe | None = None): +def print_summary(built, skipped, local, present, failed: Recipe | None = None): print("\n" + "=" * 60, flush=True) print("SUMMARY", flush=True) print("=" * 60, flush=True) for recipe in built: print(f" [OK] {recipe.reference}", flush=True) + for recipe in present: + print(f" [REMOTE] {recipe.reference} (already published)", flush=True) + for recipe in local: + print(f" [LOCAL] {recipe.reference} (only in local cache)", flush=True) for recipe, reason in skipped: print(f" [SKIP] {recipe.name} ({reason})", flush=True) if failed is not None: print(f" [FAILED] {failed.reference}", flush=True) print( - f"\nbuilt: {len(built)} skipped: {len(skipped)}" - + (" failed: 1" if failed is not None else ""), + f"\nbuilt: {len(built)} on-remote: {len(present)} local-only: {len(local)} " + f"skipped: {len(skipped)}" + (" failed: 1" if failed is not None else ""), flush=True, ) @@ -300,14 +414,19 @@ def main(): recipes = order_by_dependencies(recipes) print("Build order: " + ", ".join(r.name for r in recipes), flush=True) - built, skipped = build_all(args, recipes, host) - print_summary(built, skipped) + # Log in before building so the per-recipe pre-check can query the remote for existing binaries. + if args.upload: + configure_remote(args) + + built, skipped, local, present = build_all(args, recipes, host) + print_summary(built, skipped, local, present) if args.upload: - if not built: - print("\nNothing was built; skipping upload.", flush=True) + to_upload = built + local + if not to_upload: + print("\nNothing to upload; every recipe was already on the remote.", flush=True) else: - upload_all(args, built) + upload_all(args, to_upload) print("\nAll done.", flush=True) diff --git a/ThirdParty/ConanRecipes/clipp/conandata.yml b/ThirdParty/ConanRecipes/clipp/conandata.yml index caf93cf77..27540263e 100644 --- a/ThirdParty/ConanRecipes/clipp/conandata.yml +++ b/ThirdParty/ConanRecipes/clipp/conandata.yml @@ -1,6 +1,7 @@ platforms: - Windows-x86_64 - Macos-armv8 + - Linux-x86_64 sources: "1.2.3-exp": url: "https://github.com/muellan/clipp/archive/refs/tags/v1.2.3.tar.gz" diff --git a/ThirdParty/ConanRecipes/debugbreak/conandata.yml b/ThirdParty/ConanRecipes/debugbreak/conandata.yml index 72c15de5b..256553296 100644 --- a/ThirdParty/ConanRecipes/debugbreak/conandata.yml +++ b/ThirdParty/ConanRecipes/debugbreak/conandata.yml @@ -1,6 +1,7 @@ platforms: - Windows-x86_64 - Macos-armv8 + - Linux-x86_64 sources: "1.0-exp": url: "https://github.com/scottt/debugbreak/archive/refs/tags/v1.0.tar.gz" diff --git a/ThirdParty/ConanRecipes/dxc/conandata.yml b/ThirdParty/ConanRecipes/dxc/conandata.yml index 1e16bebc1..745c782c5 100644 --- a/ThirdParty/ConanRecipes/dxc/conandata.yml +++ b/ThirdParty/ConanRecipes/dxc/conandata.yml @@ -1,6 +1,7 @@ platforms: - Windows-x86_64 - Macos-armv8 + - Linux-x86_64 sources: "1.8.2505.1-exp": commit: "b106a961d09221b3c5bdb37be45b679257da08b8" diff --git a/ThirdParty/ConanRecipes/dxc/conanfile.py b/ThirdParty/ConanRecipes/dxc/conanfile.py index beb4d0164..0f4cf8e0a 100644 --- a/ThirdParty/ConanRecipes/dxc/conanfile.py +++ b/ThirdParty/ConanRecipes/dxc/conanfile.py @@ -74,7 +74,9 @@ def package(self): copy(self, "d3d12shader.h", os.path.join(self.source_folder, "external", "DirectX-Headers", "include", "directx"), os.path.join(self.package_folder, "include", "dxc")) copy(self, "dxil.dll", os.path.join(self.build_folder, "bin"), os.path.join(self.package_folder, "bin")) copy(self, "dxcompiler.lib", os.path.join(self.build_folder, "lib"), os.path.join(self.package_folder, "lib")) - elif self.settings.os == "Macos": + elif self.settings.os in ("Macos", "Linux"): + # install-distribution drops the shared compiler (libdxcompiler.dylib / + # libdxcompiler.so) under installed/lib on both Unix-likes. copy(self, "*", os.path.join(self.build_folder, "installed", "lib"), os.path.join(self.package_folder, "lib")) def package_info(self): diff --git a/ThirdParty/ConanRecipes/libclang/conandata.yml b/ThirdParty/ConanRecipes/libclang/conandata.yml index b55b8b381..bb38b189e 100644 --- a/ThirdParty/ConanRecipes/libclang/conandata.yml +++ b/ThirdParty/ConanRecipes/libclang/conandata.yml @@ -1,6 +1,7 @@ platforms: - Windows-x86_64 - Macos-armv8 + - Linux-x86_64 sources: "22.1.6-exp": Windows-x86_64: diff --git a/ThirdParty/ConanRecipes/qt/conandata.yml b/ThirdParty/ConanRecipes/qt/conandata.yml index 1ad84e2cf..f0bf218e3 100644 --- a/ThirdParty/ConanRecipes/qt/conandata.yml +++ b/ThirdParty/ConanRecipes/qt/conandata.yml @@ -1,5 +1,6 @@ platforms: - Windows-x86_64 - Macos-armv8 + - Linux-x86_64 versions: - "6.10.3-exp" diff --git a/ThirdParty/ConanRecipes/qt/test_package/conanfile.py b/ThirdParty/ConanRecipes/qt/test_package/conanfile.py index 04c30ffc1..fc862d4bb 100644 --- a/ThirdParty/ConanRecipes/qt/test_package/conanfile.py +++ b/ThirdParty/ConanRecipes/qt/test_package/conanfile.py @@ -24,5 +24,9 @@ def test(self): bin_path = os.path.join(self.cpp.build.bindir, "test_package") if self.settings.os == "Macos": self.run(f"open {bin_path}.app", env="conanrun") + elif self.settings.os == "Linux": + # Run the QWebEngineView smoke test headlessly: offscreen avoids needing an X display, and + # disabling Chromium's sandbox keeps it from aborting under restricted/headless environments. + self.run(f"env QT_QPA_PLATFORM=offscreen QTWEBENGINE_DISABLE_SANDBOX=1 {bin_path}", env="conanrun") else: self.run(bin_path, env="conanrun") diff --git a/Tool/MirrorTool/Src/Parser.cpp b/Tool/MirrorTool/Src/Parser.cpp index 38518e57d..5ea6d07e4 100644 --- a/Tool/MirrorTool/Src/Parser.cpp +++ b/Tool/MirrorTool/Src/Parser.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include #include @@ -413,6 +415,41 @@ namespace MirrorTool { } namespace MirrorTool { +#if PLATFORM_LINUX + // The conan libclang package ships only libclang.so, without clang's builtin headers (stddef.h, stdarg.h, ...) or any + // system include paths, so query the host GCC for the directories it searches and feed them to clang verbatim. + static std::vector GetHostSystemIncludeDirs() + { + FILE* pipe = popen("g++ -E -x c++ - -v < /dev/null 2>&1", "r"); + if (pipe == nullptr) { + return {}; + } + + std::string output; + std::array buffer {}; + while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { + output += buffer.data(); + } + pclose(pipe); + + std::vector result; + bool withinSearchList = false; + for (const std::string& line : Common::StringUtils::Split(output, "\n")) { + if (line.find("#include <...> search starts here:") != std::string::npos) { + withinSearchList = true; + } else if (line.find("End of search list.") != std::string::npos) { + break; + } else if (withinSearchList) { + const size_t start = line.find_first_not_of(" \t"); + if (start != std::string::npos) { + result.emplace_back(line.substr(start)); + } + } + } + return result; + } +#endif + Parser::Parser(std::string inSourceFile, std::vector inHeaderDirs, std::vector inFrameworkDirs) : sourceFile(std::move(inSourceFile)) , headerDirs(std::move(inHeaderDirs)) @@ -446,10 +483,16 @@ namespace MirrorTool { std::format("-I/Library/Developer/CommandLineTools/usr/lib/clang/{}/include", __clang_major__), "-I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include", "-I/Library/Developer/CommandLineTools/usr/include", -#elif DPLATFORM_LINUX +#elif PLATFORM_LINUX "-DPLATFORM_LINUX=1", #endif }; +#if PLATFORM_LINUX + for (const std::string& systemIncludeDir : GetHostSystemIncludeDirs()) { + argumentStrs.emplace_back("-isystem"); + argumentStrs.emplace_back(systemIncludeDir); + } +#endif argumentStrs.reserve(argumentStrs.size() + headerDirs.size()); for (const std::string& headerDir : headerDirs) { argumentStrs.emplace_back(std::string("-I") + headerDir);