diff --git a/CMakeLists.txt b/CMakeLists.txt index 57a406d..f9d031e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,24 @@ cmake_minimum_required(VERSION 3.31) project(Destrum) set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +include(CTest) + +option(DESTRUM_ENABLE_WARNINGS "Enable warnings for first-party targets" ON) + +function(destrum_enable_warnings target) + if (NOT DESTRUM_ENABLE_WARNINGS) + return() + endif() + + if (MSVC) + target_compile_options(${target} PRIVATE /W4 /permissive-) + elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic) + endif() +endfunction() if (MSVC) # Disable Just My Code debugging @@ -12,8 +30,18 @@ add_subdirectory(destrum) add_subdirectory(lightkeeper) add_subdirectory(TheChef) -add_custom_target(CleanupAssets) -add_dependencies(CleanupAssets +if (TARGET _internal_validate_engine_shaders) + add_custom_target(ValidateShaders) + add_dependencies(ValidateShaders _internal_validate_engine_shaders) +endif() + +destrum_enable_warnings(destrum) +destrum_enable_warnings(lightkeeper) + +add_custom_target(CleanupAssets + COMMAND ${CMAKE_COMMAND} -E rm -rf "$/assets/game" + COMMAND ${CMAKE_COMMAND} -E rm -rf "$/assets/engine" + DEPENDS _internal_clean_game_assets _internal_clean_engine_assets ) @@ -40,3 +68,7 @@ if (ENABLE_SANITIZERS AND CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") ) endforeach() endif() + +if (BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/Readme.md b/Readme.md index 3e7ae92..6f27f75 100644 --- a/Readme.md +++ b/Readme.md @@ -77,15 +77,25 @@ cmake -S . -B build Build everything: ```bash -cmake --build build +cmake --build build --config Release +``` + +Run the first-party regression tests: + +```bash +ctest --test-dir build -C Release --output-on-failure ``` Run the demo executable: ```bash -./build/lightkeeper/lightkeeper +./build/lightkeeper/Release/lightkeeper.exe ``` +For single-configuration generators, use the executable path produced by that +generator instead. Runtime assets are placed beside the executable in both +layouts. + ## Asset cooking Assets are cooked automatically as part of the build through the `TheChef` tool. @@ -95,9 +105,17 @@ You can also run the top-level custom targets manually: ```bash cmake --build build --target CookAssets cmake --build build --target CleanupAssets +cmake --build build --target ValidateShaders ``` `CookAssets` processes both engine and game assets. `CleanupAssets` removes generated runtime asset output. +`ValidateShaders` runs `spirv-val` over the cooked engine shaders when the Vulkan SDK provides it. + +Install the application, engine library, public headers, and runtime assets: + +```bash +cmake --install build --config Release +``` ## Demo app diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 7eb0716..ad13d47 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -31,6 +31,11 @@ set(SRC_FILES "src/Graphics/Util.cpp" "src/Graphics/RenderResources.cpp" + "src/Graphics/Managers/VulkanInstanceManager.cpp" + "src/Graphics/Managers/MemoryManager.cpp" + "src/Graphics/Managers/FrameManager.cpp" + "src/Graphics/Managers/ImageManager.cpp" + "src/Graphics/Resources/GPUImage.cpp" "src/Graphics/Resources/NBuffer.cpp" "src/Graphics/Resources/Cubemap.cpp" @@ -139,30 +144,80 @@ set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime") # VERBATIM #) +file(GLOB_RECURSE ENGINE_ASSET_SOURCES CONFIGURE_DEPENDS + "${ASSETS_SRC_DIR}/*") + add_custom_target(_internal_clean_engine_assets + COMMAND TheChef + --input "${ASSETS_SRC_DIR}" + --output "${ASSETS_RUNTIME_DIR}" + --clean + DEPENDS TheChef +) + +add_custom_target(_internal_cook_engine_assets COMMAND TheChef --input "${ASSETS_SRC_DIR}" --output "${ASSETS_RUNTIME_DIR}" --clean -) - -add_custom_target(_internal_cook_engine_assets ALL COMMAND TheChef --input "${ASSETS_SRC_DIR}" --output "${ASSETS_RUNTIME_DIR}" - DEPENDS TheChef + DEPENDS TheChef ${ENGINE_ASSET_SOURCES} + VERBATIM ) +find_program(SPIRV_VAL_EXECUTABLE NAMES spirv-val spirv-val.exe) +if (SPIRV_VAL_EXECUTABLE) + file(GLOB_RECURSE ENGINE_SHADER_SOURCES CONFIGURE_DEPENDS + "${ASSETS_SRC_DIR}/*.vert" + "${ASSETS_SRC_DIR}/*.frag" + "${ASSETS_SRC_DIR}/*.comp") + + set(VALIDATE_SHADER_COMMANDS) + foreach(shader_source IN LISTS ENGINE_SHADER_SOURCES) + file(RELATIVE_PATH shader_relative "${ASSETS_SRC_DIR}" "${shader_source}") + list(APPEND VALIDATE_SHADER_COMMANDS + COMMAND "${SPIRV_VAL_EXECUTABLE}" + "${ASSETS_RUNTIME_DIR}/${shader_relative}.spv") + endforeach() + + add_custom_target(_internal_validate_engine_shaders + DEPENDS _internal_cook_engine_assets + ${VALIDATE_SHADER_COMMANDS} + ) +endif() + function(destrum_cook_engine_assets GAME_TARGET GAME_OUTPUT_DIR) # This resolves to destrum's own directory at function DEFINITION time set(_engine_src "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/assets_src") set(_engine_runtime "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/assets_runtime") - set(_output_engine "${GAME_OUTPUT_DIR}/assets/engine") + set(_output_engine "$/assets/engine") + + if (WIN32) + set(_engine_asset_install_commands + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${_engine_runtime}" "${_output_engine}") + else() + set(_engine_asset_install_commands + COMMAND ${CMAKE_COMMAND} -E create_symlink + "${_engine_runtime}" "${_output_engine}") + endif() add_custom_command(TARGET ${GAME_TARGET} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory "${GAME_OUTPUT_DIR}/assets" + COMMAND ${CMAKE_COMMAND} -E make_directory "${_output_engine}/.." COMMAND ${CMAKE_COMMAND} -E rm -rf "${_output_engine}" - COMMAND ${CMAKE_COMMAND} -E create_symlink "${_engine_runtime}" "${_output_engine}" + ${_engine_asset_install_commands} VERBATIM ) + + add_dependencies(${GAME_TARGET} _internal_cook_engine_assets) + + install(DIRECTORY "${_engine_runtime}/" DESTINATION "bin/assets/engine") endfunction() + +install(TARGETS destrum + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/include/" DESTINATION include) diff --git a/destrum/assets_src/shaders/bindless.glsl b/destrum/assets_src/shaders/bindless.glsl index 64db2b0..74da315 100644 --- a/destrum/assets_src/shaders/bindless.glsl +++ b/destrum/assets_src/shaders/bindless.glsl @@ -8,7 +8,8 @@ layout (set = 0, binding = 1) uniform sampler samplers[]; #define NEAREST_SAMPLER_ID 0 #define LINEAR_SAMPLER_ID 1 -#define SHADOW_SAMPLER_ID 2 +#define ANISOTROPIC_SAMPLER_ID 2 +#define SHADOW_SAMPLER_ID 3 vec4 sampleTexture2DNearest(uint texID, vec2 uv) { return texture(nonuniformEXT(sampler2D(textures[texID], samplers[NEAREST_SAMPLER_ID])), uv); diff --git a/destrum/assets_src/shaders/skinning.comp b/destrum/assets_src/shaders/skinning.comp index de52b67..0a4a594 100644 --- a/destrum/assets_src/shaders/skinning.comp +++ b/destrum/assets_src/shaders/skinning.comp @@ -27,7 +27,7 @@ layout (push_constant) uniform constants uint numVertices; VertexBuffer inputBuffer; SkinningData skinningData; - VertexBuffer outputBuffer; + WritableVertexBuffer outputBuffer; } pcs; layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in; diff --git a/destrum/assets_src/shaders/vertex.glsl b/destrum/assets_src/shaders/vertex.glsl index 96ecc65..cebc2f8 100644 --- a/destrum/assets_src/shaders/vertex.glsl +++ b/destrum/assets_src/shaders/vertex.glsl @@ -16,4 +16,8 @@ layout (buffer_reference, std430) readonly buffer VertexBuffer { Vertex vertices[]; }; +layout (buffer_reference, std430) buffer WritableVertexBuffer { + Vertex vertices[]; +}; + #endif // VERTEX_GLSL diff --git a/destrum/include/destrum/App.h b/destrum/include/destrum/App.h index 9b1b1d4..a6c0ef4 100644 --- a/destrum/include/destrum/App.h +++ b/destrum/include/destrum/App.h @@ -1,6 +1,7 @@ #ifndef APP_H #define APP_H #include +#include #include #include "glm/vec2.hpp" @@ -25,6 +26,7 @@ public: }; App(); + virtual ~App(); void init(const AppParams& params); void run(); @@ -33,10 +35,10 @@ public: virtual void customInit() = 0; virtual void customUpdate(float dt) = 0; virtual void customDraw() = 0; - virtual void customCleanup() = 0; + virtual void customCleanup() {} virtual void customFixedUpdate(float dt) = 0; - virtual void onWindowResize(int newWidth, int newHeight) {}; + virtual void onWindowResize(int, int) {}; protected: SDL_Window* window{nullptr}; @@ -51,6 +53,9 @@ protected: Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)}; bool isRunning{false}; + bool cleanedUp{false}; + bool cleaningUp{false}; + bool customInitStarted{false}; bool gamePaused{false}; bool frameLimit{false}; diff --git a/destrum/include/destrum/Components/Animator.h b/destrum/include/destrum/Components/Animator.h index b4a9417..77273d4 100644 --- a/destrum/include/destrum/Components/Animator.h +++ b/destrum/include/destrum/Components/Animator.h @@ -18,11 +18,14 @@ class Animator final: public Component { public: explicit Animator(GameObject& parent); - void Update() override; + using Component::Update; + void Update(float dt) override; std::string GetTypeName() const override { return "Animator"; } + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; void ImGuiInspector() override; void addClip(std::shared_ptr clip); @@ -66,4 +69,4 @@ private: glm::vec3 sampleScale (const SkeletalAnimation::Track& track, float t); }; -#endif // ANIMATOR_H \ No newline at end of file +#endif // ANIMATOR_H diff --git a/destrum/include/destrum/Components/MeshRendererComponent.h b/destrum/include/destrum/Components/MeshRendererComponent.h index 91a6f49..005cefe 100644 --- a/destrum/include/destrum/Components/MeshRendererComponent.h +++ b/destrum/include/destrum/Components/MeshRendererComponent.h @@ -10,13 +10,18 @@ public: explicit MeshRendererComponent(GameObject &parent); void Start() override; - void Update() override; + void Destroy() override; + void Update(float dt) override; void Render(const RenderContext& ctx) override; - void SetMeshID(MeshID id) { meshID = id; } + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; + void ResolveReferences(const ObjectMap& objects) override; + + void SetMeshID(MeshID id); MeshID GetMeshID() const { return meshID; } - void SetMaterialID(MaterialID id) { materialID = id; } + void SetMaterialID(MaterialID id); MaterialID GetMaterialID() const { return materialID; } std::string GetTypeName() const override { @@ -26,6 +31,8 @@ public: private: MeshID meshID{NULL_MESH_ID}; MaterialID materialID{NULL_MATERIAL_ID}; + std::string meshKey; + std::string materialKey; std::unique_ptr m_skinnedMesh; }; diff --git a/destrum/include/destrum/Components/OrbitAndSpin.h b/destrum/include/destrum/Components/OrbitAndSpin.h index 4dbeecf..53c34c5 100644 --- a/destrum/include/destrum/Components/OrbitAndSpin.h +++ b/destrum/include/destrum/Components/OrbitAndSpin.h @@ -9,7 +9,7 @@ class OrbitAndSpin final : public Component { public: OrbitAndSpin( GameObject& parent, - float radius, + float radius = 5.0f, glm::vec3 center = glm::vec3(0.0f) ) : Component(parent, "OrbitAndSpin") @@ -20,9 +20,15 @@ public: // Call after constructing if you want deterministic randomness per object void Randomize(uint32_t seed); - void Update() override; + using Component::Update; + void Update(float dt) override; void Start() override; + std::string GetTypeName() const override { return "OrbitAndSpin"; } + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; + // optional setters void SetRadius(float r) { m_Radius = r; } void SetCenter(glm::vec3 c) { m_Center = c; } @@ -53,6 +59,7 @@ private: float m_SpinSpeed = 2.0f; // rad/sec MaterialID m_MaterialID{0}; + bool m_BaseScaleLoaded{false}; }; #endif //ORBITANDSPIN_H diff --git a/destrum/include/destrum/Components/Physics/BoxCollider.h b/destrum/include/destrum/Components/Physics/BoxCollider.h index a5e1588..4945cf4 100644 --- a/destrum/include/destrum/Components/Physics/BoxCollider.h +++ b/destrum/include/destrum/Components/Physics/BoxCollider.h @@ -7,9 +7,12 @@ class BoxCollider final : public Collider { public: explicit BoxCollider(GameObject& owner, const glm::vec3& halfExtents = glm::vec3{0.5f}) : Collider(owner) - , m_HalfExtents(halfExtents) {} + , m_HalfExtents(glm::max(halfExtents, glm::vec3{0.0001f})) {} - void Update() override {} + void Update(float dt) override {} + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; std::string GetTypeName() const override { @@ -26,11 +29,12 @@ public: } void SetHalfExtents(const glm::vec3& halfExtents) { - m_HalfExtents = halfExtents; + m_HalfExtents = glm::max(halfExtents, glm::vec3{0.0001f}); + NotifyPhysicsChanged(); } private: glm::vec3 m_HalfExtents{0.5f}; }; -#endif \ No newline at end of file +#endif diff --git a/destrum/include/destrum/Components/Physics/CapsuleCollider.h b/destrum/include/destrum/Components/Physics/CapsuleCollider.h index 71e2acf..c502cb5 100644 --- a/destrum/include/destrum/Components/Physics/CapsuleCollider.h +++ b/destrum/include/destrum/Components/Physics/CapsuleCollider.h @@ -2,14 +2,37 @@ #include -#include +#include class CapsuleCollider final : public Collider { public: explicit CapsuleCollider(GameObject& owner, float radius = 0.5f, float height = 2.0f) : Collider(owner) - , m_Radius(radius) - , m_Height(height) {} + , m_Radius(std::max(radius, 0.0f)) + , m_Height(std::max(height, std::max(radius, 0.0f) * 2.0f)) {} + + void Update(float dt) override {} + + std::string GetTypeName() const override { + return "CapsuleCollider"; + } + + nlohmann::json Serialize() const override { + auto data = SerializeCollider(); + data["radius"] = m_Radius; + data["height"] = m_Height; + return data; + } + + void Deserialize(const nlohmann::json& data) override { + DeserializeCollider(data); + if (data.contains("radius")) { + SetRadius(data.at("radius").get()); + } + if (data.contains("height")) { + SetHeight(data.at("height").get()); + } + } [[nodiscard]] PhysicsShapeDesc BuildPhysicsShape() const override { return PhysicsShapeDesc::Capsule(m_Radius, m_Height, m_CenterOffset, m_IsTrigger); @@ -20,7 +43,9 @@ public: } void SetRadius(float radius) { - m_Radius = std::max(radius, 0.0f); + m_Radius = std::max(radius, 0.0001f); + m_Height = std::max(m_Height, m_Radius * 2.0f); + NotifyPhysicsChanged(); } [[nodiscard]] float GetHeight() const { @@ -29,6 +54,7 @@ public: void SetHeight(float height) { m_Height = std::max(height, m_Radius * 2.0f); + NotifyPhysicsChanged(); } private: diff --git a/destrum/include/destrum/Components/Physics/Collider.h b/destrum/include/destrum/Components/Physics/Collider.h index a37325f..0b8a55f 100644 --- a/destrum/include/destrum/Components/Physics/Collider.h +++ b/destrum/include/destrum/Components/Physics/Collider.h @@ -15,12 +15,33 @@ public: [[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0; [[nodiscard]] bool IsTrigger() const { return m_IsTrigger; } - void SetTrigger(bool trigger) { m_IsTrigger = trigger; } + void SetTrigger(bool trigger) { m_IsTrigger = trigger; NotifyPhysicsChanged(); } [[nodiscard]] const glm::vec3& GetCenterOffset() const { return m_CenterOffset; } - void SetCenterOffset(const glm::vec3& offset) { m_CenterOffset = offset; } + void SetCenterOffset(const glm::vec3& offset) { m_CenterOffset = offset; NotifyPhysicsChanged(); } protected: + [[nodiscard]] nlohmann::json SerializeCollider() const { + return { + {"centerOffset", {m_CenterOffset.x, m_CenterOffset.y, m_CenterOffset.z}}, + {"isTrigger", m_IsTrigger} + }; + } + + void DeserializeCollider(const nlohmann::json& data) { + if (data.contains("centerOffset")) { + const auto& offset = data.at("centerOffset"); + m_CenterOffset = { + offset.at(0).get(), + offset.at(1).get(), + offset.at(2).get() + }; + } + if (data.contains("isTrigger")) { + m_IsTrigger = data.at("isTrigger").get(); + } + } + bool m_IsTrigger{false}; glm::vec3 m_CenterOffset{0.0f}; }; diff --git a/destrum/include/destrum/Components/Physics/Rigidbody.h b/destrum/include/destrum/Components/Physics/Rigidbody.h index 8babe8c..9d61565 100644 --- a/destrum/include/destrum/Components/Physics/Rigidbody.h +++ b/destrum/include/destrum/Components/Physics/Rigidbody.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -12,13 +13,18 @@ public: explicit Rigidbody(GameObject& owner) : Component(owner) {} - ~Rigidbody() override = default; + ~Rigidbody() override; - void Update() override {} + void Update(float dt) override {} + void Start() override; + void Destroy() override; + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; std::string GetTypeName() const override { - return "RigidBody"; + return "Rigidbody"; } void AttachPhysicsBody(PhysicsWorld* world, PhysicsBodyHandle body); @@ -34,22 +40,22 @@ public: [[nodiscard]] glm::vec3 GetLinearVelocity() const; [[nodiscard]] RigidbodyType GetType() const { return m_Type; } - void SetType(RigidbodyType type) { m_Type = type; } + void SetType(RigidbodyType type) { m_Type = type; NotifyPhysicsChanged(); } [[nodiscard]] float GetMass() const { return m_Mass; } - void SetMass(float mass) { m_Mass = mass > 0.0f ? mass : 0.0001f; } + void SetMass(float mass) { m_Mass = mass > 0.0f ? mass : 0.0001f; NotifyPhysicsChanged(); } [[nodiscard]] float GetFriction() const { return m_Friction; } - void SetFriction(float friction) { m_Friction = friction; } + void SetFriction(float friction) { m_Friction = std::max(friction, 0.0f); NotifyPhysicsChanged(); } [[nodiscard]] float GetRestitution() const { return m_Restitution; } - void SetRestitution(float restitution) { m_Restitution = restitution; } + void SetRestitution(float restitution) { m_Restitution = std::clamp(restitution, 0.0f, 1.0f); NotifyPhysicsChanged(); } [[nodiscard]] bool UsesGravity() const { return m_UseGravity; } - void SetUseGravity(bool useGravity) { m_UseGravity = useGravity; } + void SetUseGravity(bool useGravity) { m_UseGravity = useGravity; NotifyPhysicsChanged(); } [[nodiscard]] bool AllowsSleep() const { return m_AllowSleep; } - void SetAllowSleep(bool allowSleep) { m_AllowSleep = allowSleep; } + void SetAllowSleep(bool allowSleep) { m_AllowSleep = allowSleep; NotifyPhysicsChanged(); } [[nodiscard]] PhysicsWorld* GetPhysicsWorld() const { return m_World; } diff --git a/destrum/include/destrum/Components/Physics/SphereCollider.h b/destrum/include/destrum/Components/Physics/SphereCollider.h index 72124d9..467b2c2 100644 --- a/destrum/include/destrum/Components/Physics/SphereCollider.h +++ b/destrum/include/destrum/Components/Physics/SphereCollider.h @@ -8,9 +8,12 @@ class SphereCollider final : public Collider { public: explicit SphereCollider(GameObject& owner, float radius = 0.5f) : Collider(owner) - , m_Radius(radius) {} + , m_Radius(std::max(radius, 0.0001f)) {} - void Update() override {} + void Update(float dt) override {} + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; std::string GetTypeName() const override { return "SphereCollider"; @@ -25,7 +28,8 @@ public: } void SetRadius(float radius) { - m_Radius = std::max(radius, 0.0f); + m_Radius = std::max(radius, 0.0001f); + NotifyPhysicsChanged(); } private: diff --git a/destrum/include/destrum/Components/Rotator.h b/destrum/include/destrum/Components/Rotator.h index 6efbdd9..28a076f 100644 --- a/destrum/include/destrum/Components/Rotator.h +++ b/destrum/include/destrum/Components/Rotator.h @@ -8,14 +8,20 @@ class Rotator final : public Component { public: - explicit Rotator(GameObject& parent, float distance, float speed); + explicit Rotator(GameObject& parent, float distance = 0.0f, float speed = 0.0f); Rotator(const Rotator& other) = delete; Rotator(Rotator&& other) noexcept = delete; Rotator& operator=(const Rotator& other) = delete; Rotator& operator=(Rotator&& other) noexcept = delete; - void Update() override; + using Component::Update; + void Update(float dt) override; + + std::string GetTypeName() const override { return "Rotator"; } + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; void SetDistance(float distance); void SetSpeed(float speed) { m_Speed = speed; } diff --git a/destrum/include/destrum/Components/Spinner.h b/destrum/include/destrum/Components/Spinner.h index 04b6062..a0e794c 100644 --- a/destrum/include/destrum/Components/Spinner.h +++ b/destrum/include/destrum/Components/Spinner.h @@ -6,14 +6,26 @@ class Spinner final : public Component { public: - explicit Spinner(GameObject& parent, glm::vec3 axis, float speedRadPerSec) + explicit Spinner(GameObject& parent, + glm::vec3 axis = glm::vec3{0.0f, 1.0f, 0.0f}, + float speedRadPerSec = 1.0f) : Component(parent, "Spinner") - , m_Axis(glm::normalize(axis)) + , m_Axis(glm::dot(axis, axis) > 0.000001f ? glm::normalize(axis) : glm::vec3{0.0f, 1.0f, 0.0f}) , m_Speed(speedRadPerSec) {} - void Update() override; + using Component::Update; + void Update(float dt) override; - void SetAxis(const glm::vec3& axis) { m_Axis = glm::normalize(axis); } + std::string GetTypeName() const override { return "Spinner"; } + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; + + void SetAxis(const glm::vec3& axis) { + m_Axis = glm::dot(axis, axis) > 0.000001f + ? glm::normalize(axis) + : glm::vec3{0.0f, 1.0f, 0.0f}; + } void SetSpeed(float speedRadPerSec) { m_Speed = speedRadPerSec; } private: diff --git a/destrum/include/destrum/Event.h b/destrum/include/destrum/Event.h index df98041..2ecee69 100644 --- a/destrum/include/destrum/Event.h +++ b/destrum/include/destrum/Event.h @@ -1,7 +1,9 @@ #ifndef EVENT_H #define EVENT_H #include +#include #include +#include class EventListener; @@ -41,7 +43,11 @@ private: template class Event final: public BaseEvent { - using EventFunction = std::pair>; + struct EventFunction { + void* listener{nullptr}; + std::uint64_t id{0}; + std::function callback; + }; public: Event() = default; @@ -65,28 +71,45 @@ public: listener->AddEvent(this); m_EventListeners.insert(listener); - m_FunctionBinds.emplace_back( - listener, [object, memberFunction] (EventArgs... args) { (object->*memberFunction)(args...); }); + const auto id = m_NextBindingId++; + m_ActiveBindings.insert(id); + m_FunctionBinds.push_back({ + listener, + id, + [object, memberFunction] (EventArgs... args) { (object->*memberFunction)(args...); } + }); } template void AddListener(Function function) { - m_FunctionBinds.emplace_back(nullptr, [function] (EventArgs... args) { function(args...); }); + const auto id = m_NextBindingId++; + m_ActiveBindings.insert(id); + m_FunctionBinds.push_back({ + nullptr, + id, + [function] (EventArgs... args) { function(args...); } + }); } template void Invoke(Args&&... args) { - m_Invoking = true; - for (auto&& listenerFunction: m_FunctionBinds) - listenerFunction.second(args...); - m_Invoking = false; + // Callbacks are allowed to unregister themselves or register another + // callback. Iterate over a snapshot so those mutations cannot + // invalidate this invocation. + const auto listeners = m_FunctionBinds; + for (const auto& listenerFunction: listeners) { + if (m_ActiveBindings.contains(listenerFunction.id)) { + listenerFunction.callback(args...); + } + } } void RemoveListener(EventListener* listener) override { m_EventListeners.erase(listener); for (auto it = m_FunctionBinds.begin(); it != m_FunctionBinds.end();) { - if (it->first == static_cast(listener)) { + if (it->listener == static_cast(listener)) { + m_ActiveBindings.erase(it->id); it = m_FunctionBinds.erase(it); } else { ++it; @@ -95,9 +118,10 @@ public: } private: - bool m_Invoking{false}; std::vector m_FunctionBinds{}; std::unordered_set m_EventListeners{}; + std::unordered_set m_ActiveBindings{}; + std::uint64_t m_NextBindingId{1}; }; diff --git a/destrum/include/destrum/FS/AssetFS.h b/destrum/include/destrum/FS/AssetFS.h index 98ee034..89b7abd 100644 --- a/destrum/include/destrum/FS/AssetFS.h +++ b/destrum/include/destrum/FS/AssetFS.h @@ -2,7 +2,10 @@ #define ASSETFS_H #include +#include +#include #include +#include #include #include @@ -20,6 +23,7 @@ struct FSMount { class AssetFS final: public Singleton { public: void Init(std::filesystem::path exeDir); + void Reset(); void Mount(std::string scheme, std::filesystem::path root); std::vector ReadBytes(std::string_view vpath); diff --git a/destrum/include/destrum/FS/Manifest.h b/destrum/include/destrum/FS/Manifest.h index ecc7ad1..c6b6d8f 100644 --- a/destrum/include/destrum/FS/Manifest.h +++ b/destrum/include/destrum/FS/Manifest.h @@ -1,8 +1,11 @@ #ifndef MANIFEST_H #define MANIFEST_H -#include +#include +#include #include +#include +#include #include struct ManifestAsset { diff --git a/destrum/include/destrum/Graphics/BindlessSetManager.h b/destrum/include/destrum/Graphics/BindlessSetManager.h index 63ad417..5a3f841 100644 --- a/destrum/include/destrum/Graphics/BindlessSetManager.h +++ b/destrum/include/destrum/Graphics/BindlessSetManager.h @@ -9,7 +9,7 @@ struct GPUImage; class BindlessSetManager { public: - void init(VkDevice device, float maxAnisotropy); + void init(VkDevice device, VkPhysicalDevice physicalDevice, float maxAnisotropy); void cleanup(VkDevice device); VkDescriptorSetLayout getDescSetLayout() const { return descSetLayout; } @@ -18,16 +18,21 @@ public: void addImage(VkDevice device, std::uint32_t id, const VkImageView imageView); void addSampler(VkDevice device, std::uint32_t id, VkSampler sampler); + [[nodiscard]] std::uint32_t getMaxImageCount() const { return maxBindlessResources; } + private: void initDefaultSamplers(VkDevice device, float maxAnisotropy); - VkDescriptorPool descPool; - VkDescriptorSetLayout descSetLayout; - VkDescriptorSet descSet; + VkDescriptorPool descPool{VK_NULL_HANDLE}; + VkDescriptorSetLayout descSetLayout{VK_NULL_HANDLE}; + VkDescriptorSet descSet{VK_NULL_HANDLE}; - VkSampler nearestSampler; - VkSampler linearSampler; - VkSampler shadowMapSampler; + std::uint32_t maxBindlessResources{0}; + + VkSampler nearestSampler{VK_NULL_HANDLE}; + VkSampler linearSampler{VK_NULL_HANDLE}; + VkSampler anisotropicSampler{VK_NULL_HANDLE}; + VkSampler shadowMapSampler{VK_NULL_HANDLE}; }; #endif //BINDLESSSETMANAGER_H diff --git a/destrum/include/destrum/Graphics/Caches/ImageCache.h b/destrum/include/destrum/Graphics/Caches/ImageCache.h index 8f21377..b4fb5f9 100644 --- a/destrum/include/destrum/Graphics/Caches/ImageCache.h +++ b/destrum/include/destrum/Graphics/Caches/ImageCache.h @@ -30,6 +30,9 @@ public: [[nodiscard]] const GPUImage& getImage(ImageID id) const; [[nodiscard]] ImageID getFreeImageId() const; + [[nodiscard]] std::uint32_t getMaxImageCount() const { + return bindlessSetManager.getMaxImageCount(); + } void destroyImages(); diff --git a/destrum/include/destrum/Graphics/Caches/MaterialCache.h b/destrum/include/destrum/Graphics/Caches/MaterialCache.h index 86ef9fa..23d1d1f 100644 --- a/destrum/include/destrum/Graphics/Caches/MaterialCache.h +++ b/destrum/include/destrum/Graphics/Caches/MaterialCache.h @@ -2,6 +2,8 @@ #define MATERIALCACHE_H #include +#include +#include #include #include @@ -28,6 +30,8 @@ public: MaterialID addMaterial(Material material); MaterialID addSimpleTextureMaterial(ImageID textureID); [[nodiscard]] const Material& getMaterial(MaterialID id) const; + [[nodiscard]] const std::string& getMaterialKey(MaterialID id) const; + [[nodiscard]] std::optional findMaterialByKey(std::string_view key) const; [[nodiscard]] MaterialID getFreeMaterialId() const; [[nodiscard]] MaterialID getPlaceholderMaterialId() const; @@ -41,11 +45,13 @@ public: private: std::vector materials; + std::vector materialKeys; static constexpr auto MAX_MATERIALS = 1000; GPUBuffer materialDataBuffer; MaterialDefaultTextures defaultTextures; + GfxDevice* device{nullptr}; // material which is used for meshes without materials MaterialID placeholderMaterialId{NULL_MATERIAL_ID}; diff --git a/destrum/include/destrum/Graphics/Caches/MeshCache.h b/destrum/include/destrum/Graphics/Caches/MeshCache.h index 95412e0..48b7fc2 100644 --- a/destrum/include/destrum/Graphics/Caches/MeshCache.h +++ b/destrum/include/destrum/Graphics/Caches/MeshCache.h @@ -3,6 +3,9 @@ #include +#include +#include + #include "../ids.h" class GfxDevice; @@ -14,12 +17,15 @@ public: MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh); const GPUMesh& getMesh(MeshID id) const; const CPUMesh& getCPUMesh(MeshID id) const; + [[nodiscard]] const std::string& getMeshKey(MeshID id) const; + [[nodiscard]] std::optional findMeshByKey(std::string_view key) const; private: void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const; std::vector meshes; std::vector cpuMeshes; + std::vector meshKeys; }; #endif //MESHCACHE_H diff --git a/destrum/include/destrum/Graphics/GPUImage.h b/destrum/include/destrum/Graphics/GPUImage.h index 81a14af..3a34993 100644 --- a/destrum/include/destrum/Graphics/GPUImage.h +++ b/destrum/include/destrum/Graphics/GPUImage.h @@ -2,7 +2,10 @@ #define GPUIMAGE_H #include +#include #include +#include +#include #include #include #include @@ -10,20 +13,70 @@ #include struct GPUImage { - VkImage image; - VkImageView imageView; - VmaAllocation allocation; - VkFormat format; - VkImageUsageFlags usage; - VkExtent3D extent; + GPUImage() = default; + ~GPUImage() = default; + GPUImage(const GPUImage&) = delete; + GPUImage& operator=(const GPUImage&) = delete; + + GPUImage(GPUImage&& other) noexcept { + *this = std::move(other); + } + + GPUImage& operator=(GPUImage&& other) noexcept { + if (this == &other) { + return *this; + } + + image = other.image; + imageView = other.imageView; + allocation = other.allocation; + format = other.format; + usage = other.usage; + extent = other.extent; + layout = other.layout; + mipLevels = other.mipLevels; + numLayers = other.numLayers; + layerLayouts = std::move(other.layerLayouts); + isCubemap = other.isCubemap; + debugName = std::move(other.debugName); + id = other.id; + + other.image = VK_NULL_HANDLE; + other.imageView = VK_NULL_HANDLE; + other.allocation = VK_NULL_HANDLE; + other.id = NULL_BINDLESS_ID; + return *this; + } + + VkImage image{VK_NULL_HANDLE}; + VkImageView imageView{VK_NULL_HANDLE}; + VmaAllocation allocation{VK_NULL_HANDLE}; + VkFormat format{VK_FORMAT_UNDEFINED}; + VkImageUsageFlags usage{0}; + VkExtent3D extent{}; + mutable VkImageLayout layout{VK_IMAGE_LAYOUT_UNDEFINED}; std::uint32_t mipLevels{1}; std::uint32_t numLayers{1}; + mutable std::vector layerLayouts; bool isCubemap{false}; std::string debugName{}; [[nodiscard]] glm::ivec2 getSize2D() const { return glm::ivec2{extent.width, extent.height}; } [[nodiscard]] VkExtent2D getExtent2D() const { return VkExtent2D{extent.width, extent.height}; } + [[nodiscard]] VkImageLayout getLayout(std::uint32_t layer = 0) const + { + return layerLayouts.empty() ? layout : layerLayouts.at(layer); + } + + void setLayout(VkImageLayout newLayout, std::uint32_t layer = 0) const + { + layout = newLayout; + if (!layerLayouts.empty()) { + layerLayouts.at(layer) = newLayout; + } + } + [[nodiscard]] BindlessID getBindlessId() const { assert(id != NULL_BINDLESS_ID && "Image wasn't added to bindless set"); @@ -31,10 +84,10 @@ struct GPUImage { } // should be called by ImageCache only - void setBindlessId(const std::uint32_t id) + void setBindlessId(const std::uint32_t bindlessId) { - assert(id != NULL_BINDLESS_ID); - this->id = id; + assert(bindlessId != NULL_BINDLESS_ID); + id = bindlessId; } [[nodiscard]] bool isInitialized() const { return id != NULL_BINDLESS_ID; } diff --git a/destrum/include/destrum/Graphics/GfxDevice.h b/destrum/include/destrum/Graphics/GfxDevice.h index 667e4fb..273476a 100644 --- a/destrum/include/destrum/Graphics/GfxDevice.h +++ b/destrum/include/destrum/Graphics/GfxDevice.h @@ -10,11 +10,8 @@ #include -#include -#include - #include -#include +#include #include #include @@ -27,6 +24,10 @@ #include "ImmediateExecuter.h" #include "Util.h" +#include "Managers/FrameManager.h" +#include "Managers/VulkanInstanceManager.h" +#include "Managers/MemoryManager.h" +#include "Managers/ImageManager.h" class ImguiPass; @@ -35,7 +36,6 @@ namespace tracy { class VkCtx; } - using DestrumTracyVkCtx = tracy::VkCtx*; #else using DestrumTracyVkCtx = void*; @@ -46,22 +46,16 @@ using ImmediateExecuteFunction = std::function; class GfxDevice { public: - struct FrameData - { - VkCommandPool commandPool{VK_NULL_HANDLE}; - VkCommandBuffer commandBuffer{VK_NULL_HANDLE}; - }; - struct EndFrameProps { VkClearColorValue clearColor{{0.f, 0.f, 0.f, 1.f}}; - glm::ivec4 drawImageBlitRect{}; // where to blit draw image to + glm::ivec4 drawImageBlitRect{}; ImguiPass* imguiPass{nullptr}; }; public: GfxDevice(); - ~GfxDevice() = default; + ~GfxDevice(); GfxDevice(const GfxDevice&) = delete; GfxDevice& operator=(const GfxDevice&) = delete; @@ -85,23 +79,25 @@ public: return swapchain.isDirty(); } - VulkanImmediateExecutor& GetImmediateExecuter(); + VulkanImmediateExecutor& getImmediateExecuter(); + [[deprecated("Use getImmediateExecuter()")]] + VulkanImmediateExecutor& GetImmediateExecuter() { return getImmediateExecuter(); } void immediateSubmit(ImmediateExecuteFunction&& f) const; [[nodiscard]] std::uint32_t getCurrentFrameIndex() const { - return frameNumber % FRAMES_IN_FLIGHT; + return frameManager.getCurrentFrameIndex(); } - [[nodiscard]] FrameData& getCurrentFrame() + [[nodiscard]] FrameManager::FrameData& getCurrentFrame() { - return frames[getCurrentFrameIndex()]; + return frameManager.getCurrentFrame(); } - [[nodiscard]] const FrameData& getCurrentFrame() const + [[nodiscard]] const FrameManager::FrameData& getCurrentFrame() const { - return frames[getCurrentFrameIndex()]; + return frameManager.getCurrentFrame(); } [[nodiscard]] VkExtent2D getSwapchainExtent() const @@ -112,68 +108,94 @@ public: [[nodiscard]] GPUBuffer createBuffer( std::size_t allocSize, VkBufferUsageFlags usage, - VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_AUTO) const; + VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE) const + { + return memoryManager.createBuffer(allocSize, usage, memoryUsage); + } - void destroyBuffer(const GPUBuffer& buffer) const; + void destroyBuffer(GPUBuffer& buffer) const + { + memoryManager.destroyBuffer(buffer); + } - [[nodiscard]] VkDeviceAddress getBufferAddress(const GPUBuffer& buffer) const; + [[nodiscard]] VkDeviceAddress getBufferAddress(const GPUBuffer& buffer) const + { + return memoryManager.getBufferAddress(buffer); + } [[nodiscard]] GPUImage createImageRaw( const vkutil::CreateImageInfo& createInfo, - std::optional customAllocationCreateInfo = std::nullopt) const; + std::optional customAllocationCreateInfo = std::nullopt) const + { + return imageManager.createImage(createInfo, customAllocationCreateInfo); + } [[nodiscard]] std::optional loadImageFromFileRaw( const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, - TextureIntent intent) const; + TextureIntent intent) const + { + return imageManager.loadImageFromFile(path, usage, mipMap, intent); + } void uploadImageDataSized( const GPUImage& image, const void* pixelData, std::size_t byteSize, - std::uint32_t layer) const; - - void destroyImage(const GPUImage& image) const; - - [[nodiscard]] vkb::Device getDevice() const + std::uint32_t layer) const { - return device; + imageManager.uploadImageData(image, pixelData, byteSize, layer); } - [[nodiscard]] vkb::Device getVkbDevice() const + void destroyImage(GPUImage& image) const { - return device; - } - - [[nodiscard]] VkInstance getVkInstance() const - { - return instance; - } - - [[nodiscard]] VkPhysicalDevice getVkPhysicalDevice() const - { - return physicalDevice; + imageManager.destroyImage(image); } [[nodiscard]] VkDevice getVkDevice() const { - return device; + return instanceManager.getDevice(); + } + + [[nodiscard]] VkDevice getDevice() const + { + return getVkDevice(); + } + + [[nodiscard]] vkb::Device getVkbDevice() const + { + return instanceManager.getVkbDevice(); + } + + [[nodiscard]] VkInstance getVkInstance() const + { + return instanceManager.getInstance(); + } + + [[nodiscard]] VkPhysicalDevice getVkPhysicalDevice() const + { + return instanceManager.getPhysicalDevice(); } [[nodiscard]] VmaAllocator getAllocator() const { - return allocator; + return memoryManager.getAllocator(); } [[nodiscard]] std::uint32_t getGraphicsQueueFamily() const { - return graphicsQueueFamily; + return instanceManager.getGraphicsQueueFamily(); } [[nodiscard]] VkQueue getGraphicsQueue() const { - return graphicsQueue; + return instanceManager.getGraphicsQueue(); + } + + [[nodiscard]] VkQueue getPresentQueue() const + { + return instanceManager.getPresentQueue(); } [[nodiscard]] VkFormat getSwapchainFormat() const @@ -193,30 +215,29 @@ public: [[nodiscard]] DestrumTracyVkCtx getTracyVkCtx() const { - return tracyVkCtx; + return instanceManager.getTracyVkCtx(); } + VulkanInstanceManager& getInstanceManager() { return instanceManager; } + MemoryManager& getMemoryManager() { return memoryManager; } + private: - vkb::Instance instance; - vkb::PhysicalDevice physicalDevice; - vkb::Device device; - VmaAllocator allocator{VK_NULL_HANDLE}; - - DestrumTracyVkCtx tracyVkCtx{nullptr}; - - std::uint32_t graphicsQueueFamily{0}; - VkQueue graphicsQueue{VK_NULL_HANDLE}; - - VkSurfaceKHR surface{VK_NULL_HANDLE}; - VkFormat swapchainFormat{VK_FORMAT_UNDEFINED}; - Swapchain swapchain; - - std::array frames{}; - std::uint32_t frameNumber{0}; + VulkanInstanceManager instanceManager; + MemoryManager memoryManager; + FrameManager frameManager; + ImageManager imageManager; VulkanImmediateExecutor executor; + Swapchain swapchain; + VkFormat swapchainFormat{VK_FORMAT_UNDEFINED}; + + std::uint32_t activeFrameIndex{0}; + std::uint32_t activeSwapchainImageIndex{0}; + bool frameActive{false}; + bool m_vSync{false}; + bool initialized{false}; }; -#endif // GFXDEVICE_H +#endif diff --git a/destrum/include/destrum/Graphics/ImmediateExecuter.h b/destrum/include/destrum/Graphics/ImmediateExecuter.h index f639554..c4b5e2f 100644 --- a/destrum/include/destrum/Graphics/ImmediateExecuter.h +++ b/destrum/include/destrum/Graphics/ImmediateExecuter.h @@ -13,15 +13,17 @@ public: void immediateSubmit(std::function&& function) const; + [[nodiscard]] VkCommandBuffer getCommandBuffer() const { return immCommandBuffer; } + private: bool initialized{false}; - VkDevice device; - VkQueue graphicsQueue; + VkDevice device{VK_NULL_HANDLE}; + VkQueue graphicsQueue{VK_NULL_HANDLE}; - VkCommandBuffer immCommandBuffer; - VkCommandPool immCommandPool; - VkFence immFence; + VkCommandBuffer immCommandBuffer{VK_NULL_HANDLE}; + VkCommandPool immCommandPool{VK_NULL_HANDLE}; + VkFence immFence{VK_NULL_HANDLE}; }; #endif //IMMEDIATEEXECUTER_H diff --git a/destrum/include/destrum/Graphics/Managers/FrameManager.h b/destrum/include/destrum/Graphics/Managers/FrameManager.h new file mode 100644 index 0000000..e591d65 --- /dev/null +++ b/destrum/include/destrum/Graphics/Managers/FrameManager.h @@ -0,0 +1,57 @@ +#ifndef FRAMEMANAGER_H +#define FRAMEMANAGER_H + +#include +#include + +#include + +class FrameManager { +public: + struct FrameData { + VkCommandPool commandPool{VK_NULL_HANDLE}; + VkCommandBuffer commandBuffer{VK_NULL_HANDLE}; + }; + + FrameManager() = default; + ~FrameManager(); + + FrameManager(const FrameManager&) = delete; + FrameManager& operator=(const FrameManager&) = delete; + FrameManager(FrameManager&&) = delete; + FrameManager& operator=(FrameManager&&) = delete; + + void init(VkDevice device, std::uint32_t queueFamily); + void cleanup(VkDevice device); + + [[nodiscard]] VkCommandBuffer beginFrame(); + void endFrame(VkCommandBuffer cmd); + + void waitIdle() const; + + [[nodiscard]] std::uint32_t getCurrentFrameIndex() const { + return frameNumber % 2; + } + + [[nodiscard]] FrameData& getCurrentFrame() { + return frames[getCurrentFrameIndex()]; + } + + [[nodiscard]] const FrameData& getCurrentFrame() const { + return frames[getCurrentFrameIndex()]; + } + + [[nodiscard]] VkCommandBuffer getCurrentCommandBuffer() { + return frames[getCurrentFrameIndex()].commandBuffer; + } + + void nextFrame(); + +private: + static constexpr std::size_t FRAMES_IN_FLIGHT = 2; + std::array frames{}; + std::uint32_t frameNumber{0}; + VkDevice device{VK_NULL_HANDLE}; +}; + +#endif \ No newline at end of file diff --git a/destrum/include/destrum/Graphics/Managers/ImageManager.h b/destrum/include/destrum/Graphics/Managers/ImageManager.h new file mode 100644 index 0000000..7acae63 --- /dev/null +++ b/destrum/include/destrum/Graphics/Managers/ImageManager.h @@ -0,0 +1,59 @@ +#ifndef IMAGEMANAGER_H +#define IMAGEMANAGER_H + +#include +#include +#include + +#include + +#include +#include + +#include "destrum/Graphics/Util.h" + +class VulkanImmediateExecutor; +class MemoryManager; + +class ImageManager { +public: + ImageManager() = default; + ~ImageManager(); + + ImageManager(const ImageManager&) = delete; + ImageManager& operator=(const ImageManager&) = delete; + ImageManager(ImageManager&&) = delete; + ImageManager& operator=(ImageManager&&) = delete; + + void init( + VkDevice device, + VkPhysicalDevice physicalDevice, + const MemoryManager& memoryManager, + VulkanImmediateExecutor& executor); + + [[nodiscard]] GPUImage createImage( + const vkutil::CreateImageInfo& createInfo, + std::optional customAllocationCreateInfo = std::nullopt) const; + + [[nodiscard]] std::optional loadImageFromFile( + const std::filesystem::path& path, + VkImageUsageFlags usage, + bool mipMap, + TextureIntent intent) const; + + void uploadImageData( + const GPUImage& image, + const void* pixelData, + std::size_t byteSize, + std::uint32_t layer = 0) const; + + void destroyImage(GPUImage& image) const; + +private: + VkDevice device{VK_NULL_HANDLE}; + VkPhysicalDevice physicalDevice{VK_NULL_HANDLE}; + const MemoryManager* memoryManager{nullptr}; + VulkanImmediateExecutor* executor{nullptr}; +}; + +#endif diff --git a/destrum/include/destrum/Graphics/Managers/MemoryManager.h b/destrum/include/destrum/Graphics/Managers/MemoryManager.h new file mode 100644 index 0000000..c613824 --- /dev/null +++ b/destrum/include/destrum/Graphics/Managers/MemoryManager.h @@ -0,0 +1,49 @@ +#ifndef MEMORYMANAGER_H +#define MEMORYMANAGER_H + +#include +#include + +#include +#include + +#include + +class VulkanInstanceManager; + +class MemoryManager { +public: + MemoryManager() = default; + ~MemoryManager(); + + MemoryManager(const MemoryManager&) = delete; + MemoryManager& operator=(const MemoryManager&) = delete; + MemoryManager(MemoryManager&&) = delete; + MemoryManager& operator=(MemoryManager&&) = delete; + + void init(const VulkanInstanceManager& instanceManager); + void cleanup(VkDevice device); + + [[nodiscard]] VmaAllocator getAllocator() const { return allocator; } + [[nodiscard]] VkDevice getDevice() const { return device; } + + [[nodiscard]] GPUBuffer createBuffer( + std::size_t allocSize, + VkBufferUsageFlags usage, + VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_AUTO) const; + + void destroyBuffer(GPUBuffer& buffer) const; + + void flushAllocation( + const GPUBuffer& buffer, + VkDeviceSize offset = 0, + VkDeviceSize size = VK_WHOLE_SIZE) const; + + [[nodiscard]] VkDeviceAddress getBufferAddress(const GPUBuffer& buffer) const; + +private: + VmaAllocator allocator{VK_NULL_HANDLE}; + VkDevice device{VK_NULL_HANDLE}; +}; + +#endif diff --git a/destrum/include/destrum/Graphics/Managers/VulkanInstanceManager.h b/destrum/include/destrum/Graphics/Managers/VulkanInstanceManager.h new file mode 100644 index 0000000..96c67c0 --- /dev/null +++ b/destrum/include/destrum/Graphics/Managers/VulkanInstanceManager.h @@ -0,0 +1,117 @@ +#ifndef VULKANINSTANCEMANAGER_H +#define VULKANINSTANCEMANAGER_H + +#include + +#include +#include + +#include +#include + +#if defined(TRACY_ENABLE) +namespace tracy +{ + class VkCtx; +} +using DestrumTracyVkCtx = tracy::VkCtx*; +#else +using DestrumTracyVkCtx = void*; +#endif + +class VulkanInstanceManager +{ +public: + struct DeviceFeatures + { + VkPhysicalDeviceFeatures device{ + .imageCubeArray = VK_TRUE, + .geometryShader = VK_TRUE, + .depthClamp = VK_TRUE, + .fillModeNonSolid = VK_TRUE, + .samplerAnisotropy = VK_TRUE, + }; + + VkPhysicalDeviceVulkan12Features features12{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, + .descriptorIndexing = VK_TRUE, + .shaderSampledImageArrayNonUniformIndexing = VK_TRUE, + .descriptorBindingSampledImageUpdateAfterBind = VK_TRUE, + .descriptorBindingPartiallyBound = VK_TRUE, + .runtimeDescriptorArray = VK_TRUE, + .scalarBlockLayout = VK_TRUE, + .bufferDeviceAddress = VK_TRUE, + }; + + VkPhysicalDeviceVulkan13Features features13{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, + .synchronization2 = VK_TRUE, + .dynamicRendering = VK_TRUE, + }; + + VkPhysicalDeviceExtendedDynamicState3FeaturesEXT extendedDynamicState3{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_3_FEATURES_EXT, + .extendedDynamicState3PolygonMode = VK_TRUE, + }; + }; + + VulkanInstanceManager() = default; + ~VulkanInstanceManager(); + + VulkanInstanceManager(const VulkanInstanceManager&) = delete; + VulkanInstanceManager& operator=(const VulkanInstanceManager&) = delete; + VulkanInstanceManager(VulkanInstanceManager&&) = delete; + VulkanInstanceManager& operator=(VulkanInstanceManager&&) = delete; + + void init( + SDL_Window* window, + const std::string& appName, + const DeviceFeatures& features = DeviceFeatures{}); + + void cleanup(); + + void initTracy(VkCommandBuffer tracyInitCmd); + + void waitIdle() const; + + [[nodiscard]] bool isValid() const { return initialized; } + + [[nodiscard]] VkInstance getInstance() const { return instance; } + [[nodiscard]] VkPhysicalDevice getPhysicalDevice() const { return physicalDevice; } + [[nodiscard]] VkDevice getDevice() const { return device; } + + [[nodiscard]] const vkb::Instance& getVkbInstance() const { return vkbInstance; } + [[nodiscard]] const vkb::PhysicalDevice& getVkbPhysicalDevice() const { return vkbPhysicalDevice; } + [[nodiscard]] const vkb::Device& getVkbDevice() const { return vkbDevice; } + + [[nodiscard]] VkSurfaceKHR getSurface() const { return surface; } + + [[nodiscard]] uint32_t getGraphicsQueueFamily() const { return graphicsQueueFamily; } + [[nodiscard]] VkQueue getGraphicsQueue() const { return graphicsQueue; } + [[nodiscard]] uint32_t getPresentQueueFamily() const { return presentQueueFamily; } + [[nodiscard]] VkQueue getPresentQueue() const { return presentQueue; } + + [[nodiscard]] DestrumTracyVkCtx getTracyVkCtx() const { return tracyVkCtx; } + +private: + vkb::Instance vkbInstance{}; + vkb::PhysicalDevice vkbPhysicalDevice{}; + vkb::Device vkbDevice{}; + + VkInstance instance{VK_NULL_HANDLE}; + VkPhysicalDevice physicalDevice{VK_NULL_HANDLE}; + VkDevice device{VK_NULL_HANDLE}; + + VkSurfaceKHR surface{VK_NULL_HANDLE}; + + uint32_t graphicsQueueFamily{VK_QUEUE_FAMILY_IGNORED}; + VkQueue graphicsQueue{VK_NULL_HANDLE}; + uint32_t presentQueueFamily{VK_QUEUE_FAMILY_IGNORED}; + VkQueue presentQueue{VK_NULL_HANDLE}; + + DestrumTracyVkCtx tracyVkCtx{nullptr}; + + bool initialized{false}; +}; + +#endif diff --git a/destrum/include/destrum/Graphics/MeshDrawCommand.h b/destrum/include/destrum/Graphics/MeshDrawCommand.h index 1955fd9..e4f2fd8 100644 --- a/destrum/include/destrum/Graphics/MeshDrawCommand.h +++ b/destrum/include/destrum/Graphics/MeshDrawCommand.h @@ -11,11 +11,11 @@ struct MeshDrawCommand { - MeshID meshId; - glm::mat4 transformMatrix; + MeshID meshId{NULL_MESH_ID}; + glm::mat4 transformMatrix{1.0f}; // for frustum culling - Sphere worldBoundingSphere; + Sphere worldBoundingSphere{}; // If set - mesh will be drawn with overrideMaterialId // instead of whatever material the mesh has @@ -25,7 +25,7 @@ struct MeshDrawCommand { // skinned meshes only const SkinnedMesh* skinnedMesh{nullptr}; - std::uint32_t jointMatricesStartIndex; + std::uint32_t jointMatricesStartIndex{0}; }; #endif //MESHDRAWCOMMAND_H diff --git a/destrum/include/destrum/Graphics/Pipelines/ImguiPass.h b/destrum/include/destrum/Graphics/Pipelines/ImguiPass.h index eae935d..c3b4b97 100644 --- a/destrum/include/destrum/Graphics/Pipelines/ImguiPass.h +++ b/destrum/include/destrum/Graphics/Pipelines/ImguiPass.h @@ -34,6 +34,8 @@ public: [[nodiscard]] bool wantsKeyboard() const; private: + void initVulkanBackend(); + void transitionToColorAttachment( VkCommandBuffer cmd, VkImage image, @@ -50,6 +52,9 @@ private: bool initialized = false; bool frameBegun = false; + bool contextCreated = false; + bool sdlInitialized = false; + bool vulkanInitialized = false; }; #endif // DESTRUM_IMGUI_PASS_H diff --git a/destrum/include/destrum/Graphics/Pipelines/SkinningPipeline.h b/destrum/include/destrum/Graphics/Pipelines/SkinningPipeline.h index 93e83c2..69b6ff1 100644 --- a/destrum/include/destrum/Graphics/Pipelines/SkinningPipeline.h +++ b/destrum/include/destrum/Graphics/Pipelines/SkinningPipeline.h @@ -27,12 +27,16 @@ public: const MeshDrawCommand& dc); void beginDrawing(std::size_t frameIndex); + void flushCurrentFrame( + VkCommandBuffer cmd, + GfxDevice& gfxDevice, + std::size_t frameIndex); std::size_t appendJointMatrices( std::span jointMatrices, std::size_t frameIndex); private: - VkPipelineLayout m_pipelineLayout; + VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE}; std::unique_ptr skinningPipeline; struct PushConstants { VkDeviceAddress jointMatricesBuffer; diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index 91538a2..ec899b5 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.h @@ -113,6 +113,7 @@ private: std::unique_ptr skyboxPipeline; std::unique_ptr skinningPipeline; + bool initialized{false}; }; #endif //RENDERER_H diff --git a/destrum/include/destrum/Graphics/Resources/Buffer.h b/destrum/include/destrum/Graphics/Resources/Buffer.h index d005a0c..caf848c 100644 --- a/destrum/include/destrum/Graphics/Resources/Buffer.h +++ b/destrum/include/destrum/Graphics/Resources/Buffer.h @@ -6,10 +6,11 @@ struct GPUBuffer { VkBuffer buffer{VK_NULL_HANDLE}; - VmaAllocation allocation; - VmaAllocationInfo info; + VmaAllocation allocation{VK_NULL_HANDLE}; + VmaAllocationInfo info{}; VkDeviceAddress address{0}; + bool hostVisible{false}; }; #endif //BUFFER_H diff --git a/destrum/include/destrum/Graphics/Resources/NBuffer.h b/destrum/include/destrum/Graphics/Resources/NBuffer.h index caaa9ac..b0d071c 100644 --- a/destrum/include/destrum/Graphics/Resources/NBuffer.h +++ b/destrum/include/destrum/Graphics/Resources/NBuffer.h @@ -7,6 +7,7 @@ #include class GfxDevice; +class MemoryManager; class NBuffer { public: @@ -33,6 +34,7 @@ private: std::size_t gpuBufferSize{0}; std::vector stagingBuffers; GPUBuffer gpuBuffer; + const MemoryManager* memoryManager{nullptr}; bool initialized{false}; }; diff --git a/destrum/include/destrum/Graphics/Swapchain.h b/destrum/include/destrum/Graphics/Swapchain.h index efbf796..ec413f4 100644 --- a/destrum/include/destrum/Graphics/Swapchain.h +++ b/destrum/include/destrum/Graphics/Swapchain.h @@ -3,57 +3,93 @@ #include #include +#include #include -#include "VkBootstrap.h" +#include -class GfxDevice; static constexpr int FRAMES_IN_FLIGHT = 2; class Swapchain { public: + struct AcquireResult { + VkResult result{VK_SUCCESS}; + VkImage image{VK_NULL_HANDLE}; + std::uint32_t imageIndex{0}; + + [[nodiscard]] bool hasImage() const { + return image != VK_NULL_HANDLE && + (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR); + } + }; + void initSync(VkDevice device); //Ye ye, passing a pointer is bad bla bla @Kobe - void createSwapchain(GfxDevice* gfxDevice, VkFormat format, std::uint32_t width, std::uint32_t height, bool vSync); - void recreateSwapchain(const GfxDevice& gfxDevice, VkFormat format, std::uint32_t width, std::uint32_t height, bool vSync); - void cleanup(); + void createSwapchain( + VkDevice device, + vkb::Device vkbDevice, + VkSurfaceKHR surface, + VkFormat format, + std::uint32_t width, + std::uint32_t height, + bool vSync); + + void recreateSwapchain( + VkDevice device, + vkb::Device vkbDevice, + VkSurfaceKHR surface, + VkFormat format, + std::uint32_t width, + std::uint32_t height, + bool vSync); + + void cleanup(VkDevice device); [[nodiscard]] VkExtent2D getExtent() const { return extent; } [[nodiscard]] const std::vector& getImages() const { return images; } [[nodiscard]] std::uint32_t getImageCount() const { return static_cast(images.size()); } - void beginFrame(int index) const; + void beginFrame(VkDevice device, int index) const; - void resetFences(int index) const; + void resetFences(VkDevice device, int index) const; - std::pair acquireNextImage(int index); + AcquireResult acquireNextImage(VkDevice device, std::uint32_t index); - void submitAndPresent(VkCommandBuffer cmd, VkQueue graphicsQueue, std::uint32_t imageIndex, std::uint32_t frameIndex); + void submitAndPresent( + VkDevice device, + VkCommandBuffer cmd, + VkQueue graphicsQueue, + VkQueue presentQueue, + std::uint32_t imageIndex, + std::uint32_t frameIndex); [[nodiscard]] bool isDirty() const { return dirty; } [[nodiscard]] VkImageView getImageView(int index) const { return imageViews[index]; } - - VkImageView getImageView(std::uint32_t imageIndex) const { + [[nodiscard]] VkImageView getImageView(std::uint32_t imageIndex) const { return imageViews.at(imageIndex); } + [[nodiscard]] VkSurfaceKHR getSurface() const { return surface; } + [[nodiscard]] vkb::Swapchain getVkbSwapchain() const { return m_swapchain; } + [[nodiscard]] VkFormat getFormat() const { return m_swapchain.image_format; } + private: struct FrameData { - VkSemaphore swapchainSemaphore; - VkFence renderFence; + VkSemaphore swapchainSemaphore{VK_NULL_HANDLE}; + VkFence renderFence{VK_NULL_HANDLE}; }; - std::vector imageRenderSemaphores; + std::vector imageRenderSemaphores; std::array frames; - vkb::Swapchain m_swapchain; //Euuuh, m_ cuz like cpluhpluh + vkb::Swapchain m_swapchain; std::vector images; std::vector imageViews; bool dirty{false}; - GfxDevice* m_gfxDevice{nullptr}; + VkSurfaceKHR surface{VK_NULL_HANDLE}; VkExtent2D extent{}; }; -#endif //SWAPCHAIN_H +#endif diff --git a/destrum/include/destrum/Graphics/Util.h b/destrum/include/destrum/Graphics/Util.h index 326a431..48017df 100644 --- a/destrum/include/destrum/Graphics/Util.h +++ b/destrum/include/destrum/Graphics/Util.h @@ -3,16 +3,31 @@ #include #include +#include +#include +#include #include #include "glm/vec4.hpp" -#define VK_CHECK(call) \ - do { \ - VkResult result_ = call; \ - assert(result_ == VK_SUCCESS); \ +inline void checkVkResult(VkResult result, const char* expression, const char* file, int line) +{ + if (result == VK_SUCCESS) { + return; + } + + throw std::runtime_error( + std::string{"Vulkan call failed: "} + expression + + " returned " + std::to_string(static_cast(result)) + + " at " + file + ":" + std::to_string(line)); +} + +#define VK_CHECK(call) \ + do { \ + const VkResult result_ = (call); \ + checkVkResult(result_, #call, __FILE__, __LINE__); \ } while (0) namespace vkutil { @@ -34,6 +49,13 @@ namespace vkutil { VkImageLayout currentLayout, VkImageLayout newLayout); + void transitionImage( + VkCommandBuffer cmd, + VkImage image, + VkImageLayout currentLayout, + VkImageLayout newLayout, + const VkImageSubresourceRange& subresourceRange); + void copyImageToImage( VkCommandBuffer cmd, VkImage source, @@ -59,6 +81,12 @@ namespace vkutil { VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE); + void bufferHostWriteToTransferReadBarrier( + VkCommandBuffer cmd, + VkBuffer buffer, + VkDeviceSize offset = 0, + VkDeviceSize size = VK_WHOLE_SIZE); + void addDebugLabel(VkDevice device, VkImage image, const char* label); void addDebugLabel(VkDevice device, VkImageView imageView, const char* label); void addDebugLabel(VkDevice device, VkShaderModule shaderModule, const char* label); @@ -76,9 +104,59 @@ namespace vkutil { }; struct RenderInfo { - VkRenderingAttachmentInfo colorAttachment; - VkRenderingAttachmentInfo depthAttachment; - VkRenderingInfo renderingInfo; + VkRenderingAttachmentInfo colorAttachment{}; + VkRenderingAttachmentInfo depthAttachment{}; + VkRenderingInfo renderingInfo{}; + + RenderInfo() = default; + + RenderInfo(const RenderInfo& other) + : colorAttachment(other.colorAttachment), + depthAttachment(other.depthAttachment), + renderingInfo(other.renderingInfo) + { + rebind(); + } + + RenderInfo& operator=(const RenderInfo& other) + { + if (this != &other) { + colorAttachment = other.colorAttachment; + depthAttachment = other.depthAttachment; + renderingInfo = other.renderingInfo; + rebind(); + } + return *this; + } + + RenderInfo(RenderInfo&& other) noexcept + : colorAttachment(other.colorAttachment), + depthAttachment(other.depthAttachment), + renderingInfo(other.renderingInfo) + { + rebind(); + } + + RenderInfo& operator=(RenderInfo&& other) noexcept + { + if (this != &other) { + colorAttachment = other.colorAttachment; + depthAttachment = other.depthAttachment; + renderingInfo = other.renderingInfo; + rebind(); + } + return *this; + } + + void rebind() + { + renderingInfo.pColorAttachments = renderingInfo.colorAttachmentCount != 0 + ? &colorAttachment + : nullptr; + renderingInfo.pDepthAttachment = renderingInfo.pDepthAttachment != nullptr + ? &depthAttachment + : nullptr; + } }; RenderInfo createRenderingInfo(const RenderingInfoParams& params); diff --git a/destrum/include/destrum/ObjectModel/Component.h b/destrum/include/destrum/ObjectModel/Component.h index 9507a5c..17a46a9 100644 --- a/destrum/include/destrum/ObjectModel/Component.h +++ b/destrum/include/destrum/ObjectModel/Component.h @@ -33,9 +33,11 @@ public: virtual void Start(); virtual void SetEnabled(bool enabled); - virtual void Update() = 0; - virtual void LateUpdate(); - virtual void FixedUpdate(); + // Reason for keeping No DT function -> i am tired and lazy + // If im feeling frisky im gonna claim backwards compatibility + virtual void Update(float dt); + virtual void LateUpdate(float dt); + virtual void FixedUpdate(float fixedDt); virtual void ImGuiInspector(); virtual void ImGuiRender(); @@ -49,10 +51,10 @@ public: return {}; } - virtual void Deserialize(const nlohmann::json& data) { + virtual void Deserialize(const nlohmann::json&) { } - virtual void ResolveReferences(const ObjectMap& objects) { + virtual void ResolveReferences(const ObjectMap&) { } bool HasStarted{false}; @@ -60,9 +62,15 @@ public: protected: explicit Component(GameObject& pParent, const std::string& name = "Component"); + [[nodiscard]] float GetLastUpdateDeltaTime() const { return m_LastUpdateDeltaTime; } + [[nodiscard]] float GetLastFixedDeltaTime() const { return m_LastFixedDeltaTime; } + void NotifyPhysicsChanged(); + private: GameObject* m_ParentGameObjectPtr{}; bool m_IsEnabled{true}; + float m_LastUpdateDeltaTime{0.0f}; + float m_LastFixedDeltaTime{0.0f}; }; -#endif // COMPONENT_H \ No newline at end of file +#endif // COMPONENT_H diff --git a/destrum/include/destrum/ObjectModel/GameObject.h b/destrum/include/destrum/ObjectModel/GameObject.h index 09dc2a8..75868eb 100644 --- a/destrum/include/destrum/ObjectModel/GameObject.h +++ b/destrum/include/destrum/ObjectModel/GameObject.h @@ -10,20 +10,24 @@ #include class Scene; +class SceneSerializer; class GameObject final : public Object { public: friend class Scene; + friend class SceneSerializer; + friend class Transform; - void Update(); - void LateUpdate(); - void FixedUpdate(); + void Update(float dt); + void LateUpdate(float dt); + void FixedUpdate(float fixedDt); void Render(const RenderContext& ctx) const; void Destroy() override; void CleanupComponents(); + void RefreshPhysics(); void SetActive(bool active) { if (active == m_Active) { @@ -32,6 +36,7 @@ public: m_Active = active; SetActiveDirty(); + RefreshPhysics(); } [[nodiscard]] bool IsActive() const { return m_Active; } @@ -71,14 +76,17 @@ public: auto& addedComponent = m_Components.emplace_back( std::make_unique(*this, std::forward(args)...)); + RefreshPhysics(); return static_cast(addedComponent.get()); } template [[nodiscard]] TComponent* GetComponent() { for (const auto& component : m_Components) { - if (auto casted = dynamic_cast(component.get())) { - return casted; + if (component != nullptr && !component->IsBeingDestroyed()) { + if (auto casted = dynamic_cast(component.get())) { + return casted; + } } } return nullptr; @@ -87,8 +95,10 @@ public: template [[nodiscard]] const TComponent* GetComponent() const { for (const auto& component : m_Components) { - if (auto casted = dynamic_cast(component.get())) { - return casted; + if (component != nullptr && !component->IsBeingDestroyed()) { + if (auto casted = dynamic_cast(component.get())) { + return casted; + } } } return nullptr; @@ -97,9 +107,11 @@ public: template TComponent* DestroyComponent() { for (const auto& component : m_Components) { - if (auto casted = dynamic_cast(component.get())) { - casted->Destroy(); - return casted; + if (component != nullptr && !component->IsBeingDestroyed()) { + if (auto casted = dynamic_cast(component.get())) { + casted->Destroy(); + return casted; + } } } return nullptr; @@ -108,7 +120,8 @@ public: template [[nodiscard]] bool HasComponent() const { for (const auto& component : m_Components) { - if (dynamic_cast(component.get())) { + if (component != nullptr && !component->IsBeingDestroyed() && + dynamic_cast(component.get())) { return true; } } @@ -145,6 +158,7 @@ private: void SetActiveDirty(); void UpdateActiveState(); + static ObjectId AllocateId(); inline static ObjectId s_NextId{1}; @@ -157,4 +171,4 @@ private: bool m_ActiveDirty{true}; bool m_ActiveInHierarchy{true}; -}; \ No newline at end of file +}; diff --git a/destrum/include/destrum/ObjectModel/Transform.h b/destrum/include/destrum/ObjectModel/Transform.h index 4361372..30ad012 100644 --- a/destrum/include/destrum/ObjectModel/Transform.h +++ b/destrum/include/destrum/ObjectModel/Transform.h @@ -73,6 +73,9 @@ private: void SetRotationDirty(); void SetScaleDirty(); + void SetLocalFromWorldMatrix(const glm::mat4& worldMatrix); + void UpdateWorldCache(); + glm::vec3 m_LocalPosition{}; glm::quat m_LocalRotation{ glm::mat4{ 1.0f }}; glm::vec3 m_LocalScale{ 1, 1, 1 }; diff --git a/destrum/include/destrum/Physics/PhysicsSceneBridge.h b/destrum/include/destrum/Physics/PhysicsSceneBridge.h index 9047b7e..f4aa49a 100644 --- a/destrum/include/destrum/Physics/PhysicsSceneBridge.h +++ b/destrum/include/destrum/Physics/PhysicsSceneBridge.h @@ -25,6 +25,7 @@ public: [[nodiscard]] const PhysicsWorld& GetWorld() const { return *m_World; } void RegisterGameObject(GameObject& object); + void RefreshGameObject(GameObject& object); void UnregisterGameObject(GameObject& object); void FixedUpdate(float fixedDt); diff --git a/destrum/include/destrum/Physics/PhysicsWorld.h b/destrum/include/destrum/Physics/PhysicsWorld.h index c966f9c..ed48915 100644 --- a/destrum/include/destrum/Physics/PhysicsWorld.h +++ b/destrum/include/destrum/Physics/PhysicsWorld.h @@ -1,6 +1,9 @@ #pragma once #include + +#include + #include #include @@ -16,9 +19,10 @@ struct PhysicsRaycastHit { class PhysicsWorld { public: - virtual ~PhysicsWorld() = default; + virtual ~PhysicsWorld(); void RegisterRigidbody(Rigidbody& rigidbody); + void RefreshRigidbody(Rigidbody& rigidbody); void UnregisterRigidbody(Rigidbody& rigidbody); virtual void Step(float fixedDt) = 0; @@ -44,4 +48,7 @@ public: const glm::vec3& direction, float maxDistance, PhysicsRaycastHit& hit) const = 0; + +private: + std::unordered_set m_RegisteredRigidbodies; }; diff --git a/destrum/include/destrum/Scene/Scene.h b/destrum/include/destrum/Scene/Scene.h index 02f1b61..5f688dd 100644 --- a/destrum/include/destrum/Scene/Scene.h +++ b/destrum/include/destrum/Scene/Scene.h @@ -10,9 +10,11 @@ #include "destrum/Physics/PhysicsSceneBridge.h" class GameObject; +class SceneSerializer; class Scene final { friend Scene& SceneManager::CreateScene(const std::string& name); + friend class SceneSerializer; public: // void Add(std::shared_ptr object); @@ -23,10 +25,10 @@ public: void Load(); - void Update(); + void Update(float dt); void FixedUpdate(float dt); - void LateUpdate(); - void Render(const RenderContext& ctx) const; + void LateUpdate(float dt); + void Render(const RenderContext& ctx); void RenderImgui(); void CleanupDestroyedGameObjects(); @@ -36,6 +38,11 @@ public: [[nodiscard]] bool IsBeingUnloaded() const { return m_BeingUnloaded; } void CommitPendingAdditions(); + void RefreshPhysics(); + + [[nodiscard]] bool IsIterating() const { return m_IterationDepth != 0; } + void BeginIteration(); + void EndIteration(); [[nodiscard]] const std::vector>& GetObjects() const { return m_objects; @@ -50,20 +57,40 @@ public: } void UnloadBindings() { + if (!m_bindingsLoaded) { + m_bindingsUnloaded = true; + return; + } if (m_unregisterBindings) { m_unregisterBindings(); } + m_bindingsLoaded = false; + m_bindingsUnloaded = true; } void LoadBindings() { - OnSceneLoaded.Invoke(); - if (m_registerBindings) { - m_registerBindings(); + if (m_BeingUnloaded || m_bindingsLoaded) { + return; + } + m_bindingsLoaded = true; + m_bindingsUnloaded = false; + try { + OnSceneLoaded.Invoke(); + if (m_registerBindings) { + m_registerBindings(); + } + } catch (...) { + if (m_unregisterBindings) { + m_unregisterBindings(); + } + m_bindingsLoaded = false; + m_bindingsUnloaded = true; + throw; } } [[nodiscard]] const std::string& GetName() const { return m_name; } - [[nodiscard]] unsigned int GetId() const { return m_idCounter++; } + [[nodiscard]] unsigned int GetId() const { return m_id; } ~Scene(); Scene(const Scene& other) = delete; @@ -84,8 +111,10 @@ private: std::vector> m_objects{}; std::vector> m_pendingAdditions{}; bool m_BeingUnloaded{false}; + std::size_t m_IterationDepth{0}; static unsigned int m_idCounter; + unsigned int m_id{0}; bool m_renderImgui{false}; @@ -94,6 +123,8 @@ private: std::function m_registerBindings; std::function m_unregisterBindings; + bool m_bindingsUnloaded{false}; + bool m_bindingsLoaded{false}; }; #endif // SCENE_H diff --git a/destrum/include/destrum/Scene/SceneManager.h b/destrum/include/destrum/Scene/SceneManager.h index b63c811..8c23758 100644 --- a/destrum/include/destrum/Scene/SceneManager.h +++ b/destrum/include/destrum/Scene/SceneManager.h @@ -15,12 +15,11 @@ class SceneManager final: public Singleton { public: Scene& CreateScene(const std::string& name); - //TODO: Verry bad fix - Scene& GetCurrentScene() const { return *m_scenes[m_ActiveSceneIndex]; } + Scene& GetCurrentScene() const; - void Update(); + void Update(float dt); void FixedUpdate(float dt); - void LateUpdate(); + void LateUpdate(float dt); void Render(const RenderContext& ctx); void RenderImgui(); @@ -35,7 +34,7 @@ public: void Destroy(); void SwitchScene(int index); - int GetActiveSceneId() const { return m_ActiveSceneIndex; } + int GetActiveSceneId() const { return m_scenes.empty() ? -1 : m_ActiveSceneIndex; } [[nodiscard]] const std::vector>& GetScenes() const { return m_scenes; } diff --git a/destrum/include/destrum/Serialization/ComponentFactory.h b/destrum/include/destrum/Serialization/ComponentFactory.h index 73dd4c8..0d86f86 100644 --- a/destrum/include/destrum/Serialization/ComponentFactory.h +++ b/destrum/include/destrum/Serialization/ComponentFactory.h @@ -27,6 +27,10 @@ public: return it->second(owner); } + [[nodiscard]] static bool IsRegistered(const std::string& typeName) { + return Registry().contains(typeName); + } + private: static std::unordered_map& Registry() { static std::unordered_map registry; diff --git a/destrum/include/destrum/Util/GameState.h b/destrum/include/destrum/Util/GameState.h index 3559071..435a763 100644 --- a/destrum/include/destrum/Util/GameState.h +++ b/destrum/include/destrum/Util/GameState.h @@ -13,6 +13,7 @@ public: friend class Singleton; void SetGfxDevice(GfxDevice* device) { m_gfxDevice = device; } + [[nodiscard]] bool HasGfxDevice() const { return m_gfxDevice != nullptr; } GfxDevice& Gfx() { assert(m_gfxDevice && "GfxDevice not registered yet!"); return *m_gfxDevice; @@ -23,6 +24,7 @@ public: } void SetRenderer(GameRenderer* renderer) { m_renderer = renderer; } + [[nodiscard]] bool HasRenderer() const { return m_renderer != nullptr; } GameRenderer& Renderer() { assert(m_renderer && "Renderer not registered yet!"); return *m_renderer; diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index c722488..6288578 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -1,9 +1,12 @@ #include +#include #include #include +#include #include #include +#include #include #include "imgui.h" @@ -25,41 +28,60 @@ App::App() { } +App::~App() +{ + if (!cleanedUp) { + try { + cleanup(); + } catch (...) { + // Destructors must not throw. Initialization failures are already + // reported by the caller, while cleanup is best effort here. + } + } +} + void App::init(const AppParams& params) { - m_params = params; - ZoneScopedN("App::init"); - tracy::SetThreadName("Main Thread"); - TracySetProgramName(params.appName.c_str()); - AssetFS::GetInstance().Init(params.exeDir); - // AssetFS::GetInstance().Mount("engine", params.exeDir / "assets" / "engine"); - // AssetFS::GetInstance().Mount("game", params.exeDir / "assets" / "game"); + cleanedUp = false; + customInitStarted = false; + try { + m_params = params; + ZoneScopedN("App::init"); + tracy::SetThreadName("Main Thread"); + TracySetProgramName(params.appName.c_str()); + AssetFS::GetInstance().Init(params.exeDir); - window = SDL_CreateWindow( - params.windowTitle.c_str(), - // pos - SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, - // size - params.windowSize.x, - params.windowSize.y, - SDL_WINDOW_VULKAN); + window = SDL_CreateWindow( + params.windowTitle.c_str(), + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + params.windowSize.x, + params.windowSize.y, + SDL_WINDOW_VULKAN); - SDL_SetWindowResizable(window, SDL_TRUE); - if (!window) - { - spdlog::error("Failed to create window. SDL Error: {}", SDL_GetError()); - std::exit(1); + if (!window) { + spdlog::error("Failed to create window. SDL Error: {}", SDL_GetError()); + throw std::runtime_error( + "Failed to create window: " + std::string{SDL_GetError()}); + } + SDL_SetWindowResizable(window, SDL_TRUE); + + gfxDevice.init(window, params.appName, false); + imguiPass.init(window, gfxDevice); + + InputManager::GetInstance().Init(); + Time::GetInstance().Update(); + + customInitStarted = true; + customInit(); + cleanedUp = false; + } catch (...) { + try { + cleanup(); + } catch (...) { + } + throw; } - - gfxDevice.init(window, params.appName, false); - imguiPass.init(window, gfxDevice); - - - InputManager::GetInstance().Init(); - Time::GetInstance().Update(); - - customInit(); } void App::run() @@ -95,9 +117,8 @@ void App::run() accumulator += dt; { - ZoneScopedN("Input BeginFrame + Camera"); + ZoneScopedN("Input BeginFrame"); InputManager::GetInstance().BeginFrame(); - camera.Update(dt); } { @@ -157,6 +178,10 @@ void App::run() if (!isRunning) break; + // Consume SDL events before updating the base camera so held and + // newly pressed inputs are applied in the same frame. + camera.Update(dt); + { ZoneScopedN("ImGui BeginFrame"); imguiPass.beginFrame(); @@ -258,6 +283,7 @@ void App::run() { ZoneScopedN("Recreate Swapchain"); gfxDevice.recreateSwapchain(w, h); + imguiPass.onSwapchainRecreated(); onWindowResize(w, h); } @@ -288,8 +314,46 @@ void App::run() void App::cleanup() { + if (cleanedUp || cleaningUp) { + return; + } + + cleaningUp = true; spdlog::info("Cleaning up"); - customCleanup(); + + std::exception_ptr firstException; + const auto attempt = [&firstException](auto&& operation) { + try { + operation(); + } catch (...) { + if (!firstException) { + firstException = std::current_exception(); + } + } + }; + + if (customInitStarted) { + attempt([this] { customCleanup(); }); + } + attempt([] { SceneManager::GetInstance().Destroy(); }); + attempt([this] { renderer.cleanup(gfxDevice); }); + attempt([this] { imguiPass.cleanup(); }); + attempt([this] { resources.cleanup(gfxDevice); }); + attempt([this] { gfxDevice.cleanup(); }); + + if (window) { + SDL_DestroyWindow(window); + window = nullptr; + } + + AssetFS::GetInstance().Reset(); + customInitStarted = false; + cleanedUp = true; + cleaningUp = false; + + if (firstException) { + std::rethrow_exception(firstException); + } } void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator) @@ -336,4 +400,4 @@ void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator) } ImGui::End(); -} \ No newline at end of file +} diff --git a/destrum/src/Components/Animator.cpp b/destrum/src/Components/Animator.cpp index 2dabb17..e8cc486 100644 --- a/destrum/src/Components/Animator.cpp +++ b/destrum/src/Components/Animator.cpp @@ -1,33 +1,247 @@ #include #include #include +#include #include #include -#include #include "spdlog/spdlog.h" +namespace { + nlohmann::json Vec3Json(const glm::vec3& value) { + return {value.x, value.y, value.z}; + } + + glm::vec3 ReadVec3(const nlohmann::json& value) { + return {value.at(0).get(), value.at(1).get(), value.at(2).get()}; + } + + nlohmann::json QuatJson(const glm::quat& value) { + return {value.x, value.y, value.z, value.w}; + } + + glm::quat ReadQuat(const nlohmann::json& value) { + return { + value.at(3).get(), + value.at(0).get(), + value.at(1).get(), + value.at(2).get() + }; + } + + nlohmann::json Mat4Json(const glm::mat4& value) { + nlohmann::json result = nlohmann::json::array(); + for (std::size_t column = 0; column < 4; ++column) { + for (std::size_t row = 0; row < 4; ++row) { + result.push_back(value[static_cast(column)] + [static_cast(row)]); + } + } + return result; + } + + glm::mat4 ReadMat4(const nlohmann::json& value) { + glm::mat4 result{1.0f}; + for (std::size_t column = 0; column < 4; ++column) { + for (std::size_t row = 0; row < 4; ++row) { + result[static_cast(column)] + [static_cast(row)] = + value.at(column * 4 + row).get(); + } + } + return result; + } +} + Animator::Animator(GameObject& parent) : Component(parent, "Animator") {} -void Animator::Update() { +nlohmann::json Animator::Serialize() const { + nlohmann::json skeleton; + skeleton["hierarchy"] = nlohmann::json::array(); + for (const auto& node : m_skeleton.hierarchy) { + skeleton["hierarchy"].push_back({ + {"id", node.id}, + {"children", node.children} + }); + } + skeleton["inverseBindMatrices"] = nlohmann::json::array(); + for (const auto& matrix : m_skeleton.inverseBindMatrices) { + skeleton["inverseBindMatrices"].push_back(Mat4Json(matrix)); + } + skeleton["joints"] = nlohmann::json::array(); + for (const auto& joint : m_skeleton.joints) { + skeleton["joints"].push_back({ + {"id", joint.id}, + {"translation", Vec3Json(joint.localTranslation)}, + {"rotation", QuatJson(joint.localRotation)}, + {"scale", Vec3Json(joint.localScale)} + }); + } + skeleton["jointNames"] = m_skeleton.jointNames; + skeleton["parentIndex"] = m_skeleton.parentIndex; + skeleton["rootPreTransform"] = Mat4Json(m_skeleton.rootPreTransform); + + nlohmann::json clips = nlohmann::json::array(); + for (const auto& [name, clip] : m_clips) { + nlohmann::json clipJson{ + {"name", name}, + {"duration", clip->duration}, + {"looped", clip->looped}, + {"startFrame", clip->startFrame}, + {"tracks", nlohmann::json::array()}, + {"events", nlohmann::json::object()} + }; + for (const auto& track : clip->tracks) { + nlohmann::json trackJson{ + {"jointIndex", track.jointIndex}, + {"keyframes", nlohmann::json::array()} + }; + for (const auto& keyframe : track.keyframes) { + trackJson["keyframes"].push_back({ + {"time", keyframe.time}, + {"translation", Vec3Json(keyframe.translation)}, + {"rotation", QuatJson(keyframe.rotation)}, + {"scale", Vec3Json(keyframe.scale)} + }); + } + clipJson["tracks"].push_back(std::move(trackJson)); + } + for (const auto& [frame, events] : clip->events) { + clipJson["events"][std::to_string(frame)] = events; + } + clips.push_back(std::move(clipJson)); + } + + const auto playback = [](const PlaybackState& state, const std::string& name) { + return nlohmann::json{ + {"clip", name}, + {"time", state.time}, + {"speed", state.speed} + }; + }; + + return { + {"skeleton", std::move(skeleton)}, + {"clips", std::move(clips)}, + {"current", playback(m_current, m_currentClipName)}, + {"previous", playback(m_previous, m_previous.clip ? m_previous.clip->name : std::string{})}, + {"blendT", m_blendT}, + {"blendDuration", m_blendDuration} + }; +} + +void Animator::Deserialize(const nlohmann::json& data) { + m_skeleton = {}; + m_clips.clear(); + m_current = {}; + m_previous = {}; + m_currentClipName.clear(); + + if (data.contains("skeleton")) { + const auto& skeleton = data.at("skeleton"); + for (const auto& node : skeleton.value("hierarchy", nlohmann::json::array())) { + m_skeleton.hierarchy.push_back({ + node.at("id").get(), + node.value("children", std::vector{}) + }); + } + for (const auto& matrix : skeleton.value("inverseBindMatrices", nlohmann::json::array())) { + m_skeleton.inverseBindMatrices.push_back(ReadMat4(matrix)); + } + for (const auto& joint : skeleton.value("joints", nlohmann::json::array())) { + m_skeleton.joints.push_back({ + joint.at("id").get(), + ReadVec3(joint.at("translation")), + ReadQuat(joint.at("rotation")), + ReadVec3(joint.at("scale")) + }); + } + m_skeleton.jointNames = skeleton.value("jointNames", std::vector{}); + m_skeleton.parentIndex = skeleton.value("parentIndex", std::vector{}); + if (skeleton.contains("rootPreTransform")) { + m_skeleton.rootPreTransform = ReadMat4(skeleton.at("rootPreTransform")); + } + if (m_skeleton.parentIndex.size() != m_skeleton.joints.size()) { + buildParentIndex(m_skeleton); + } + } + + for (const auto& clipJson : data.value("clips", nlohmann::json::array())) { + auto clip = std::make_shared(); + clip->name = clipJson.at("name").get(); + clip->duration = clipJson.value("duration", 0.0f); + clip->looped = clipJson.value("looped", true); + clip->startFrame = clipJson.value("startFrame", 0); + for (const auto& trackJson : clipJson.value("tracks", nlohmann::json::array())) { + SkeletalAnimation::Track track; + track.jointIndex = trackJson.at("jointIndex").get(); + for (const auto& keyframeJson : trackJson.value("keyframes", nlohmann::json::array())) { + track.keyframes.push_back({ + keyframeJson.at("time").get(), + ReadVec3(keyframeJson.at("translation")), + ReadQuat(keyframeJson.at("rotation")), + ReadVec3(keyframeJson.at("scale")) + }); + } + clip->tracks.push_back(std::move(track)); + } + if (clipJson.contains("events")) { + for (auto it = clipJson.at("events").begin(); it != clipJson.at("events").end(); ++it) { + clip->events[std::stoi(it.key())] = it.value().get>(); + } + } + m_clips[clip->name] = std::move(clip); + } + + const auto restorePlayback = [this](const nlohmann::json& playback, PlaybackState& state, + std::string* clipName) { + const std::string name = playback.value("clip", std::string{}); + state.time = playback.value("time", 0.0f); + state.speed = playback.value("speed", 1.0f); + state.clip = nullptr; + if (!name.empty()) { + const auto it = m_clips.find(name); + if (it == m_clips.end()) { + throw std::runtime_error("Animator clip not found: " + name); + } + state.clip = it->second.get(); + } + if (clipName != nullptr) { + *clipName = name; + } + }; + + if (data.contains("current")) { + restorePlayback(data.at("current"), m_current, &m_currentClipName); + } + if (data.contains("previous")) { + restorePlayback(data.at("previous"), m_previous, nullptr); + } + m_blendT = data.value("blendT", 0.0f); + m_blendDuration = data.value("blendDuration", 0.0f); +} + +void Animator::Update(float dt) { if (!m_current.clip) return; - const float dt = Time::GetInstance().DeltaTime(); - m_current.time += dt * m_current.speed; - if (m_current.clip->looped) + if (m_current.clip->duration > 0.0f && m_current.clip->looped) m_current.time = std::fmod(m_current.time, m_current.clip->duration); - else + else if (m_current.clip->duration > 0.0f) m_current.time = std::min(m_current.time, m_current.clip->duration); if (m_previous.clip) { m_previous.time += dt * m_previous.speed; - if (m_previous.clip->looped) + if (m_previous.clip->duration > 0.0f && m_previous.clip->looped) m_previous.time = std::fmod(m_previous.time, m_previous.clip->duration); - m_blendT += dt / m_blendDuration; + if (m_blendDuration > 0.0f) { + m_blendT += dt / m_blendDuration; + } else { + m_blendT = 1.0f; + } if (m_blendT >= 1.f) { m_blendT = 1.f; m_previous = {}; @@ -184,4 +398,4 @@ glm::vec3 Animator::sampleScale(const SkeletalAnimation::Track& track, float t) } } return kf.back().scale; -} \ No newline at end of file +} diff --git a/destrum/src/Components/MeshRendererComponent.cpp b/destrum/src/Components/MeshRendererComponent.cpp index b8c6059..d1e4a97 100644 --- a/destrum/src/Components/MeshRendererComponent.cpp +++ b/destrum/src/Components/MeshRendererComponent.cpp @@ -5,6 +5,7 @@ #include "destrum/ObjectModel/GameObject.h" #include "destrum/Util/GameState.h" +#include MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") { @@ -12,7 +13,11 @@ MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(pare void MeshRendererComponent::Start() { Component::Start(); - if (auto* animator = GetGameObject()->GetComponent()) { + ResolveReferences(ObjectMap{}); + if (GetGameObject()->GetComponent() && + meshID != NULL_MESH_ID && + GameState::GetInstance().HasGfxDevice() && + GameState::GetInstance().HasRenderer()) { const auto& gfxDevice = GameState::GetInstance().Gfx(); const auto& mesh = GameState::GetInstance().Renderer().getResources()->meshes().getMesh(meshID); @@ -26,14 +31,101 @@ void MeshRendererComponent::Start() { } } -void MeshRendererComponent::Update() { +void MeshRendererComponent::Destroy() +{ + if (m_skinnedMesh && GameState::GetInstance().HasGfxDevice()) { + auto& gfxDevice = GameState::GetInstance().Gfx(); + gfxDevice.waitIdle(); + gfxDevice.destroyBuffer(m_skinnedMesh->skinnedVertexBuffer); + } + m_skinnedMesh.reset(); + Component::Destroy(); +} + +void MeshRendererComponent::Update(float) { +} + +nlohmann::json MeshRendererComponent::Serialize() const { + return { + {"meshId", meshID}, + {"materialId", materialID}, + {"meshKey", meshKey}, + {"materialKey", materialKey} + }; +} + +void MeshRendererComponent::Deserialize(const nlohmann::json& data) { + if (data.contains("meshId")) { + meshID = data.at("meshId").get(); + } + if (data.contains("materialId")) { + materialID = data.at("materialId").get(); + } + if (data.contains("meshKey")) { + meshKey = data.at("meshKey").get(); + } + if (data.contains("materialKey")) { + materialKey = data.at("materialKey").get(); + } +} + +void MeshRendererComponent::ResolveReferences(const ObjectMap&) { + if (!GameState::GetInstance().HasRenderer()) { + return; + } + + RenderResources* resources = GameState::GetInstance().Renderer().getResources(); + if (resources == nullptr) { + return; + } + + if (!meshKey.empty()) { + const auto resolved = resources->meshes().findMeshByKey(meshKey); + if (!resolved) { + throw std::runtime_error("Mesh resource not found: " + meshKey); + } + meshID = *resolved; + } + if (!materialKey.empty()) { + const auto resolved = resources->materials().findMaterialByKey(materialKey); + if (!resolved) { + throw std::runtime_error("Material resource not found: " + materialKey); + } + materialID = *resolved; + } + + if (meshID != NULL_MESH_ID && meshKey.empty()) { + (void)resources->meshes().getMesh(meshID); + } + if (materialID != NULL_MATERIAL_ID && materialKey.empty()) { + (void)resources->materials().getMaterial(materialID); + } +} + +void MeshRendererComponent::SetMeshID(MeshID id) { + meshID = id; + meshKey.clear(); + if (id != NULL_MESH_ID && GameState::GetInstance().HasRenderer()) { + if (auto* resources = GameState::GetInstance().Renderer().getResources()) { + meshKey = resources->meshes().getMeshKey(id); + } + } +} + +void MeshRendererComponent::SetMaterialID(MaterialID id) { + materialID = id; + materialKey.clear(); + if (id != NULL_MATERIAL_ID && GameState::GetInstance().HasRenderer()) { + if (auto* resources = GameState::GetInstance().Renderer().getResources()) { + materialKey = resources->materials().getMaterialKey(id); + } + } } void MeshRendererComponent::Render(const RenderContext& ctx) { if (meshID == NULL_MESH_ID || materialID == NULL_MATERIAL_ID) return; if (auto* animator = GetGameObject()->GetComponent(); animator && m_skinnedMesh) { - const auto& mesh = ctx.renderer.getResources()->meshes().getCPUMesh(meshID); const auto skeleton = GetGameObject()->GetComponent()->getSkeleton(); std::uint32_t frameIdx = GameState::GetInstance().Gfx().getCurrentFrameIndex(); diff --git a/destrum/src/Components/OrbitAndSpin.cpp b/destrum/src/Components/OrbitAndSpin.cpp index 7eba3e6..3381865 100644 --- a/destrum/src/Components/OrbitAndSpin.cpp +++ b/destrum/src/Components/OrbitAndSpin.cpp @@ -8,6 +8,7 @@ #include "destrum/ObjectModel/Transform.h" #include "destrum/Components/MeshRendererComponent.h" #include "destrum/Util/GameState.h" +#include "destrum/Util/DeltaTime.h" static glm::vec3 RandomUnitVector(std::mt19937& rng) { @@ -54,6 +55,9 @@ void OrbitAndSpin::Randomize(uint32_t seed) void OrbitAndSpin::BuildOrbitBasis() { + if (glm::dot(m_OrbitAxis, m_OrbitAxis) < 1e-8f) + m_OrbitAxis = glm::vec3(0, 1, 0); + m_OrbitAxis = glm::normalize(m_OrbitAxis); // pick any vector not parallel to axis @@ -63,9 +67,8 @@ void OrbitAndSpin::BuildOrbitBasis() m_V = glm::normalize(glm::cross(m_OrbitAxis, m_U)); } -void OrbitAndSpin::Update() +void OrbitAndSpin::Update(float dt) { - float dt = 1.0f / 60.0f; // orbit m_OrbitAngle += m_OrbitSpeed * dt; @@ -84,8 +87,6 @@ void OrbitAndSpin::Update() // grow (always positive) m_GrowPhase += m_GrowSpeed * dt; - m_GrowPhase += m_GrowSpeed * dt; - // 0..1 float t = 0.5f * (std::sin(m_GrowPhase) + 1.0f); @@ -93,7 +94,7 @@ void OrbitAndSpin::Update() float s = glm::mix(m_GrowMin, m_GrowMax, t); // respect original scale - GetTransform().SetLocalScale(glm::vec3(s)); + GetTransform().SetLocalScale(m_BaseScale * s); // GetTransform().SetLocalScale(glm::vec3(std::sin(m_GrowPhase))); @@ -109,8 +110,68 @@ void OrbitAndSpin::Update() void OrbitAndSpin::Start() { auto meshComp = this->GetGameObject()->GetComponent(); - m_MaterialID = meshComp->GetMaterialID(); + if (meshComp != nullptr) { + m_MaterialID = meshComp->GetMaterialID(); + } - m_BaseScale = GetTransform().GetLocalScale(); // <-- important + if (!m_BaseScaleLoaded) { + m_BaseScale = GetTransform().GetLocalScale(); + } } + +nlohmann::json OrbitAndSpin::Serialize() const { + return { + {"radius", m_Radius}, + {"center", {m_Center.x, m_Center.y, m_Center.z}}, + {"orbitAxis", {m_OrbitAxis.x, m_OrbitAxis.y, m_OrbitAxis.z}}, + {"orbitSpeed", m_OrbitSpeed}, + {"orbitAngle", m_OrbitAngle}, + {"orbitPhase", m_OrbitPhase}, + {"growPhase", m_GrowPhase}, + {"growSpeed", m_GrowSpeed}, + {"growMin", m_GrowMin}, + {"growMax", m_GrowMax}, + {"spinAxis", {m_SpinAxis.x, m_SpinAxis.y, m_SpinAxis.z}}, + {"spinSpeed", m_SpinSpeed}, + {"baseScale", {m_BaseScale.x, m_BaseScale.y, m_BaseScale.z}}, + {"materialId", m_MaterialID} + }; +} + +void OrbitAndSpin::Deserialize(const nlohmann::json& data) { + const auto readVec3 = [&data](const char* key, glm::vec3& value) { + if (!data.contains(key)) { + return; + } + + const auto& array = data.at(key); + value = {array.at(0).get(), array.at(1).get(), array.at(2).get()}; + }; + + if (data.contains("radius")) m_Radius = data.at("radius").get(); + readVec3("center", m_Center); + readVec3("orbitAxis", m_OrbitAxis); + if (data.contains("orbitSpeed")) m_OrbitSpeed = data.at("orbitSpeed").get(); + if (data.contains("orbitAngle")) m_OrbitAngle = data.at("orbitAngle").get(); + if (data.contains("orbitPhase")) m_OrbitPhase = data.at("orbitPhase").get(); + if (data.contains("growPhase")) m_GrowPhase = data.at("growPhase").get(); + if (data.contains("growSpeed")) m_GrowSpeed = data.at("growSpeed").get(); + if (data.contains("growMin")) m_GrowMin = data.at("growMin").get(); + if (data.contains("growMax")) m_GrowMax = data.at("growMax").get(); + readVec3("spinAxis", m_SpinAxis); + readVec3("baseScale", m_BaseScale); + m_BaseScaleLoaded = data.contains("baseScale"); + if (data.contains("spinSpeed")) m_SpinSpeed = data.at("spinSpeed").get(); + if (data.contains("materialId")) m_MaterialID = data.at("materialId").get(); + + if (m_GrowMin > m_GrowMax) { + std::swap(m_GrowMin, m_GrowMax); + } + if (glm::dot(m_SpinAxis, m_SpinAxis) < 1e-8f) { + m_SpinAxis = glm::vec3(0, 1, 0); + } else { + m_SpinAxis = glm::normalize(m_SpinAxis); + } + BuildOrbitBasis(); +} diff --git a/destrum/src/Components/Physics/BoxCollider.cpp b/destrum/src/Components/Physics/BoxCollider.cpp index d91a30e..9849989 100644 --- a/destrum/src/Components/Physics/BoxCollider.cpp +++ b/destrum/src/Components/Physics/BoxCollider.cpp @@ -1 +1,19 @@ -#include \ No newline at end of file +#include + +nlohmann::json BoxCollider::Serialize() const { + auto data = SerializeCollider(); + data["halfExtents"] = {m_HalfExtents.x, m_HalfExtents.y, m_HalfExtents.z}; + return data; +} + +void BoxCollider::Deserialize(const nlohmann::json& data) { + DeserializeCollider(data); + if (data.contains("halfExtents")) { + const auto& extents = data.at("halfExtents"); + SetHalfExtents({ + extents.at(0).get(), + extents.at(1).get(), + extents.at(2).get() + }); + } +} diff --git a/destrum/src/Components/Physics/Rigidbody.cpp b/destrum/src/Components/Physics/Rigidbody.cpp index 47353d9..86dc1ba 100644 --- a/destrum/src/Components/Physics/Rigidbody.cpp +++ b/destrum/src/Components/Physics/Rigidbody.cpp @@ -1,6 +1,59 @@ #include #include +#include +#include + +namespace { + [[nodiscard]] const char* RigidbodyTypeName(RigidbodyType type) { + switch (type) { + case RigidbodyType::Static: + return "Static"; + case RigidbodyType::Kinematic: + return "Kinematic"; + case RigidbodyType::Dynamic: + default: + return "Dynamic"; + } + } + + [[nodiscard]] RigidbodyType ParseRigidbodyType(const nlohmann::json& value) { + if (value.is_string()) { + const std::string type = value.get(); + if (type == "Static") return RigidbodyType::Static; + if (type == "Kinematic") return RigidbodyType::Kinematic; + return RigidbodyType::Dynamic; + } + + const int type = value.get(); + switch (type) { + case 0: return RigidbodyType::Static; + case 2: return RigidbodyType::Kinematic; + case 1: + default: return RigidbodyType::Dynamic; + } + } +} + +Rigidbody::~Rigidbody() { + if (m_World != nullptr) { + m_World->UnregisterRigidbody(*this); + } +} + +void Rigidbody::Destroy() { + if (m_World != nullptr) { + m_World->UnregisterRigidbody(*this); + } + Component::Destroy(); +} + +void Rigidbody::Start() +{ + if (GetGameObject() != nullptr && GetGameObject()->GetScene() != nullptr) { + GetGameObject()->GetScene()->GetPhysics().RegisterGameObject(*GetGameObject()); + } +} void Rigidbody::AttachPhysicsBody(PhysicsWorld* world, PhysicsBodyHandle body) { m_World = world; @@ -43,3 +96,35 @@ glm::vec3 Rigidbody::GetLinearVelocity() const { return m_World->GetLinearVelocity(m_Body); } + +nlohmann::json Rigidbody::Serialize() const { + return { + {"type", RigidbodyTypeName(m_Type)}, + {"mass", m_Mass}, + {"friction", m_Friction}, + {"restitution", m_Restitution}, + {"useGravity", m_UseGravity}, + {"allowSleep", m_AllowSleep} + }; +} + +void Rigidbody::Deserialize(const nlohmann::json& data) { + if (data.contains("type")) { + m_Type = ParseRigidbodyType(data.at("type")); + } + if (data.contains("mass")) { + SetMass(data.at("mass").get()); + } + if (data.contains("friction")) { + SetFriction(data.at("friction").get()); + } + if (data.contains("restitution")) { + SetRestitution(data.at("restitution").get()); + } + if (data.contains("useGravity")) { + m_UseGravity = data.at("useGravity").get(); + } + if (data.contains("allowSleep")) { + m_AllowSleep = data.at("allowSleep").get(); + } +} diff --git a/destrum/src/Components/Physics/SphereCollider.cpp b/destrum/src/Components/Physics/SphereCollider.cpp index c55ff27..f83a64c 100644 --- a/destrum/src/Components/Physics/SphereCollider.cpp +++ b/destrum/src/Components/Physics/SphereCollider.cpp @@ -1 +1,14 @@ -#include \ No newline at end of file +#include + +nlohmann::json SphereCollider::Serialize() const { + auto data = SerializeCollider(); + data["radius"] = m_Radius; + return data; +} + +void SphereCollider::Deserialize(const nlohmann::json& data) { + DeserializeCollider(data); + if (data.contains("radius")) { + SetRadius(data.at("radius").get()); + } +} diff --git a/destrum/src/Components/Rotator.cpp b/destrum/src/Components/Rotator.cpp index 2a6d193..d54b4d7 100644 --- a/destrum/src/Components/Rotator.cpp +++ b/destrum/src/Components/Rotator.cpp @@ -5,6 +5,7 @@ #include #include // glm::quat, glm::angleAxis #include // operator*(quat, vec3) +#include glm::vec3 Rotator::MakePerpendicularUnitVector(const glm::vec3& axis) { @@ -84,12 +85,46 @@ void Rotator::SetDistance(float distance) m_InitialOffset = MakePerpendicularUnitVector(m_Axis) * m_Distance; } -void Rotator::Update() +void Rotator::Update(float dt) { - // Replace 0.001f with your engine delta time if you have one. - m_CurrentAngle += m_Speed * 0.001f; + m_CurrentAngle += m_Speed * dt; const glm::quat q = glm::angleAxis(m_CurrentAngle, glm::normalize(m_Axis)); const glm::vec3 rotatedOffset = q * m_InitialOffset; GetTransform().SetLocalPosition(m_Pivot + rotatedOffset); } + +nlohmann::json Rotator::Serialize() const { + return { + {"distance", m_Distance}, + {"speed", m_Speed}, + {"currentAngle", m_CurrentAngle}, + {"pivot", {m_Pivot.x, m_Pivot.y, m_Pivot.z}}, + {"axis", {m_Axis.x, m_Axis.y, m_Axis.z}}, + {"initialOffset", {m_InitialOffset.x, m_InitialOffset.y, m_InitialOffset.z}} + }; +} + +void Rotator::Deserialize(const nlohmann::json& data) { + if (data.contains("distance")) { + m_Distance = data.at("distance").get(); + } + if (data.contains("speed")) { + m_Speed = data.at("speed").get(); + } + if (data.contains("currentAngle")) { + m_CurrentAngle = data.at("currentAngle").get(); + } + if (data.contains("pivot")) { + const auto& value = data.at("pivot"); + m_Pivot = {value.at(0).get(), value.at(1).get(), value.at(2).get()}; + } + if (data.contains("axis")) { + const auto& value = data.at("axis"); + SetAxis({value.at(0).get(), value.at(1).get(), value.at(2).get()}); + } + if (data.contains("initialOffset")) { + const auto& value = data.at("initialOffset"); + m_InitialOffset = {value.at(0).get(), value.at(1).get(), value.at(2).get()}; + } +} diff --git a/destrum/src/Components/Spinner.cpp b/destrum/src/Components/Spinner.cpp index c9793fb..bf9c116 100644 --- a/destrum/src/Components/Spinner.cpp +++ b/destrum/src/Components/Spinner.cpp @@ -3,24 +3,33 @@ #include #include "destrum/ObjectModel/Transform.h" +#include "destrum/Util/DeltaTime.h" -void Spinner::Update() +void Spinner::Update(float dt) { - // Replace with your engine dt if you have it available in Component. - const float dt = 1.0f / 60.0f; - - m_Angle += m_Speed * dt; - // If you already have SetLocalRotation / SetWorldRotation, use that. - // Here I'm assuming you can set rotation as a quaternion or Euler somewhere. - // If not, tell me your Transform rotation API and I’ll adjust. - const glm::quat q = glm::angleAxis(m_Angle, m_Axis); - - // Example APIs you might have: - // GetTransform().SetLocalRotation(q); - // or GetTransform().SetWorldRotation(q); - GetTransform().SetLocalRotation(q); } + +nlohmann::json Spinner::Serialize() const { + return { + {"axis", {m_Axis.x, m_Axis.y, m_Axis.z}}, + {"speed", m_Speed}, + {"angle", m_Angle} + }; +} + +void Spinner::Deserialize(const nlohmann::json& data) { + if (data.contains("axis")) { + const auto& value = data.at("axis"); + SetAxis({value.at(0).get(), value.at(1).get(), value.at(2).get()}); + } + if (data.contains("speed")) { + m_Speed = data.at("speed").get(); + } + if (data.contains("angle")) { + m_Angle = data.at("angle").get(); + } +} diff --git a/destrum/src/FS/AssetFS.cpp b/destrum/src/FS/AssetFS.cpp index 184a90e..8483567 100644 --- a/destrum/src/FS/AssetFS.cpp +++ b/destrum/src/FS/AssetFS.cpp @@ -1,95 +1,204 @@ -#include -#include #include +#include +#include +#include +#include + #include "spdlog/spdlog.h" -void AssetFS::Init(std::filesystem::path exeDir) { - Mount("engine", exeDir / "assets/engine"); - Mount("game", exeDir / "assets/game"); - initialized = true; -} +namespace { + struct ParsedVirtualPath { + std::string scheme; + std::filesystem::path relativePath; + }; -void AssetFS::Mount(std::string scheme, std::filesystem::path root) { - spdlog::debug("Mounting assetfs scheme '{}' to root '{}'", scheme, root.string()); - - FSMount m; - m.scheme = std::move(scheme); - m.root = std::move(root); - - const auto manifestPath = m.root / "manifest.json"; - if (std::filesystem::exists(manifestPath)) { - m.manifest = FS::LoadAssetManifest(manifestPath); - } - - mounts.push_back(std::move(m)); -} - -std::vector AssetFS::ReadBytes(std::string_view vpath) { - assert(initialized && "AssetFS not initialized"); - // parse "engine://path/inside" - auto pos = vpath.find("://"); - if (pos == std::string_view::npos) throw std::runtime_error("bad vpath"); - - std::string scheme(vpath.substr(0, pos)); - std::filesystem::path rel(std::string(vpath.substr(pos + 3))); - - for (auto& m : mounts) { - if (m.scheme == scheme) { - auto full = m.root / rel; - return ReadFile(full); + [[nodiscard]] std::filesystem::path ValidateRelativePath(std::string_view value) { + if (value.empty()) { + throw std::runtime_error("asset path is empty"); } - } - throw std::runtime_error("mount not found"); -} -std::filesystem::path AssetFS::GetFullPath(std::string_view vpath) const { - assert(initialized && "AssetFS not initialized"); - // parse "engine://path/inside" - auto pos = vpath.find("://"); - if (pos == std::string_view::npos) throw std::runtime_error("bad vpath"); - - std::string scheme(vpath.substr(0, pos)); - std::filesystem::path rel(std::string(vpath.substr(pos + 3))); - - for (auto& m : mounts) { - if (m.scheme == scheme) { - auto full = m.root / rel; - return full; + const std::filesystem::path path{std::string(value)}; + if (path.empty() || path.is_absolute() || path.has_root_path()) { + throw std::runtime_error("asset path must be relative"); } - } - throw std::runtime_error("mount not found"); -} -std::filesystem::path AssetFS::GetCookedPathForFile(std::string_view vpath) const { - assert(initialized && "AssetFS not initialized"); - - const auto pos = vpath.find("://"); - if (pos == std::string_view::npos) - throw std::runtime_error("bad vpath"); - - const std::string scheme(vpath.substr(0, pos)); - const std::filesystem::path rel(std::string(vpath.substr(pos + 3))); - const std::string relStr = rel.generic_string(); - - for (const auto& m : mounts) { - if (m.scheme != scheme) continue; - - // If we have a manifest, consult it - if (m.manifest) { - if (const ManifestAsset* asset = m.manifest->FindBySrc(relStr)) { - if (asset->out) { - return m.root / *asset->out; - } + for (const auto& part : path) { + if (part == "..") { + throw std::runtime_error("asset path traversal is not allowed"); } } - // Fallback to raw file - return m.root / rel; + const std::filesystem::path normalized = path.lexically_normal(); + if (normalized.empty() || normalized == ".") { + throw std::runtime_error("asset path is invalid"); + } + + return normalized; } - throw std::runtime_error("mount not found"); - return {}; + [[nodiscard]] ParsedVirtualPath ParseVirtualPath(std::string_view value) { + const std::size_t separator = value.find("://"); + if (separator == std::string_view::npos || separator == 0) { + throw std::runtime_error("asset path must use scheme://relative/path syntax"); + } + + const std::string scheme(value.substr(0, separator)); + if (scheme.find_first_of("/\\:") != std::string::npos) { + throw std::runtime_error("asset scheme is invalid"); + } + + return { + scheme, + ValidateRelativePath(value.substr(separator + 3)) + }; + } + + [[nodiscard]] std::filesystem::path ResolvePath(const FSMount& mount, + const std::filesystem::path& relativePath) { + const std::filesystem::path root = mount.root.lexically_normal(); + const std::filesystem::path full = (root / relativePath).lexically_normal(); + const std::filesystem::path relative = full.lexically_relative(root); + + if (relative.empty() || relative.is_absolute() || relative == ".." || + relative.generic_string().starts_with("../")) { + throw std::runtime_error("asset path escapes its mount"); + } + + std::error_code error; + const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(root, error); + if (error) { + throw std::runtime_error("failed to resolve asset mount: " + root.string()); + } + + error.clear(); + const std::filesystem::path canonicalFull = std::filesystem::weakly_canonical(full, error); + if (error) { + throw std::runtime_error("failed to resolve asset path: " + full.string()); + } + + const std::filesystem::path canonicalRelative = + canonicalFull.lexically_relative(canonicalRoot); + if (canonicalRelative.empty() || canonicalRelative.is_absolute() || + canonicalRelative == ".." || canonicalRelative.generic_string().starts_with("../")) { + throw std::runtime_error("asset path escapes its mount"); + } + + return full; + } + + [[nodiscard]] const FSMount* FindMount(const std::vector& mounts, + const std::string& scheme) { + for (const FSMount& mount : mounts) { + if (mount.scheme == scheme) { + return &mount; + } + } + + return nullptr; + } +} + +void AssetFS::Init(std::filesystem::path exeDir) { + if (initialized) { + return; + } + + const std::size_t originalMountCount = mounts.size(); + try { + Mount("engine", exeDir / "assets/engine"); + Mount("game", exeDir / "assets/game"); + initialized = true; + } catch (...) { + mounts.resize(originalMountCount); + throw; + } +} + +void AssetFS::Reset() { + mounts.clear(); + initialized = false; +} + +void AssetFS::Mount(std::string scheme, std::filesystem::path root) { + if (scheme.empty() || scheme.find_first_of("/\\:") != std::string::npos) { + throw std::runtime_error("asset mount scheme is invalid"); + } + if (FindMount(mounts, scheme) != nullptr) { + throw std::runtime_error("asset mount already exists: " + scheme); + } + + FSMount mount; + mount.scheme = std::move(scheme); + mount.root = std::filesystem::absolute(std::move(root)).lexically_normal(); + + const std::filesystem::path manifestPath = mount.root / "manifest.json"; + std::error_code error; + if (std::filesystem::is_regular_file(manifestPath, error)) { + mount.manifest = FS::LoadAssetManifest(manifestPath); + } + + spdlog::debug("Mounting assetfs scheme '{}' to root '{}'", mount.scheme, mount.root.string()); + mounts.push_back(std::move(mount)); +} + +std::vector AssetFS::ReadBytes(std::string_view vpath) { + return ReadFile(GetFullPath(vpath)); +} + +std::filesystem::path AssetFS::GetFullPath(std::string_view vpath) const { + if (!initialized) { + throw std::runtime_error("AssetFS is not initialized"); + } + + const ParsedVirtualPath parsed = ParseVirtualPath(vpath); + const FSMount* mount = FindMount(mounts, parsed.scheme); + if (mount == nullptr) { + throw std::runtime_error("asset mount not found: " + parsed.scheme); + } + + return ResolvePath(*mount, parsed.relativePath); +} + +std::filesystem::path AssetFS::GetCookedPathForFile(std::string_view vpath) const { + if (!initialized) { + throw std::runtime_error("AssetFS is not initialized"); + } + + const ParsedVirtualPath parsed = ParseVirtualPath(vpath); + const FSMount* mount = FindMount(mounts, parsed.scheme); + if (mount == nullptr) { + throw std::runtime_error("asset mount not found: " + parsed.scheme); + } + + const std::filesystem::path rawPath = ResolvePath(*mount, parsed.relativePath); + if (mount->manifest) { + if (const ManifestAsset* asset = mount->manifest->FindBySrc(parsed.relativePath.generic_string())) { + if (asset->out) { + const std::filesystem::path cookedRelative = ValidateRelativePath(*asset->out); + const std::filesystem::path cookedPath = ResolvePath(*mount, cookedRelative); + + std::error_code error; + if (std::filesystem::is_regular_file(cookedPath, error)) { + error.clear(); + const auto sourceTime = std::filesystem::last_write_time(rawPath, error); + error.clear(); + const auto cookedTime = std::filesystem::last_write_time(cookedPath, error); + if (!error && sourceTime > cookedTime) { + throw std::runtime_error( + "cooked asset output is stale: " + cookedPath.string()); + } + return cookedPath; + } + + throw std::runtime_error( + "cooked asset output is missing: " + cookedPath.string()); + } + } + } + + // Assets without a cooked output, such as shader includes, use the source path. + return rawPath; } std::vector AssetFS::ReadFile(const std::filesystem::path& fullPath) { @@ -99,10 +208,14 @@ std::vector AssetFS::ReadFile(const std::filesystem::path& fullPath) { } file.seekg(0, std::ios::end); - std::streamsize size = file.tellg(); + const std::streamsize size = file.tellg(); + if (size < 0) { + throw std::runtime_error("failed to determine file size: " + fullPath.string()); + } file.seekg(0, std::ios::beg); - std::vector buffer(size); - if (!file.read(reinterpret_cast(buffer.data()), size)) { + + std::vector buffer(static_cast(size)); + if (size > 0 && !file.read(reinterpret_cast(buffer.data()), size)) { throw std::runtime_error("failed to read file: " + fullPath.string()); } return buffer; diff --git a/destrum/src/FS/Manifest.cpp b/destrum/src/FS/Manifest.cpp index fb787dc..9bd29b2 100644 --- a/destrum/src/FS/Manifest.cpp +++ b/destrum/src/FS/Manifest.cpp @@ -2,8 +2,30 @@ #include #include +#include #include +namespace { + [[nodiscard]] std::string NormalizeRelativePath(const std::string& value, const char* field) { + const std::filesystem::path path(value); + if (value.empty() || path.empty() || path.is_absolute() || path.has_root_path()) { + throw std::runtime_error(std::string("Invalid manifest ") + field + " path: " + value); + } + + for (const auto& part : path) { + if (part == "..") { + throw std::runtime_error(std::string("Manifest ") + field + " path escapes its mount: " + value); + } + } + + const std::string normalized = path.lexically_normal().generic_string(); + if (normalized.empty() || normalized == ".") { + throw std::runtime_error(std::string("Invalid manifest ") + field + " path: " + value); + } + + return normalized; + } +} AssetManifest FS::LoadAssetManifest(const std::filesystem::path& manifestPath) { std::ifstream f(manifestPath); @@ -25,18 +47,25 @@ AssetManifest FS::LoadAssetManifest(const std::filesystem::path& manifestPath) { ManifestAsset asset; asset.src = a.at("src").get(); asset.type = a.at("type").get(); - asset.mtime_epoch_ns = a.value("mtime_epoch_ns", 0); - asset.size_bytes = a.value("size_bytes", 0); + if (asset.type.empty()) { + throw std::runtime_error("Invalid manifest asset type"); + } + asset.mtime_epoch_ns = a.value("mtime_epoch_ns", std::int64_t{0}); + asset.size_bytes = a.value("size_bytes", std::uint64_t{0}); if (a.contains("out") && !a["out"].is_null()) { asset.out = a["out"].get(); } - // Normalize to forward slashes for cross-platform matching - std::filesystem::path p(asset.src); - asset.src = p.generic_string(); + asset.src = NormalizeRelativePath(asset.src, "source"); + if (asset.out) { + *asset.out = NormalizeRelativePath(*asset.out, "output"); + } - manifest.assetsBySrc.emplace(asset.src, std::move(asset)); + const std::string source = asset.src; + if (!manifest.assetsBySrc.emplace(source, std::move(asset)).second) { + throw std::runtime_error("Duplicate manifest source path: " + source); + } } return manifest; diff --git a/destrum/src/Graphics/BindlessSetManager.cpp b/destrum/src/Graphics/BindlessSetManager.cpp index ff90dc7..7da332f 100644 --- a/destrum/src/Graphics/BindlessSetManager.cpp +++ b/destrum/src/Graphics/BindlessSetManager.cpp @@ -1,21 +1,64 @@ #include #include +#include +#include #include #include namespace { - constexpr std::uint32_t maxBindlessResources = 16536; + constexpr std::uint32_t requestedMaxBindlessResources = 16536; constexpr std::uint32_t maxSamplers = 32; constexpr std::uint32_t texturesBinding = 0; constexpr std::uint32_t samplersBinding = 1; } -void BindlessSetManager::init(VkDevice device, float maxAnisotropy) +void BindlessSetManager::init( + VkDevice device, + VkPhysicalDevice physicalDevice, + float maxAnisotropy) { + try { + VkPhysicalDeviceDescriptorIndexingProperties indexingProperties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES, + }; + VkPhysicalDeviceMaintenance3Properties maintenanceProperties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_3_PROPERTIES, + .pNext = &indexingProperties, + }; + VkPhysicalDeviceProperties2 properties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, + .pNext = &maintenanceProperties, + }; + vkGetPhysicalDeviceProperties2(physicalDevice, &properties); + + if (indexingProperties.maxDescriptorSetUpdateAfterBindSamplers < maxSamplers || + indexingProperties.maxPerStageDescriptorUpdateAfterBindSamplers < maxSamplers) { + throw std::runtime_error("The device exposes too few bindless samplers"); + } + + const auto descriptorCapacityAfterSamplers = [](std::uint32_t capacity) { + return capacity > maxSamplers ? capacity - maxSamplers : 0u; + }; + const auto maxPerSetImages = descriptorCapacityAfterSamplers( + maintenanceProperties.maxPerSetDescriptors); + const auto maxAllPoolsImages = descriptorCapacityAfterSamplers( + indexingProperties.maxUpdateAfterBindDescriptorsInAllPools); + maxBindlessResources = std::min( + requestedMaxBindlessResources, + std::min({ + indexingProperties.maxDescriptorSetUpdateAfterBindSampledImages, + indexingProperties.maxPerStageDescriptorUpdateAfterBindSampledImages, + maxPerSetImages, + maxAllPoolsImages, + })); + if (maxBindlessResources == 0) { + throw std::runtime_error("The device exposes no bindless sampled-image capacity"); + } + { // create pool const auto poolSizesBindless = std::array{{ {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, maxBindlessResources}, @@ -25,7 +68,7 @@ void BindlessSetManager::init(VkDevice device, float maxAnisotropy) const auto poolInfo = VkDescriptorPoolCreateInfo{ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, .flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT, - .maxSets = 10, + .maxSets = 1, .poolSizeCount = static_cast(poolSizesBindless.size()), .pPoolSizes = poolSizesBindless.data(), }; @@ -78,17 +121,14 @@ void BindlessSetManager::init(VkDevice device, float maxAnisotropy) .pSetLayouts = &descSetLayout, }; - std::uint32_t maxBinding = maxBindlessResources - 1; - const auto countInfo = VkDescriptorSetVariableDescriptorCountAllocateInfo{ - .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO, - .descriptorSetCount = 1, - .pDescriptorCounts = &maxBinding, - }; - VK_CHECK(vkAllocateDescriptorSets(device, &allocInfo, &descSet)); } - initDefaultSamplers(device, maxAnisotropy); + initDefaultSamplers(device, maxAnisotropy); + } catch (...) { + cleanup(device); + throw; + } } void BindlessSetManager::initDefaultSamplers(VkDevice device, float maxAnisotropy) @@ -96,7 +136,8 @@ void BindlessSetManager::initDefaultSamplers(VkDevice device, float maxAnisotrop // Keep in sync with bindless.glsl static const std::uint32_t nearestSamplerId = 0; static const std::uint32_t linearSamplerId = 1; - static const std::uint32_t shadowSamplerId = 2; + static const std::uint32_t anisotropicSamplerId = 2; + static const std::uint32_t shadowSamplerId = 3; { // init nearest sampler const auto samplerCreateInfo = VkSamplerCreateInfo{ @@ -115,15 +156,26 @@ void BindlessSetManager::initDefaultSamplers(VkDevice device, float maxAnisotrop .magFilter = VK_FILTER_LINEAR, .minFilter = VK_FILTER_LINEAR, .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, - // TODO: make possible to disable anisotropy or set other values? - .anisotropyEnable = VK_TRUE, - .maxAnisotropy = maxAnisotropy, }; VK_CHECK(vkCreateSampler(device, &samplerCreateInfo, nullptr, &linearSampler)); vkutil::addDebugLabel(device, linearSampler, "linear"); addSampler(device, linearSamplerId, linearSampler); } + { // init anisotropic sampler + const auto samplerCreateInfo = VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, + .anisotropyEnable = VK_TRUE, + .maxAnisotropy = maxAnisotropy, + }; + VK_CHECK(vkCreateSampler(device, &samplerCreateInfo, nullptr, &anisotropicSampler)); + vkutil::addDebugLabel(device, anisotropicSampler, "anisotropic"); + addSampler(device, anisotropicSamplerId, anisotropicSampler); + } + { // init shadow map sampler const auto samplerCreateInfo = VkSamplerCreateInfo{ .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, @@ -140,12 +192,47 @@ void BindlessSetManager::initDefaultSamplers(VkDevice device, float maxAnisotrop void BindlessSetManager::cleanup(VkDevice device) { - vkDestroySampler(device, nearestSampler, nullptr); - vkDestroySampler(device, linearSampler, nullptr); - vkDestroySampler(device, shadowMapSampler, nullptr); + if (descPool != VK_NULL_HANDLE) { + if (device != VK_NULL_HANDLE) { + vkDestroyDescriptorPool(device, descPool, nullptr); + } + descPool = VK_NULL_HANDLE; + } + descSet = VK_NULL_HANDLE; - vkDestroyDescriptorSetLayout(device, descSetLayout, nullptr); - vkDestroyDescriptorPool(device, descPool, nullptr); + if (descSetLayout != VK_NULL_HANDLE) { + if (device != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(device, descSetLayout, nullptr); + } + descSetLayout = VK_NULL_HANDLE; + } + + if (nearestSampler != VK_NULL_HANDLE) { + if (device != VK_NULL_HANDLE) { + vkDestroySampler(device, nearestSampler, nullptr); + } + nearestSampler = VK_NULL_HANDLE; + } + if (linearSampler != VK_NULL_HANDLE) { + if (device != VK_NULL_HANDLE) { + vkDestroySampler(device, linearSampler, nullptr); + } + linearSampler = VK_NULL_HANDLE; + } + if (anisotropicSampler != VK_NULL_HANDLE) { + if (device != VK_NULL_HANDLE) { + vkDestroySampler(device, anisotropicSampler, nullptr); + } + anisotropicSampler = VK_NULL_HANDLE; + } + if (shadowMapSampler != VK_NULL_HANDLE) { + if (device != VK_NULL_HANDLE) { + vkDestroySampler(device, shadowMapSampler, nullptr); + } + shadowMapSampler = VK_NULL_HANDLE; + } + + maxBindlessResources = 0; } void BindlessSetManager::addImage( @@ -153,8 +240,12 @@ void BindlessSetManager::addImage( std::uint32_t id, const VkImageView imageView) { + if (id >= maxBindlessResources) { + throw std::out_of_range("Bindless image descriptor capacity exceeded"); + } + const auto imageInfo = VkDescriptorImageInfo{ - .imageView = imageView, .imageLayout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL}; + .imageView = imageView, .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; const auto writeSet = VkWriteDescriptorSet{ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = descSet, @@ -170,7 +261,7 @@ void BindlessSetManager::addImage( void BindlessSetManager::addSampler(const VkDevice device, std::uint32_t id, VkSampler sampler) { const auto imageInfo = - VkDescriptorImageInfo{.sampler = sampler, .imageLayout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL}; + VkDescriptorImageInfo{.sampler = sampler, .imageLayout = VK_IMAGE_LAYOUT_UNDEFINED}; const auto writeSet = VkWriteDescriptorSet{ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = descSet, diff --git a/destrum/src/Graphics/Caches/ImageCache.cpp b/destrum/src/Graphics/Caches/ImageCache.cpp index 6465dc2..03793c4 100644 --- a/destrum/src/Graphics/Caches/ImageCache.cpp +++ b/destrum/src/Graphics/Caches/ImageCache.cpp @@ -3,6 +3,8 @@ #include #include "spdlog/spdlog.h" +#include +#include ImageCache::ImageCache(GfxDevice& gfxDevice) : gfxDevice(gfxDevice) { } @@ -36,44 +38,68 @@ ImageID ImageCache::loadImageFromFile( auto image = std::move(imageOpt.value()); - const auto id = getFreeImageId(); - - addImage(id, std::move(image)); - - loadedImagesInfo.emplace( - id, - LoadedImageInfo{ - .path = path, - .intent = intent, - .usage = usage, - .mipMap = mipMap, - }); - - return id; + ImageID id = NULL_IMAGE_ID; + bool infoInserted = false; + try { + id = getFreeImageId(); + loadedImagesInfo.emplace( + id, + LoadedImageInfo{ + .path = path, + .intent = intent, + .usage = usage, + .mipMap = mipMap, + }); + infoInserted = true; + addImage(id, std::move(image)); + return id; + } catch (...) { + if (infoInserted) { + loadedImagesInfo.erase(id); + } + gfxDevice.destroyImage(image); + throw; + } } ImageID ImageCache::addImage(GPUImage image) { - return addImage(getFreeImageId(), std::move(image)); + try { + return addImage(getFreeImageId(), std::move(image)); + } catch (...) { + gfxDevice.destroyImage(image); + throw; + } } ImageID ImageCache::addImage(ImageID id, GPUImage image) { - image.setBindlessId(static_cast(id)); - - if (id < images.size()) { - gfxDevice.destroyImage(images[id]); - images[id] = std::move(image); - } else { - assert(id == images.size()); - images.push_back(std::move(image)); + if (id >= getMaxImageCount()) { + throw std::out_of_range("ImageCache bindless capacity exhausted"); + } + if (id > images.size()) { + throw std::out_of_range("ImageCache image ID is not contiguous"); } - bindlessSetManager.addImage( - gfxDevice.getDevice(), - id, - images[id].imageView - ); + try { + image.setBindlessId(static_cast(id)); - return id; + if (id < images.size()) { + gfxDevice.destroyImage(images[id]); + images[id] = std::move(image); + } else { + images.push_back(std::move(image)); + } + + bindlessSetManager.addImage( + gfxDevice.getDevice(), + id, + images[id].imageView + ); + + return id; + } catch (...) { + gfxDevice.destroyImage(image); + throw; + } } const GPUImage& ImageCache::getImage(ImageID id) const { @@ -82,11 +108,17 @@ const GPUImage& ImageCache::getImage(ImageID id) const { } ImageID ImageCache::getFreeImageId() const { - return images.size(); + if (images.size() >= getMaxImageCount()) { + throw std::out_of_range("ImageCache bindless capacity exhausted"); + } + if (images.size() >= std::numeric_limits::max()) { + throw std::out_of_range("ImageCache ID range exhausted"); + } + return static_cast(images.size()); } void ImageCache::destroyImages() { - for (const auto& image: images) { + for (auto& image: images) { gfxDevice.destroyImage(image); } images.clear(); diff --git a/destrum/src/Graphics/Caches/MaterialCache.cpp b/destrum/src/Graphics/Caches/MaterialCache.cpp index 4e9485b..9ff33d4 100644 --- a/destrum/src/Graphics/Caches/MaterialCache.cpp +++ b/destrum/src/Graphics/Caches/MaterialCache.cpp @@ -4,36 +4,58 @@ #include #include "spdlog/spdlog.h" +#include +#include +#include void MaterialCache::init( GfxDevice& gfxDevice, MaterialDefaultTextures defaults) { - defaultTextures = defaults; + try { + defaultTextures = defaults; + device = &gfxDevice; - materialDataBuffer = gfxDevice.createBuffer( - MAX_MATERIALS * sizeof(MaterialData), - VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + materialDataBuffer = gfxDevice.createBuffer( + MAX_MATERIALS * sizeof(MaterialData), + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); - vkutil::addDebugLabel( - gfxDevice.getDevice(), - materialDataBuffer.buffer, - "material data"); + vkutil::addDebugLabel( + gfxDevice.getDevice(), + materialDataBuffer.buffer, + "material data"); - Material placeholderMaterial{}; - placeholderMaterial.name = "PLACEHOLDER_MATERIAL"; - placeholderMaterial.diffuseTexture = defaultTextures.white; + Material placeholderMaterial{}; + placeholderMaterial.name = "PLACEHOLDER_MATERIAL"; + placeholderMaterial.diffuseTexture = defaultTextures.white; - placeholderMaterialId = addMaterial(placeholderMaterial); + placeholderMaterialId = addMaterial(placeholderMaterial); + gfxDevice.getMemoryManager().flushAllocation(materialDataBuffer); + } catch (...) { + cleanup(gfxDevice); + throw; + } } void MaterialCache::cleanup(GfxDevice& gfxDevice) { gfxDevice.destroyBuffer(materialDataBuffer); + materialDataBuffer = {}; + materials.clear(); + materialKeys.clear(); + placeholderMaterialId = NULL_MATERIAL_ID; + device = nullptr; } MaterialID MaterialCache::addMaterial(Material material) { + if (!materials.empty() && device != nullptr) { + // The material buffer is shared by all in-flight frames. A newly + // appended entry may be written while an older frame is reading it. + device->waitIdle(); + } + const auto getTextureOrElse = [](ImageID imageId, ImageID placeholder) { return imageId != NULL_IMAGE_ID ? imageId : placeholder; @@ -42,7 +64,12 @@ MaterialID MaterialCache::addMaterial(Material material) MaterialData* data = static_cast(materialDataBuffer.info.pMappedData); const auto id = getFreeMaterialId(); - assert(id < MAX_MATERIALS); + if (id >= MAX_MATERIALS) { + throw std::runtime_error("MaterialCache capacity exhausted"); + } + if (data == nullptr) { + throw std::runtime_error("MaterialCache material buffer is not mapped"); + } data[id] = MaterialData{ .baseColor = glm::vec4(material.baseColor, 1.0f), @@ -66,10 +93,30 @@ MaterialID MaterialCache::addMaterial(Material material) .emissiveTex = defaultTextures.emissive, }; + device->getMemoryManager().flushAllocation( + materialDataBuffer, + static_cast(id) * sizeof(MaterialData), + sizeof(MaterialData)); + + // The caller may update multiple materials in one frame; the range flush is + // cheap for coherent allocations and required for non-coherent memory. + // The cache does not own a device reference, so the renderer flushes the + // exact range after batching updates. + + const auto materialId = static_cast(materials.size()); + std::string key = material.name.empty() + ? "material:" + std::to_string(materialId) + : material.name; + const std::string baseKey = key; + std::size_t suffix = 1; + while (std::find(materialKeys.begin(), materialKeys.end(), key) != materialKeys.end()) { + key = baseKey + "#" + std::to_string(suffix++); + } materials.push_back(std::move(material)); + materialKeys.push_back(std::move(key)); - return id; + return materialId; } MaterialID MaterialCache::addSimpleTextureMaterial(ImageID textureID) @@ -88,9 +135,23 @@ const Material& MaterialCache::getMaterial(MaterialID id) const return materials.at(id); } +const std::string& MaterialCache::getMaterialKey(MaterialID id) const +{ + return materialKeys.at(id); +} + +std::optional MaterialCache::findMaterialByKey(std::string_view key) const +{ + const auto it = std::find(materialKeys.begin(), materialKeys.end(), key); + if (it == materialKeys.end()) { + return std::nullopt; + } + return static_cast(std::distance(materialKeys.begin(), it)); +} + MaterialID MaterialCache::getFreeMaterialId() const { - return materials.size(); + return static_cast(materials.size()); } MaterialID MaterialCache::getPlaceholderMaterialId() const @@ -136,4 +197,8 @@ void MaterialCache::updateMaterialGPU(MaterialID id) .metallicRoughnessTex = defaultTextures.metallicRoughness, .emissiveTex = defaultTextures.emissive, }; + device->getMemoryManager().flushAllocation( + materialDataBuffer, + static_cast(id) * sizeof(MaterialData), + sizeof(MaterialData)); } diff --git a/destrum/src/Graphics/ComputePipeline.cpp b/destrum/src/Graphics/ComputePipeline.cpp index 1e6c941..7a0e4d6 100644 --- a/destrum/src/Graphics/ComputePipeline.cpp +++ b/destrum/src/Graphics/ComputePipeline.cpp @@ -5,6 +5,7 @@ #include #include +#include "volk.h" #include "destrum/Util/GameState.h" #include "spdlog/spdlog.h" @@ -12,15 +13,25 @@ ComputePipeline::ComputePipeline(GfxDevice& device, const std::string& compPath, const ComputePipelineConfigInfo& configInfo) : m_device(device) { - CreateComputePipeline(compPath, configInfo); + try { + CreateComputePipeline(compPath, configInfo); + } catch (...) { + if (m_device.getDevice() != VK_NULL_HANDLE) { + vkDestroyShaderModule(m_device.getDevice(), m_compShaderModule, nullptr); + vkDestroyPipeline(m_device.getDevice(), m_computePipeline, nullptr); + } + throw; + } } ComputePipeline::~ComputePipeline() { - if (m_compShaderModule != VK_NULL_HANDLE) { - vkDestroyShaderModule(m_device.getDevice(), m_compShaderModule, nullptr); - } - if (m_computePipeline != VK_NULL_HANDLE) { - vkDestroyPipeline(m_device.getDevice(), m_computePipeline, nullptr); + if (m_device.getDevice() != VK_NULL_HANDLE) { + if (m_compShaderModule != VK_NULL_HANDLE) { + vkDestroyShaderModule(m_device.getDevice(), m_compShaderModule, nullptr); + } + if (m_computePipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_device.getDevice(), m_computePipeline, nullptr); + } } } diff --git a/destrum/src/Graphics/GfxDevice.cpp b/destrum/src/Graphics/GfxDevice.cpp index e74b1d3..87010ea 100644 --- a/destrum/src/Graphics/GfxDevice.cpp +++ b/destrum/src/Graphics/GfxDevice.cpp @@ -1,260 +1,158 @@ #include -#include "destrum/Graphics/Util.h" - #define VOLK_IMPLEMENTATION #include -#define VMA_IMPLEMENTATION -#include -#include - #include #include +#include #include #include - #include "destrum/Graphics/imageLoader.h" #include "destrum/Util/GameState.h" #include "spdlog/spdlog.h" -#include "tracy/Tracy.hpp" -#include "tracy/Tracy.hpp" -#include "tracy/TracyVulkan.hpp" +#include +#include GfxDevice::GfxDevice() { } +GfxDevice::~GfxDevice() { + cleanup(); +} + void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync) { - VK_CHECK(volkInitialize()); - m_vSync = vSync; - instance = vkb::InstanceBuilder{} - .set_app_name(appName.c_str()) - .set_app_version(1, 0, 0) - .request_validation_layers() - .use_default_debug_messenger() - .require_api_version(1, 3, 0) - .build() - .value(); - - volkLoadInstance(instance); - - const auto res = SDL_Vulkan_CreateSurface(window, instance, &surface); - if (res != SDL_TRUE) { - spdlog::error("Failed to create Vulkan surface: {}", SDL_GetError()); - std::exit(1); + if (initialized) { + throw std::logic_error("GfxDevice::init called twice"); } + if (window == nullptr) { + throw std::invalid_argument("GfxDevice::init requires a valid window"); + } + m_vSync = vSync; - constexpr auto deviceFeatures = VkPhysicalDeviceFeatures{ - .imageCubeArray = VK_TRUE, - .geometryShader = VK_TRUE, // for im3d - .depthClamp = VK_TRUE, - .fillModeNonSolid = VK_TRUE, - .samplerAnisotropy = VK_TRUE - }; + instanceManager.init(window, appName); - constexpr auto features12 = VkPhysicalDeviceVulkan12Features{ - .descriptorIndexing = true, - .descriptorBindingSampledImageUpdateAfterBind = true, - .descriptorBindingStorageImageUpdateAfterBind = true, - .descriptorBindingPartiallyBound = true, - .descriptorBindingVariableDescriptorCount = true, - .runtimeDescriptorArray = true, - .scalarBlockLayout = true, - .bufferDeviceAddress = true, - }; - constexpr auto features13 = VkPhysicalDeviceVulkan13Features{ - .synchronization2 = true, - .dynamicRendering = true, - }; - const auto extendedDynamicState3Features = - VkPhysicalDeviceExtendedDynamicState3FeaturesEXT{ - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_3_FEATURES_EXT, - .extendedDynamicState3PolygonMode = VK_TRUE, - }; + memoryManager.init(instanceManager); - physicalDevice = vkb::PhysicalDeviceSelector{instance} - .set_minimum_version(1, 3) - .set_required_features(deviceFeatures) - .set_required_features_12(features12) - .set_required_features_13(features13) - .add_required_extension(VK_EXT_EXTENDED_DYNAMIC_STATE_3_EXTENSION_NAME) - .add_required_extension_features(extendedDynamicState3Features) - .set_surface(surface) - .prefer_gpu_device_type(vkb::PreferredDeviceType::discrete) - .select() - .value(); + executor.init(instanceManager.getDevice(), instanceManager.getGraphicsQueueFamily(), instanceManager.getGraphicsQueue()); - device = vkb::DeviceBuilder{physicalDevice}.build().value(); - volkLoadDevice(device); + frameManager.init(instanceManager.getDevice(), instanceManager.getGraphicsQueueFamily()); +#if defined(TRACY_ENABLE) + instanceManager.initTracy(executor.getCommandBuffer()); +#endif - graphicsQueueFamily = device.get_queue_index(vkb::QueueType::graphics).value(); - graphicsQueue = device.get_queue(vkb::QueueType::graphics).value(); - - //Vma - const auto vulkanFunctions = VmaVulkanFunctions{ - .vkGetInstanceProcAddr = vkGetInstanceProcAddr, - .vkGetDeviceProcAddr = vkGetDeviceProcAddr, - }; - - const auto allocatorInfo = VmaAllocatorCreateInfo{ - .flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, - .physicalDevice = physicalDevice, - .device = device, - .pVulkanFunctions = &vulkanFunctions, - .instance = instance, - }; - vmaCreateAllocator(&allocatorInfo, &allocator); - - executor.init(device, graphicsQueueFamily, graphicsQueue); - + imageManager.init( + instanceManager.getDevice(), + instanceManager.getPhysicalDevice(), + memoryManager, + executor); int w, h; SDL_GetWindowSize(window, &w, &h); swapchainFormat = VK_FORMAT_B8G8R8A8_SRGB; - swapchain.createSwapchain(this, swapchainFormat, w, h, vSync); - VkPhysicalDeviceProperties props{}; - vkGetPhysicalDeviceProperties(physicalDevice, &props); + swapchain.createSwapchain( + instanceManager.getDevice(), + instanceManager.getVkbDevice(), + instanceManager.getSurface(), + swapchainFormat, + static_cast(w), + static_cast(h), + vSync); - // imageCache.bindlessSetManager.init(device, props.limits.maxSamplerAnisotropy); + swapchainFormat = swapchain.getFormat(); - swapchain.initSync(device); - - const auto poolCreateInfo = vkinit::commandPoolCreateInfo(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, graphicsQueueFamily); - - for (std::uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { - auto& commandPool = frames[i].commandPool; - VK_CHECK(vkCreateCommandPool(device, &poolCreateInfo, nullptr, &commandPool)); - - const auto cmdAllocInfo = vkinit::commandBufferAllocateInfo(commandPool, 1); - auto& mainCommandBuffer = frames[i].commandBuffer; - VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer)); - } - -#if defined(TRACY_ENABLE) - { - VkCommandBuffer tracyInitCmd = frames[0].commandBuffer; - -#if defined(TRACY_VK_USE_SYMBOL_TABLE) - tracyVkCtx = TracyVkContext( - instance, - physicalDevice, - device, - graphicsQueue, - tracyInitCmd, - vkGetInstanceProcAddr, - vkGetDeviceProcAddr - ); -#else - tracyVkCtx = TracyVkContext( - physicalDevice, - device, - graphicsQueue, - tracyInitCmd - ); -#endif - - static constexpr char ctxName[] = "Graphics Queue"; - TracyVkContextName(tracyVkCtx, ctxName, sizeof(ctxName) - 1); - } -#endif - - // { // create white texture - // std::uint32_t pixel = 0xFFFFFFFF; - // whiteImageId = createImage( - // { - // .format = VK_FORMAT_R8G8B8A8_UNORM, - // .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, - // .extent = VkExtent3D{1, 1, 1}, - // }, - // "white texture", - // &pixel); - // } - // - // { // create error texture (black/magenta checker) - // constexpr auto black = 0xFF000000; - // constexpr auto magenta = 0xFFFF00FF; - // - // std::array pixels{black, magenta, magenta, black}; - // errorImageId = createImage( - // { - // .format = VK_FORMAT_R8G8B8A8_UNORM, - // .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | - // VK_IMAGE_USAGE_TRANSFER_SRC_BIT, - // .extent = VkExtent3D{2, 2, 1}, - // }, - // "error texture", - // pixels.data()); - // imageCache.setErrorImageId(errorImageId); - // } + swapchain.initSync(instanceManager.getDevice()); GameState::GetInstance().SetGfxDevice(this); + initialized = true; +} + +void GfxDevice::cleanup() { + if (!initialized && + instanceManager.getDevice() == VK_NULL_HANDLE && + instanceManager.getInstance() == VK_NULL_HANDLE && + memoryManager.getAllocator() == VK_NULL_HANDLE) { + return; + } + + if (instanceManager.getDevice() != VK_NULL_HANDLE) { + vkDeviceWaitIdle(instanceManager.getDevice()); + } + + swapchain.cleanup(instanceManager.getDevice()); + frameManager.cleanup(instanceManager.getDevice()); + executor.cleanup(instanceManager.getDevice()); + memoryManager.cleanup(instanceManager.getDevice()); + instanceManager.cleanup(); + + GameState::GetInstance().SetGfxDevice(nullptr); + frameActive = false; + swapchainFormat = VK_FORMAT_UNDEFINED; + initialized = false; } void GfxDevice::recreateSwapchain(int width, int height) { - assert(width != 0 && height != 0); + if (!initialized) { + throw std::logic_error("GfxDevice::recreateSwapchain called before init"); + } + if (width <= 0 || height <= 0) { + throw std::invalid_argument("GfxDevice::recreateSwapchain requires a non-zero extent"); + } waitIdle(); - swapchain.recreateSwapchain(*this, swapchainFormat, width, height, m_vSync); + + swapchain.recreateSwapchain( + instanceManager.getDevice(), + instanceManager.getVkbDevice(), + instanceManager.getSurface(), + swapchainFormat, + static_cast(width), + static_cast(height), + m_vSync); + + swapchainFormat = swapchain.getFormat(); } VkCommandBuffer GfxDevice::beginFrame() { ZoneScopedN("GfxDevice::beginFrame"); + const auto frameIndex = getCurrentFrameIndex(); + { ZoneScopedN("Swapchain BeginFrame"); - swapchain.beginFrame(getCurrentFrameIndex()); + swapchain.beginFrame(instanceManager.getDevice(), frameIndex); } - const auto& frame = getCurrentFrame(); - const auto& cmd = frame.commandBuffer; - - const auto cmdBeginInfo = VkCommandBufferBeginInfo{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - }; - - { - ZoneScopedN("vkBeginCommandBuffer"); - VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo)); + const auto acquire = swapchain.acquireNextImage(instanceManager.getDevice(), frameIndex); + if (!acquire.hasImage()) { + return VK_NULL_HANDLE; } - return cmd; + activeFrameIndex = frameIndex; + activeSwapchainImageIndex = acquire.imageIndex; + frameActive = true; + + return frameManager.beginFrame(); } -VulkanImmediateExecutor& GfxDevice::GetImmediateExecuter() { +VulkanImmediateExecutor& GfxDevice::getImmediateExecuter() { return executor; } void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const EndFrameProps& props) { ZoneScopedN("GfxDevice::endFrame"); - // get swapchain image - VkImage swapchainImage = VK_NULL_HANDLE; - std::uint32_t swapchainImageIndex = 0; - - { - ZoneScopedN("Swapchain AcquireNextImage"); - - const auto result = swapchain.acquireNextImage(getCurrentFrameIndex()); - swapchainImage = result.first; - swapchainImageIndex = result.second; + if (!frameActive || cmd == VK_NULL_HANDLE) { + throw std::logic_error("GfxDevice::endFrame called without an active frame"); } - if (swapchainImage == VK_NULL_HANDLE) { - spdlog::info("Swapchain is freaky, skipping frame..."); - return; - } - - // Fences are reset here to prevent the deadlock in case swapchain becomes dirty - { - ZoneScopedN("Swapchain ResetFences"); - swapchain.resetFences(getCurrentFrameIndex()); - } + const VkImage swapchainImage = swapchain.getImages().at(activeSwapchainImageIndex); + const auto swapchainImageIndex = activeSwapchainImageIndex; auto swapchainLayout = VK_IMAGE_LAYOUT_UNDEFINED; { ZoneScopedN("Clear Swapchain Image"); @@ -270,45 +168,26 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E if (true) { ZoneScopedN("Copy DrawImage To Swapchain"); - // copy from draw image into swapchain vkutil::transitionImage( cmd, drawImage.image, - VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + drawImage.layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + drawImage.setLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); vkutil::transitionImage( cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); swapchainLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - auto filter = false ? VK_FILTER_LINEAR : VK_FILTER_NEAREST; - filter = VK_FILTER_NEAREST; - if (false) { - vkutil::copyImageToImage( - cmd, - drawImage.image, - swapchainImage, - drawImage.getExtent2D(), - props.drawImageBlitRect.x, - props.drawImageBlitRect.y, - props.drawImageBlitRect.z, - props.drawImageBlitRect.w, - filter); - } else { - // will stretch image to swapchain - vkutil::copyImageToImage( - cmd, - drawImage.image, - swapchainImage, - drawImage.getExtent2D(), - getSwapchainExtent(), - filter); - } + auto filter = VK_FILTER_NEAREST; + vkutil::copyImageToImage( + cmd, + drawImage.image, + swapchainImage, + drawImage.getExtent2D(), + getSwapchainExtent(), + filter); } - // prepare for present - // vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); - // swapchainLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - if (props.imguiPass) { ZoneScopedN("ImGui Render"); @@ -321,7 +200,6 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E ); } - // prepare for present { ZoneScopedN("Transition Swapchain To Present"); @@ -329,369 +207,38 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E swapchainLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; } #if defined(TRACY_ENABLE) - TracyVkCollect(tracyVkCtx, cmd); + TracyVkCollect(instanceManager.getTracyVkCtx(), cmd); #endif - { - ZoneScopedN("vkEndCommandBuffer"); - VK_CHECK(vkEndCommandBuffer(cmd)); - } - // swapchain.submitAndPresent(cmd, graphicsQueue, getCurrentFrameIndex(), swapchainImageIndex); + frameManager.endFrame(cmd); + { ZoneScopedN("Swapchain SubmitAndPresent"); - swapchain.submitAndPresent(cmd, graphicsQueue, swapchainImageIndex, getCurrentFrameIndex()); + try { + swapchain.submitAndPresent( + instanceManager.getDevice(), + cmd, + instanceManager.getGraphicsQueue(), + instanceManager.getPresentQueue(), + swapchainImageIndex, + activeFrameIndex); + } catch (...) { + frameManager.nextFrame(); + frameActive = false; + throw; + } } - frameNumber++; + frameManager.nextFrame(); + frameActive = false; + FrameMark; } -void GfxDevice::cleanup() { -#if defined(TRACY_ENABLE) - if (tracyVkCtx) { - TracyVkDestroy(tracyVkCtx); - tracyVkCtx = nullptr; - } -#endif -} - void GfxDevice::waitIdle() { - VK_CHECK(vkDeviceWaitIdle(device)); + frameManager.waitIdle(); } - - void GfxDevice::immediateSubmit(ImmediateExecuteFunction&& f) const { executor.immediateSubmit(std::move(f)); } - -GPUBuffer GfxDevice::createBuffer( - std::size_t allocSize, - VkBufferUsageFlags usage, - VmaMemoryUsage memoryUsage) const { - const auto bufferInfo = VkBufferCreateInfo{ - .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, - .size = allocSize, - .usage = usage, - }; - - const auto allocInfo = VmaAllocationCreateInfo{ - .flags = VMA_ALLOCATION_CREATE_MAPPED_BIT | - // TODO: allow to set VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT when needed - VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT, - .usage = memoryUsage, - }; - - GPUBuffer buffer{}; - VK_CHECK(vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer.buffer, &buffer.allocation, &buffer.info)); - if ((usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0) { - const auto deviceAdressInfo = VkBufferDeviceAddressInfo{ - .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, - .buffer = buffer.buffer, - }; - buffer.address = vkGetBufferDeviceAddress(device, &deviceAdressInfo); - } - - return buffer; -} - -VkDeviceAddress GfxDevice::getBufferAddress(const GPUBuffer& buffer) const { - const auto deviceAdressInfo = VkBufferDeviceAddressInfo{ - .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, - .buffer = buffer.buffer, - }; - return vkGetBufferDeviceAddress(device, &deviceAdressInfo); -} - -void GfxDevice::destroyBuffer(const GPUBuffer& buffer) const { - vmaDestroyBuffer(allocator, buffer.buffer, buffer.allocation); -} - -// -// GPUImage GfxDevice::loadImageFromFileRaw( -// const std::filesystem::path& path, -// VkFormat format, -// VkImageUsageFlags usage, -// bool mipMap) const -// { -// auto data = util::loadImage(path); -// if (!data.pixels) { -// fmt::println("[error] failed to load image from '{}'", path.string()); -// return getImage(errorImageId); -// } -// -// auto image = createImageRaw({ -// .format = format, -// .usage = usage | // -// VK_IMAGE_USAGE_TRANSFER_DST_BIT | // for uploading pixel data to image -// VK_IMAGE_USAGE_TRANSFER_SRC_BIT, // for generating mips -// .extent = -// VkExtent3D{ -// .width = (std::uint32_t)data.width, -// .height = (std::uint32_t)data.height, -// .depth = 1, -// }, -// .mipMap = mipMap, -// }); -// uploadImageData(image, data.pixels); -// -// image.debugName = path.string(); -// vkutil::addDebugLabel(device, image.image, path.string().c_str()); -// -// return image; -// } - - -GPUImage GfxDevice::createImageRaw( - const vkutil::CreateImageInfo& createInfo, - std::optional customAllocationCreateInfo) const { - std::uint32_t mipLevels = 1; - if (createInfo.mipMap) { - const auto maxExtent = std::max(createInfo.extent.width, createInfo.extent.height); - mipLevels = (std::uint32_t)std::floor(std::log2(maxExtent)) + 1; - } - - if (createInfo.isCubemap) { - assert(createInfo.numLayers % 6 == 0); - // assert(!createInfo.mipMap); - assert((createInfo.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0); - } - - auto imgInfo = VkImageCreateInfo{ - .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - .flags = createInfo.flags, - .imageType = VK_IMAGE_TYPE_2D, - .format = createInfo.format, - .extent = createInfo.extent, - .mipLevels = mipLevels, - .arrayLayers = createInfo.numLayers, - .samples = createInfo.samples, - .tiling = createInfo.tiling, - .usage = createInfo.usage, - }; - - static const auto defaultAllocInfo = VmaAllocationCreateInfo{ - .usage = VMA_MEMORY_USAGE_AUTO, - .requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, - }; - const auto allocInfo = customAllocationCreateInfo.has_value() ? customAllocationCreateInfo.value() : defaultAllocInfo; - - GPUImage image{}; - image.format = createInfo.format; - image.usage = createInfo.usage; - image.extent = createInfo.extent; - image.mipLevels = mipLevels; - image.numLayers = createInfo.numLayers; - image.isCubemap = createInfo.isCubemap; - - VK_CHECK(vmaCreateImage(allocator, &imgInfo, &allocInfo, &image.image, &image.allocation, nullptr)); - - // create view only when usage flags allow it - bool shouldCreateView = ((createInfo.usage & VK_IMAGE_USAGE_SAMPLED_BIT) != 0) || - ((createInfo.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) || - ((createInfo.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0) || - ((createInfo.usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0); - - if (shouldCreateView) { - VkImageAspectFlags aspectFlag = VK_IMAGE_ASPECT_COLOR_BIT; - if (createInfo.format == VK_FORMAT_D32_SFLOAT) { - // TODO: support other depth formats - aspectFlag = VK_IMAGE_ASPECT_DEPTH_BIT; - } - - auto viewType = - createInfo.numLayers == 1 ? VK_IMAGE_VIEW_TYPE_2D : VK_IMAGE_VIEW_TYPE_2D_ARRAY; - if (createInfo.isCubemap && createInfo.numLayers == 6) { - viewType = VK_IMAGE_VIEW_TYPE_CUBE; - } - - const auto viewCreateInfo = VkImageViewCreateInfo{ - .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - .image = image.image, - .viewType = viewType, - .format = createInfo.format, - .subresourceRange = - VkImageSubresourceRange{ - .aspectMask = aspectFlag, - .baseMipLevel = 0, - .levelCount = mipLevels, - .baseArrayLayer = 0, - .layerCount = createInfo.numLayers, - }, - }; - - VK_CHECK(vkCreateImageView(device, &viewCreateInfo, nullptr, &image.imageView)); - } - - return image; -} - -std::optional GfxDevice::loadImageFromFileRaw( - const std::filesystem::path& path, - VkImageUsageFlags usage, - bool mipMap, - TextureIntent intent) const -{ - const auto data = util::loadImage(path, intent); - - if (data.vkFormat == VK_FORMAT_UNDEFINED || - data.byteSize == 0 || - (data.hdr ? data.hdrPixels == nullptr : data.pixels == nullptr)) - { - spdlog::error("Failed to load image '{}'", path.string()); - return std::nullopt; - } - - auto image = createImageRaw({ - .format = data.vkFormat, - .usage = usage | - VK_IMAGE_USAGE_TRANSFER_DST_BIT | - (mipMap ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0), - .extent = VkExtent3D{ - .width = static_cast(data.width), - .height = static_cast(data.height), - .depth = 1, - }, - .mipMap = mipMap, - }); - - const void* src = - data.hdr - ? static_cast(data.hdrPixels) - : static_cast(data.pixels); - - uploadImageDataSized(image, src, data.byteSize, 0); - - image.debugName = path.string(); - vkutil::addDebugLabel(device, image.image, path.string().c_str()); - - return image; -} - -// void GfxDevice::uploadImageData(const GPUImage& image, void* pixelData, std::uint32_t layer) const { -// VkDeviceSize dataSize = -// VkDeviceSize(image.extent.depth) * -// image.extent.width * -// image.extent.height * -// BytesPerTexel(image.format); -// -// auto uploadBuffer = createBuffer(dataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); -// memcpy(uploadBuffer.info.pMappedData, pixelData, size_t(dataSize)); -// -// executor.immediateSubmit([&] (VkCommandBuffer cmd) { -// assert( -// (image.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 && -// "Image needs to have VK_IMAGE_USAGE_TRANSFER_DST_BIT to upload data to it"); -// vkutil::transitionImage( -// cmd, image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); -// -// const auto copyRegion = VkBufferImageCopy{ -// .bufferOffset = 0, -// .bufferRowLength = 0, -// .bufferImageHeight = 0, -// .imageSubresource = -// { -// .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, -// .mipLevel = 0, -// .baseArrayLayer = layer, -// .layerCount = 1, -// }, -// .imageExtent = image.extent, -// }; -// -// vkCmdCopyBufferToImage( -// cmd, -// uploadBuffer.buffer, -// image.image, -// VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, -// 1, -// ©Region); -// -// if (image.mipLevels > 1) { -// assert( -// (image.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 && -// (image.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0 && -// "Image needs to have VK_IMAGE_USAGE_TRANSFER_{DST,SRC}_BIT to generate mip maps"); -// // graphics::generateMipmaps( -// // cmd, -// // image.image, -// // VkExtent2D{image.extent.width, image.extent.height}, -// // image.mipLevels); -// spdlog::warn("Yea dawg, i ain't written this yet :pray:"); -// } else { -// vkutil::transitionImage( -// cmd, -// image.image, -// VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, -// VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); -// } -// }); -// -// destroyBuffer(uploadBuffer); -// } - -void GfxDevice::uploadImageDataSized(const GPUImage& image, const void* pixelData, std::size_t byteSize, std::uint32_t layer) const { - auto uploadBuffer = createBuffer(byteSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); - - // safety checks - assert(uploadBuffer.info.pMappedData); - assert(pixelData); - assert(byteSize > 0); - - std::memcpy(uploadBuffer.info.pMappedData, pixelData, byteSize); - - executor.immediateSubmit([&] (VkCommandBuffer cmd) { - vkutil::transitionImage(cmd, image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - - VkBufferImageCopy copyRegion{}; - copyRegion.bufferOffset = 0; - copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - copyRegion.imageSubresource.mipLevel = 0; - copyRegion.imageSubresource.baseArrayLayer = layer; - copyRegion.imageSubresource.layerCount = 1; - copyRegion.imageExtent = image.extent; - - vkCmdCopyBufferToImage(cmd, uploadBuffer.buffer, image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Region); - - vkutil::transitionImage(cmd, image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - }); - - destroyBuffer(uploadBuffer); -} - -// GPUImage GfxDevice::loadImageFromFileRaw(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap) const { -// const auto data = util::loadImage(path); -// const bool isHdr = data.hdr && data.hdrPixels; -// const bool isLdr = !data.hdr && data.pixels; -// -// if (!isHdr && !isLdr || data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0) { -// spdlog::error("failed to load image from '{}'", path.string()); -// return getImage(errorImageId); -// } -// -// auto image = createImageRaw({ -// .format = data.vkFormat, -// .usage = usage | -// VK_IMAGE_USAGE_TRANSFER_DST_BIT | -// (mipMap ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0), -// .extent = VkExtent3D{ -// .width = (std::uint32_t)data.width, -// .height = (std::uint32_t)data.height, -// .depth = 1, -// }, -// .mipMap = mipMap, -// }); -// -// const void* src = isHdr ? (const void*)data.hdrPixels : (const void*)data.pixels; -// uploadImageDataSized(image, src, data.byteSize, 0); -// -// image.debugName = path.string(); -// vkutil::addDebugLabel(device, image.image, path.string().c_str()); -// return image; -// } - -void GfxDevice::destroyImage(const GPUImage& image) const { - vkDestroyImageView(device, image.imageView, nullptr); - vmaDestroyImage(allocator, image.image, image.allocation); - // TODO: if image has bindless id, update the set -} diff --git a/destrum/src/Graphics/ImmediateExecuter.cpp b/destrum/src/Graphics/ImmediateExecuter.cpp index 4445237..602d0a5 100644 --- a/destrum/src/Graphics/ImmediateExecuter.cpp +++ b/destrum/src/Graphics/ImmediateExecuter.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -14,36 +15,61 @@ constexpr auto NO_TIMEOUT = std::numeric_limits::max(); } void VulkanImmediateExecutor::init( - VkDevice device, + VkDevice deviceHandle, std::uint32_t graphicsQueueFamily, - VkQueue graphicsQueue) + VkQueue graphicsQueueHandle) { - assert(!initialized); + if (initialized) { + throw std::logic_error("VulkanImmediateExecutor::init called twice"); + } - this->device = device; - this->graphicsQueue = graphicsQueue; + this->device = deviceHandle; + this->graphicsQueue = graphicsQueueHandle; - const auto poolCreateInfo = vkinit:: - commandPoolCreateInfo(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, graphicsQueueFamily); - VK_CHECK(vkCreateCommandPool(device, &poolCreateInfo, nullptr, &immCommandPool)); + try { + const auto poolCreateInfo = vkinit:: + commandPoolCreateInfo(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, graphicsQueueFamily); + VK_CHECK(vkCreateCommandPool(deviceHandle, &poolCreateInfo, nullptr, &immCommandPool)); - const auto cmdAllocInfo = vkinit::commandBufferAllocateInfo(immCommandPool, 1); - VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &immCommandBuffer)); + const auto cmdAllocInfo = vkinit::commandBufferAllocateInfo(immCommandPool, 1); + VK_CHECK(vkAllocateCommandBuffers(deviceHandle, &cmdAllocInfo, &immCommandBuffer)); - constexpr auto fenceCreateInfo = VkFenceCreateInfo{ - .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, - .flags = VK_FENCE_CREATE_SIGNALED_BIT, - }; - VK_CHECK(vkCreateFence(device, &fenceCreateInfo, nullptr, &immFence)); + constexpr auto fenceCreateInfo = VkFenceCreateInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .flags = VK_FENCE_CREATE_SIGNALED_BIT, + }; + VK_CHECK(vkCreateFence(deviceHandle, &fenceCreateInfo, nullptr, &immFence)); - initialized = true; + initialized = true; + } catch (...) { + cleanup(deviceHandle); + throw; + } } -void VulkanImmediateExecutor::cleanup(VkDevice device) +void VulkanImmediateExecutor::cleanup(VkDevice deviceHandle) { - assert(initialized); - vkDestroyCommandPool(device, immCommandPool, nullptr); - vkDestroyFence(device, immFence, nullptr); + if (deviceHandle == VK_NULL_HANDLE && device != VK_NULL_HANDLE) { + deviceHandle = device; + } + + if (deviceHandle == VK_NULL_HANDLE) { + return; + } + + if (immCommandPool != VK_NULL_HANDLE) { + vkDestroyCommandPool(deviceHandle, immCommandPool, nullptr); + } + if (immFence != VK_NULL_HANDLE) { + vkDestroyFence(deviceHandle, immFence, nullptr); + } + + immCommandPool = VK_NULL_HANDLE; + immCommandBuffer = VK_NULL_HANDLE; + immFence = VK_NULL_HANDLE; + this->device = VK_NULL_HANDLE; + graphicsQueue = VK_NULL_HANDLE; + initialized = false; } void VulkanImmediateExecutor::immediateSubmit( diff --git a/destrum/src/Graphics/Managers/FrameManager.cpp b/destrum/src/Graphics/Managers/FrameManager.cpp new file mode 100644 index 0000000..6c09a92 --- /dev/null +++ b/destrum/src/Graphics/Managers/FrameManager.cpp @@ -0,0 +1,86 @@ +#include + +#include + +#include + +#include + +#include "destrum/Graphics/Util.h" + +FrameManager::~FrameManager() { +} + +void FrameManager::init(VkDevice dev, std::uint32_t queueFamily) { + device = dev; + + try { + const auto poolCreateInfo = vkinit::commandPoolCreateInfo( + VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, + queueFamily); + + for (std::uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { + auto& commandPool = frames[i].commandPool; + VK_CHECK(vkCreateCommandPool(device, &poolCreateInfo, nullptr, &commandPool)); + + const auto cmdAllocInfo = vkinit::commandBufferAllocateInfo(commandPool, 1); + auto& mainCommandBuffer = frames[i].commandBuffer; + VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer)); + } + } catch (...) { + cleanup(dev); + throw; + } +} + +void FrameManager::cleanup(VkDevice dev) { + for (std::uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { + if (frames[i].commandPool != VK_NULL_HANDLE && dev != VK_NULL_HANDLE) { + vkDestroyCommandPool(dev, frames[i].commandPool, nullptr); + } + frames[i].commandPool = VK_NULL_HANDLE; + frames[i].commandBuffer = VK_NULL_HANDLE; + } + frameNumber = 0; + device = VK_NULL_HANDLE; +} + +VkCommandBuffer FrameManager::beginFrame() { + ZoneScopedN("FrameManager::beginFrame"); + + const auto& frame = getCurrentFrame(); + const auto& cmd = frame.commandBuffer; + + const auto cmdBeginInfo = VkCommandBufferBeginInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + }; + + { + ZoneScopedN("vkBeginCommandBuffer"); + VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo)); + } + + return cmd; +} + +void FrameManager::endFrame(VkCommandBuffer cmd) { + ZoneScopedN("FrameManager::endFrame"); + + { + ZoneScopedN("vkEndCommandBuffer"); + VK_CHECK(vkEndCommandBuffer(cmd)); + } + +} + +void FrameManager::nextFrame() { + frameNumber++; + FrameMark; +} + +void FrameManager::waitIdle() const { + if (device != VK_NULL_HANDLE) { + VK_CHECK(vkDeviceWaitIdle(device)); + } +} diff --git a/destrum/src/Graphics/Managers/ImageManager.cpp b/destrum/src/Graphics/Managers/ImageManager.cpp new file mode 100644 index 0000000..deb7e08 --- /dev/null +++ b/destrum/src/Graphics/Managers/ImageManager.cpp @@ -0,0 +1,362 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +ImageManager::~ImageManager() +{ +} + +void ImageManager::init( + VkDevice dev, + VkPhysicalDevice physicalDev, + const MemoryManager& memoryManagerRef, + VulkanImmediateExecutor& exec) +{ + device = dev; + physicalDevice = physicalDev; + this->memoryManager = &memoryManagerRef; + executor = &exec; +} + +GPUImage ImageManager::createImage( + const vkutil::CreateImageInfo& createInfo, + std::optional customAllocationCreateInfo) const +{ + if (createInfo.extent.width == 0 || createInfo.extent.height == 0 || + createInfo.extent.depth == 0 || createInfo.numLayers == 0) { + throw std::invalid_argument("Cannot create an empty Vulkan image"); + } + + std::uint32_t mipLevels = 1; + if (createInfo.mipMap) + { + const auto maxExtent = std::max(createInfo.extent.width, createInfo.extent.height); + mipLevels = static_cast(std::floor(std::log2(maxExtent))) + 1; + } + + if (createInfo.isCubemap) + { + assert(createInfo.numLayers % 6 == 0); + assert((createInfo.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0); + } + + auto imgInfo = VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .flags = createInfo.flags, + .imageType = VK_IMAGE_TYPE_2D, + .format = createInfo.format, + .extent = createInfo.extent, + .mipLevels = mipLevels, + .arrayLayers = createInfo.numLayers, + .samples = createInfo.samples, + .tiling = createInfo.tiling, + .usage = createInfo.usage, + }; + + static const auto defaultAllocInfo = VmaAllocationCreateInfo{ + .usage = VMA_MEMORY_USAGE_AUTO, + .requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + }; + const auto allocInfo = customAllocationCreateInfo.has_value() + ? customAllocationCreateInfo.value() + : defaultAllocInfo; + + GPUImage image{}; + image.format = createInfo.format; + image.usage = createInfo.usage; + image.extent = createInfo.extent; + image.mipLevels = mipLevels; + image.numLayers = createInfo.numLayers; + image.isCubemap = createInfo.isCubemap; + image.layerLayouts.assign(createInfo.numLayers, VK_IMAGE_LAYOUT_UNDEFINED); + + try { + VK_CHECK( + vmaCreateImage(memoryManager->getAllocator(), &imgInfo, &allocInfo, &image.image, &image.allocation, nullptr)); + + const bool shouldCreateView = ((createInfo.usage & VK_IMAGE_USAGE_SAMPLED_BIT) != 0) || + ((createInfo.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) || + ((createInfo.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0) || + ((createInfo.usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0); + + if (shouldCreateView) { + VkImageAspectFlags aspectFlag = VK_IMAGE_ASPECT_COLOR_BIT; + if (createInfo.format == VK_FORMAT_D32_SFLOAT) { + aspectFlag = VK_IMAGE_ASPECT_DEPTH_BIT; + } + + auto viewType = createInfo.numLayers == 1 + ? VK_IMAGE_VIEW_TYPE_2D + : VK_IMAGE_VIEW_TYPE_2D_ARRAY; + if (createInfo.isCubemap && createInfo.numLayers == 6) { + viewType = VK_IMAGE_VIEW_TYPE_CUBE; + } + + const auto viewCreateInfo = VkImageViewCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = image.image, + .viewType = viewType, + .format = createInfo.format, + .subresourceRange = VkImageSubresourceRange{ + .aspectMask = aspectFlag, + .baseMipLevel = 0, + .levelCount = mipLevels, + .baseArrayLayer = 0, + .layerCount = createInfo.numLayers, + }, + }; + + VK_CHECK(vkCreateImageView(device, &viewCreateInfo, nullptr, &image.imageView)); + } + + return image; + } catch (...) { + destroyImage(image); + throw; + } +} + +std::optional ImageManager::loadImageFromFile( + const std::filesystem::path& path, + VkImageUsageFlags usage, + bool mipMap, + TextureIntent intent) const +{ + const auto data = util::loadImage(path, intent); + + if (data.vkFormat == VK_FORMAT_UNDEFINED || + data.byteSize == 0 || + (data.hdr ? data.hdrPixels == nullptr : data.pixels == nullptr)) + { + spdlog::error("Failed to load image '{}'", path.string()); + return std::nullopt; + } + + if (mipMap) { + VkFormatProperties formatProperties{}; + vkGetPhysicalDeviceFormatProperties(physicalDevice, data.vkFormat, &formatProperties); + const auto requiredFeatures = + VK_FORMAT_FEATURE_BLIT_SRC_BIT | + VK_FORMAT_FEATURE_BLIT_DST_BIT | + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + if ((formatProperties.optimalTilingFeatures & requiredFeatures) != requiredFeatures) { + spdlog::error( + "Format {} does not support linear mip generation", + static_cast(data.vkFormat)); + return std::nullopt; + } + } + + auto image = createImage({ + .format = data.vkFormat, + .usage = usage | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (mipMap ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0), + .extent = VkExtent3D{ + .width = static_cast(data.width), + .height = static_cast(data.height), + .depth = 1, + }, + .mipMap = mipMap, + }); + + const void* src = data.hdr + ? static_cast(data.hdrPixels) + : static_cast(data.pixels); + + try { + uploadImageData(image, src, data.byteSize, 0); + } catch (...) { + destroyImage(image); + throw; + } + + image.debugName = path.string(); + vkutil::addDebugLabel(device, image.image, path.string().c_str()); + + return image; +} + +void ImageManager::uploadImageData( + const GPUImage& image, + const void* pixelData, + std::size_t byteSize, + std::uint32_t layer) const +{ + if (layer >= image.numLayers) { + throw std::out_of_range("Image upload layer is out of range"); + } + if (pixelData == nullptr || byteSize == 0) { + throw std::invalid_argument("Image upload requires non-empty pixel data"); + } + + GPUBuffer uploadBuffer = memoryManager->createBuffer(byteSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + + if (uploadBuffer.info.pMappedData == nullptr) { + memoryManager->destroyBuffer(uploadBuffer); + throw std::runtime_error("Image upload staging buffer is not mapped"); + } + + std::memcpy(uploadBuffer.info.pMappedData, pixelData, byteSize); + memoryManager->flushAllocation(uploadBuffer); + + try { + executor->immediateSubmit([&](VkCommandBuffer cmd) + { + vkutil::bufferHostWriteToTransferReadBarrier( + cmd, + uploadBuffer.buffer, + 0, + VK_WHOLE_SIZE); + + const VkImageSubresourceRange uploadRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = image.mipLevels, + .baseArrayLayer = layer, + .layerCount = 1, + }; + vkutil::transitionImage( + cmd, + image.image, + image.getLayout(layer), + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + uploadRange); + + VkBufferImageCopy copyRegion{}; + copyRegion.bufferOffset = 0; + copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copyRegion.imageSubresource.mipLevel = 0; + copyRegion.imageSubresource.baseArrayLayer = layer; + copyRegion.imageSubresource.layerCount = 1; + copyRegion.imageExtent = image.extent; + + vkCmdCopyBufferToImage( + cmd, + uploadBuffer.buffer, + image.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, + ©Region); + + if (image.mipLevels == 1) { + vkutil::transitionImage( + cmd, + image.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + uploadRange); + } else { + for (std::uint32_t mip = 1; mip < image.mipLevels; ++mip) { + const VkImageSubresourceRange previousRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = mip - 1, + .levelCount = 1, + .baseArrayLayer = layer, + .layerCount = 1, + }; + vkutil::transitionImage( + cmd, + image.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + previousRange); + + const VkImageBlit2 blitRegion{ + .sType = VK_STRUCTURE_TYPE_IMAGE_BLIT_2, + .srcSubresource = VkImageSubresourceLayers{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = mip - 1, + .baseArrayLayer = layer, + .layerCount = 1, + }, + .srcOffsets = { + {0, 0, 0}, + { + static_cast(std::max(1u, image.extent.width >> (mip - 1))), + static_cast(std::max(1u, image.extent.height >> (mip - 1))), + 1, + }, + }, + .dstSubresource = VkImageSubresourceLayers{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = mip, + .baseArrayLayer = layer, + .layerCount = 1, + }, + .dstOffsets = { + {0, 0, 0}, + { + static_cast(std::max(1u, image.extent.width >> mip)), + static_cast(std::max(1u, image.extent.height >> mip)), + 1, + }, + }, + }; + const VkBlitImageInfo2 blitInfo{ + .sType = VK_STRUCTURE_TYPE_BLIT_IMAGE_INFO_2, + .srcImage = image.image, + .srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .dstImage = image.image, + .dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .regionCount = 1, + .pRegions = &blitRegion, + .filter = VK_FILTER_LINEAR, + }; + vkCmdBlitImage2(cmd, &blitInfo); + + vkutil::transitionImage( + cmd, + image.image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + previousRange); + } + + const VkImageSubresourceRange lastRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = image.mipLevels - 1, + .levelCount = 1, + .baseArrayLayer = layer, + .layerCount = 1, + }; + vkutil::transitionImage( + cmd, + image.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + lastRange); + } + }); + } catch (...) { + memoryManager->destroyBuffer(uploadBuffer); + throw; + } + + image.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layer); + + memoryManager->destroyBuffer(uploadBuffer); +} + +void ImageManager::destroyImage(GPUImage& image) const +{ + if (image.imageView != VK_NULL_HANDLE && device != VK_NULL_HANDLE) { + vkDestroyImageView(device, image.imageView, nullptr); + } + image.imageView = VK_NULL_HANDLE; + if (image.image != VK_NULL_HANDLE && memoryManager != nullptr && + memoryManager->getAllocator() != VK_NULL_HANDLE) { + vmaDestroyImage(memoryManager->getAllocator(), image.image, image.allocation); + } + if (image.image != VK_NULL_HANDLE) { + image.image = VK_NULL_HANDLE; + image.allocation = VK_NULL_HANDLE; + } +} diff --git a/destrum/src/Graphics/Managers/MemoryManager.cpp b/destrum/src/Graphics/Managers/MemoryManager.cpp new file mode 100644 index 0000000..7060664 --- /dev/null +++ b/destrum/src/Graphics/Managers/MemoryManager.cpp @@ -0,0 +1,115 @@ +#include + +#define VMA_IMPLEMENTATION +#include + +#include + +#include + +#include + +#include "destrum/Graphics/Util.h" + +MemoryManager::~MemoryManager() { +} + +void MemoryManager::init(const VulkanInstanceManager& instanceManager) { + const auto vulkanFunctions = VmaVulkanFunctions{ + .vkGetInstanceProcAddr = vkGetInstanceProcAddr, + .vkGetDeviceProcAddr = vkGetDeviceProcAddr, + }; + + const auto allocatorInfo = VmaAllocatorCreateInfo{ + .flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, + .physicalDevice = instanceManager.getPhysicalDevice(), + .device = instanceManager.getDevice(), + .pVulkanFunctions = &vulkanFunctions, + .instance = instanceManager.getInstance(), + }; + VK_CHECK(vmaCreateAllocator(&allocatorInfo, &allocator)); + device = instanceManager.getDevice(); +} + +void MemoryManager::cleanup(VkDevice) { + if (allocator != VK_NULL_HANDLE) { + vmaDestroyAllocator(allocator); + allocator = VK_NULL_HANDLE; + } + device = VK_NULL_HANDLE; +} + +GPUBuffer MemoryManager::createBuffer( + std::size_t allocSize, + VkBufferUsageFlags usage, + VmaMemoryUsage memoryUsage) const +{ + if (allocSize == 0) { + throw std::invalid_argument("Cannot create a zero-sized Vulkan buffer"); + } + const auto bufferInfo = VkBufferCreateInfo{ + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .size = allocSize, + .usage = usage, + }; + + const bool hostVisible = + memoryUsage == VMA_MEMORY_USAGE_CPU_ONLY || + memoryUsage == VMA_MEMORY_USAGE_CPU_TO_GPU || + memoryUsage == VMA_MEMORY_USAGE_GPU_TO_CPU || + memoryUsage == VMA_MEMORY_USAGE_CPU_COPY || + memoryUsage == VMA_MEMORY_USAGE_AUTO_PREFER_HOST; + + const auto hostAccessFlag = + memoryUsage == VMA_MEMORY_USAGE_GPU_TO_CPU || memoryUsage == VMA_MEMORY_USAGE_CPU_COPY + ? VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT + : VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + const auto allocInfo = VmaAllocationCreateInfo{ + .flags = hostVisible + ? static_cast( + VMA_ALLOCATION_CREATE_MAPPED_BIT | hostAccessFlag) + : VmaAllocationCreateFlags{}, + .usage = memoryUsage, + }; + + GPUBuffer buffer{}; + VK_CHECK(vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer.buffer, &buffer.allocation, &buffer.info)); + if ((usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0) { + const auto deviceAdressInfo = VkBufferDeviceAddressInfo{ + .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, + .buffer = buffer.buffer, + }; + buffer.address = vkGetBufferDeviceAddress(device, &deviceAdressInfo); + } + + buffer.hostVisible = hostVisible; + + return buffer; +} + +VkDeviceAddress MemoryManager::getBufferAddress(const GPUBuffer& buffer) const { + const auto deviceAdressInfo = VkBufferDeviceAddressInfo{ + .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, + .buffer = buffer.buffer, + }; + return vkGetBufferDeviceAddress(device, &deviceAdressInfo); +} + +void MemoryManager::destroyBuffer(GPUBuffer& buffer) const { + if (allocator != VK_NULL_HANDLE && buffer.buffer != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator, buffer.buffer, buffer.allocation); + } + buffer = {}; +} + +void MemoryManager::flushAllocation( + const GPUBuffer& buffer, + VkDeviceSize offset, + VkDeviceSize size) const +{ + if (!buffer.hostVisible || buffer.allocation == VK_NULL_HANDLE) { + return; + } + + VK_CHECK(vmaFlushAllocation(allocator, buffer.allocation, offset, size)); +} diff --git a/destrum/src/Graphics/Managers/VulkanInstanceManager.cpp b/destrum/src/Graphics/Managers/VulkanInstanceManager.cpp new file mode 100644 index 0000000..49d8d4d --- /dev/null +++ b/destrum/src/Graphics/Managers/VulkanInstanceManager.cpp @@ -0,0 +1,182 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +VulkanInstanceManager::~VulkanInstanceManager() +{ + cleanup(); +} + +void VulkanInstanceManager::init( + SDL_Window* window, + const std::string& appName, + const DeviceFeatures& features) +{ + if (initialized) { + throw std::logic_error("VulkanInstanceManager::init called twice"); + } + + VK_CHECK(volkInitialize()); + + const auto instanceResult = vkb::InstanceBuilder{} + .set_app_name(appName.c_str()) + .set_app_version(1, 0, 0) + .request_validation_layers() + .use_default_debug_messenger() + .require_api_version(1, 3, 0) + .build(); + if (!instanceResult.has_value()) { + throw std::runtime_error( + "Failed to create Vulkan instance: " + instanceResult.error().message()); + } + vkbInstance = instanceResult.value(); + + instance = vkbInstance; + + volkLoadInstance(instance); + + const auto res = SDL_Vulkan_CreateSurface(window, instance, &surface); + if (res != SDL_TRUE) + { + throw std::runtime_error( + "Failed to create Vulkan surface: " + std::string{SDL_GetError()}); + } + + const auto physicalDeviceResult = + vkb::PhysicalDeviceSelector{vkbInstance} + .set_minimum_version(1, 3) + .set_required_features(features.device) + .set_required_features_12(features.features12) + .set_required_features_13(features.features13) + .add_required_extension( + VK_EXT_EXTENDED_DYNAMIC_STATE_3_EXTENSION_NAME) + .add_required_extension_features( + features.extendedDynamicState3) + .set_surface(surface) + .prefer_gpu_device_type(vkb::PreferredDeviceType::discrete) + .select(); + if (!physicalDeviceResult.has_value()) { + throw std::runtime_error( + "Failed to select Vulkan physical device: " + + physicalDeviceResult.error().message()); + } + vkbPhysicalDevice = physicalDeviceResult.value(); + + physicalDevice = vkbPhysicalDevice; + + const auto deviceResult = vkb::DeviceBuilder{vkbPhysicalDevice}.build(); + if (!deviceResult.has_value()) { + throw std::runtime_error( + "Failed to create Vulkan device: " + deviceResult.error().message()); + } + vkbDevice = deviceResult.value(); + + device = vkbDevice; + + volkLoadDevice(vkbDevice); + + const auto graphicsQueueFamilyResult = + vkbDevice.get_queue_index(vkb::QueueType::graphics); + const auto graphicsQueueResult = vkbDevice.get_queue(vkb::QueueType::graphics); + const auto presentQueueFamilyResult = + vkbDevice.get_queue_index(vkb::QueueType::present); + const auto presentQueueResult = vkbDevice.get_queue(vkb::QueueType::present); + if (!graphicsQueueFamilyResult.has_value() || + !graphicsQueueResult.has_value() || + !presentQueueFamilyResult.has_value() || + !presentQueueResult.has_value()) { + throw std::runtime_error("Failed to retrieve graphics/present queues"); + } + + graphicsQueueFamily = graphicsQueueFamilyResult.value(); + graphicsQueue = graphicsQueueResult.value(); + presentQueueFamily = presentQueueFamilyResult.value(); + presentQueue = presentQueueResult.value(); + + initialized = true; +} + +void VulkanInstanceManager::initTracy(VkCommandBuffer tracyInitCmd) +{ +#if defined(TRACY_ENABLE) + +#if defined(TRACY_VK_USE_SYMBOL_TABLE) + tracyVkCtx = TracyVkContext( + instance, + physicalDevice, + device, + graphicsQueue, + tracyInitCmd, + vkGetInstanceProcAddr, + vkGetDeviceProcAddr); +#else + tracyVkCtx = TracyVkContext( + physicalDevice, + device, + graphicsQueue, + tracyInitCmd); +#endif + + static constexpr char ctxName[] = "Graphics Queue"; + + TracyVkContextName( + tracyVkCtx, + ctxName, + sizeof(ctxName) - 1); + +#endif +} + +void VulkanInstanceManager::cleanup() +{ +#if defined(TRACY_ENABLE) + if (tracyVkCtx) + { + TracyVkDestroy(tracyVkCtx); + tracyVkCtx = nullptr; + } +#endif + + if (device != VK_NULL_HANDLE) { + vkDestroyDevice(device, nullptr); + device = VK_NULL_HANDLE; + } + + if (surface != VK_NULL_HANDLE && instance != VK_NULL_HANDLE) { + vkDestroySurfaceKHR(instance, surface, nullptr); + surface = VK_NULL_HANDLE; + } + + if (instance != VK_NULL_HANDLE) { + vkb::destroy_instance(vkbInstance); + instance = VK_NULL_HANDLE; + } + + physicalDevice = VK_NULL_HANDLE; + graphicsQueueFamily = VK_QUEUE_FAMILY_IGNORED; + graphicsQueue = VK_NULL_HANDLE; + presentQueueFamily = VK_QUEUE_FAMILY_IGNORED; + presentQueue = VK_NULL_HANDLE; + vkbInstance = {}; + vkbPhysicalDevice = {}; + vkbDevice = {}; + initialized = false; +} + +void VulkanInstanceManager::waitIdle() const +{ + if (device != VK_NULL_HANDLE) + { + VK_CHECK(vkDeviceWaitIdle(device)); + } +} diff --git a/destrum/src/Graphics/MeshCache.cpp b/destrum/src/Graphics/MeshCache.cpp index e51c8f7..401650b 100644 --- a/destrum/src/Graphics/MeshCache.cpp +++ b/destrum/src/Graphics/MeshCache.cpp @@ -5,6 +5,11 @@ #include #include + +#include +#include +#include +#include "volk.h" // #include MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh) @@ -27,49 +32,110 @@ MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh) const auto id = meshes.size(); meshes.push_back(std::move(gpuMesh)); cpuMeshes.push_back(cpuMesh); // store a copy of the CPU mesh + std::string key = cpuMesh.name.empty() + ? "mesh:" + std::to_string(id) + : cpuMesh.name; + const std::string baseKey = key; + std::size_t suffix = 1; + while (std::find(meshKeys.begin(), meshKeys.end(), key) != meshKeys.end()) { + key = baseKey + "#" + std::to_string(suffix++); + } + meshKeys.push_back(std::move(key)); return id; } void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const { + try { // create index buffer const auto indexBufferSize = cpuMesh.indices.size() * sizeof(std::uint32_t); gpuMesh.indexBuffer = gfxDevice.createBuffer( - indexBufferSize, VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + indexBufferSize, + VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE); // create vertex buffer const auto vertexBufferSize = cpuMesh.vertices.size() * sizeof(CPUMesh::Vertex); gpuMesh.vertexBuffer = gfxDevice.createBuffer( vertexBufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | - VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE); - const auto staging = + auto vertexIndexStaging = gfxDevice - .createBuffer(vertexBufferSize + indexBufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + .createBuffer( + vertexBufferSize + indexBufferSize, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VMA_MEMORY_USAGE_CPU_ONLY); - // copy data - void* data = staging.info.pMappedData; - memcpy(data, cpuMesh.vertices.data(), vertexBufferSize); - memcpy((char*)data + vertexBufferSize, cpuMesh.indices.data(), indexBufferSize); + try { + // copy data + void* vertexIndexData = vertexIndexStaging.info.pMappedData; + memcpy(vertexIndexData, cpuMesh.vertices.data(), vertexBufferSize); + memcpy(static_cast(vertexIndexData) + vertexBufferSize, cpuMesh.indices.data(), indexBufferSize); + gfxDevice.getMemoryManager().flushAllocation(vertexIndexStaging); + + gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) { + vkutil::bufferHostWriteToTransferReadBarrier( + cmd, + vertexIndexStaging.buffer, + 0, + VK_WHOLE_SIZE); - gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) { const auto vertexCopy = VkBufferCopy{ .srcOffset = 0, .dstOffset = 0, .size = vertexBufferSize, }; - vkCmdCopyBuffer(cmd, staging.buffer, gpuMesh.vertexBuffer.buffer, 1, &vertexCopy); + vkCmdCopyBuffer(cmd, vertexIndexStaging.buffer, gpuMesh.vertexBuffer.buffer, 1, &vertexCopy); const auto indexCopy = VkBufferCopy{ .srcOffset = vertexBufferSize, .dstOffset = 0, .size = indexBufferSize, }; - vkCmdCopyBuffer(cmd, staging.buffer, gpuMesh.indexBuffer.buffer, 1, &indexCopy); - }); + vkCmdCopyBuffer(cmd, vertexIndexStaging.buffer, gpuMesh.indexBuffer.buffer, 1, &indexCopy); - gfxDevice.destroyBuffer(staging); + const std::array destinationBarriers{{ + { + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT, + .dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = gpuMesh.vertexBuffer.buffer, + .offset = 0, + .size = VK_WHOLE_SIZE, + }, + { + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT, + .dstAccessMask = VK_ACCESS_2_INDEX_READ_BIT, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = gpuMesh.indexBuffer.buffer, + .offset = 0, + .size = VK_WHOLE_SIZE, + } + }}; + const VkDependencyInfo destinationDependency{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .bufferMemoryBarrierCount = static_cast(destinationBarriers.size()), + .pBufferMemoryBarriers = destinationBarriers.data(), + }; + vkCmdPipelineBarrier2(cmd, &destinationDependency); + }); + } catch (...) { + gfxDevice.destroyBuffer(vertexIndexStaging); + throw; + } + + gfxDevice.destroyBuffer(vertexIndexStaging); const auto vtxBufferName = cpuMesh.name + " (vtx)"; const auto idxBufferName = cpuMesh.name + " (idx)"; @@ -82,25 +148,67 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh gpuMesh.skinningDataBuffer = gfxDevice.createBuffer( skinningDataSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | - VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE); - const auto staging = - gfxDevice.createBuffer(skinningDataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + auto skinningStaging = + gfxDevice.createBuffer( + skinningDataSize, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VMA_MEMORY_USAGE_CPU_ONLY); - // copy data - void* data = staging.info.pMappedData; - memcpy(data, cpuMesh.skinningData.data(), skinningDataSize); + try { + // copy data + void* skinningData = skinningStaging.info.pMappedData; + memcpy(skinningData, cpuMesh.skinningData.data(), skinningDataSize); + gfxDevice.getMemoryManager().flushAllocation(skinningStaging); + + gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) { + vkutil::bufferHostWriteToTransferReadBarrier( + cmd, + skinningStaging.buffer, + 0, + VK_WHOLE_SIZE); - gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) { const auto vertexCopy = VkBufferCopy{ .srcOffset = 0, .dstOffset = 0, .size = skinningDataSize, }; - vkCmdCopyBuffer(cmd, staging.buffer, gpuMesh.skinningDataBuffer.buffer, 1, &vertexCopy); - }); + vkCmdCopyBuffer(cmd, skinningStaging.buffer, gpuMesh.skinningDataBuffer.buffer, 1, &vertexCopy); - gfxDevice.destroyBuffer(staging); + const VkBufferMemoryBarrier2 destinationBarrier{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = gpuMesh.skinningDataBuffer.buffer, + .offset = 0, + .size = VK_WHOLE_SIZE, + }; + const VkDependencyInfo destinationDependency{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &destinationBarrier, + }; + vkCmdPipelineBarrier2(cmd, &destinationDependency); + }); + } catch (...) { + gfxDevice.destroyBuffer(skinningStaging); + throw; + } + + gfxDevice.destroyBuffer(skinningStaging); + } + } catch (...) { + gfxDevice.destroyBuffer(gpuMesh.skinningDataBuffer); + gfxDevice.destroyBuffer(gpuMesh.vertexBuffer); + gfxDevice.destroyBuffer(gpuMesh.indexBuffer); + gpuMesh = {}; + throw; } } @@ -114,10 +222,28 @@ const CPUMesh& MeshCache::getCPUMesh(MeshID id) const return cpuMeshes.at(id); } +const std::string& MeshCache::getMeshKey(MeshID id) const +{ + return meshKeys.at(id); +} + +std::optional MeshCache::findMeshByKey(std::string_view key) const +{ + const auto it = std::find(meshKeys.begin(), meshKeys.end(), key); + if (it == meshKeys.end()) { + return std::nullopt; + } + return static_cast(std::distance(meshKeys.begin(), it)); +} + void MeshCache::cleanup(GfxDevice& gfxDevice) { - for (const auto& mesh : meshes) { + for (auto& mesh : meshes) { gfxDevice.destroyBuffer(mesh.indexBuffer); gfxDevice.destroyBuffer(mesh.vertexBuffer); + gfxDevice.destroyBuffer(mesh.skinningDataBuffer); } + meshes.clear(); + cpuMeshes.clear(); + meshKeys.clear(); } diff --git a/destrum/src/Graphics/Pipeline.cpp b/destrum/src/Graphics/Pipeline.cpp index 356ce90..cb3a940 100644 --- a/destrum/src/Graphics/Pipeline.cpp +++ b/destrum/src/Graphics/Pipeline.cpp @@ -5,19 +5,31 @@ #include #include +#include "volk.h" #include "destrum/Graphics/Util.h" #include "spdlog/spdlog.h" Pipeline::Pipeline(GfxDevice& device, const std::string& vertPath, const std::string& fragPath, const PipelineConfigInfo& configInfo): m_device(device) { - CreateGraphicsPipeline(vertPath, fragPath, configInfo); + try { + CreateGraphicsPipeline(vertPath, fragPath, configInfo); + } catch (...) { + if (m_device.getDevice() != VK_NULL_HANDLE) { + vkDestroyShaderModule(m_device.getDevice(), m_vertShaderModule, nullptr); + vkDestroyShaderModule(m_device.getDevice(), m_fragShaderModule, nullptr); + vkDestroyPipeline(m_device.getDevice(), m_graphicsPipeline, nullptr); + } + throw; + } } Pipeline::~Pipeline() { - vkDestroyShaderModule(m_device.getDevice(), m_vertShaderModule, nullptr); - vkDestroyShaderModule(m_device.getDevice(), m_fragShaderModule, nullptr); - vkDestroyPipeline(m_device.getDevice(), m_graphicsPipeline, nullptr); + if (m_device.getDevice() != VK_NULL_HANDLE) { + vkDestroyShaderModule(m_device.getDevice(), m_vertShaderModule, nullptr); + vkDestroyShaderModule(m_device.getDevice(), m_fragShaderModule, nullptr); + vkDestroyPipeline(m_device.getDevice(), m_graphicsPipeline, nullptr); + } } void Pipeline::bind(VkCommandBuffer buffer) const { diff --git a/destrum/src/Graphics/Pipelines/ImguiPass.cpp b/destrum/src/Graphics/Pipelines/ImguiPass.cpp index 946b80e..5a54a80 100644 --- a/destrum/src/Graphics/Pipelines/ImguiPass.cpp +++ b/destrum/src/Graphics/Pipelines/ImguiPass.cpp @@ -16,12 +16,14 @@ void ImguiPass::init(SDL_Window* window, GfxDevice& gfxDevice) { return; } + try { gfx = &gfxDevice; sdlWindow = window; colorFormat = gfx->getSwapchainFormat(); IMGUI_CHECKVERSION(); ImGui::CreateContext(); + contextCreated = true; ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; @@ -32,6 +34,7 @@ void ImguiPass::init(SDL_Window* window, GfxDevice& gfxDevice) { if (!ImGui_ImplSDL2_InitForVulkan(sdlWindow)) { throw std::runtime_error("ImGui_ImplSDL2_InitForVulkan failed"); } + sdlInitialized = true; const std::array poolSizes{{ { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 }, @@ -56,6 +59,21 @@ void ImguiPass::init(SDL_Window* window, GfxDevice& gfxDevice) { VK_CHECK(vkCreateDescriptorPool(gfx->getVkDevice(), &poolInfo, nullptr, &descriptorPool)); + initVulkanBackend(); + + initialized = true; + } catch (...) { + cleanup(); + throw; + } +} + +void ImguiPass::initVulkanBackend() +{ + if (gfx == nullptr) { + throw std::runtime_error("Cannot initialize ImGui Vulkan backend without a device"); + } + VkPipelineRenderingCreateInfoKHR pipelineRenderingInfo{}; pipelineRenderingInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR; pipelineRenderingInfo.colorAttachmentCount = 1; @@ -69,21 +87,26 @@ void ImguiPass::init(SDL_Window* window, GfxDevice& gfxDevice) { ImGui_ImplVulkan_InitInfo initInfo{}; initInfo.ApiVersion = VK_API_VERSION_1_3; - initInfo.Instance = gfxDevice.getVkInstance(); - initInfo.PhysicalDevice = gfxDevice.getVkPhysicalDevice(); - initInfo.Device = gfxDevice.getDevice(); - initInfo.Queue = gfxDevice.getGraphicsQueue(); + initInfo.Instance = gfx->getVkInstance(); + initInfo.PhysicalDevice = gfx->getVkPhysicalDevice(); + initInfo.Device = gfx->getDevice(); + initInfo.Queue = gfx->getGraphicsQueue(); + initInfo.QueueFamily = gfx->getGraphicsQueueFamily(); initInfo.DescriptorPool = descriptorPool; - initInfo.MinImageCount = 3; - initInfo.ImageCount = 3; + const auto imageCount = gfx->getSwapchainImageCount(); + if (imageCount < 2) { + throw std::runtime_error("ImGui Vulkan backend requires at least two swapchain images"); + } + initInfo.MinImageCount = imageCount; + initInfo.ImageCount = imageCount; initInfo.UseDynamicRendering = true; initInfo.PipelineInfoMain = pipelineInfo; + vulkanInitialized = true; if (!ImGui_ImplVulkan_Init(&initInfo)) { + vulkanInitialized = false; throw std::runtime_error("ImGui_ImplVulkan_Init failed"); } - - initialized = true; } void ImguiPass::handleEvent(const SDL_Event& event) { @@ -168,25 +191,52 @@ void ImguiPass::onSwapchainRecreated() { return; } - ImGui_ImplVulkan_SetMinImageCount(2); + const auto imageCount = gfx->getSwapchainImageCount(); + const auto newFormat = gfx->getSwapchainFormat(); + if (newFormat != colorFormat) { + if (vulkanInitialized) { + ImGui_ImplVulkan_Shutdown(); + vulkanInitialized = false; + } + colorFormat = newFormat; + initVulkanBackend(); + } else { + if (imageCount < 2) { + throw std::runtime_error("ImGui Vulkan backend requires at least two swapchain images"); + } + ImGui_ImplVulkan_SetMinImageCount(std::max(2u, imageCount)); + } } void ImguiPass::cleanup() { - if (!initialized || gfx == nullptr) { + if (gfx == nullptr && !contextCreated && !sdlInitialized && + !vulkanInitialized && descriptorPool == VK_NULL_HANDLE) { return; } - vkDeviceWaitIdle(gfx->getVkDevice()); - - ImGui_ImplVulkan_Shutdown(); - ImGui_ImplSDL2_Shutdown(); - ImGui::DestroyContext(); - - if (descriptorPool != VK_NULL_HANDLE) { - vkDestroyDescriptorPool(gfx->getVkDevice(), descriptorPool, nullptr); - descriptorPool = VK_NULL_HANDLE; + if (gfx != nullptr && gfx->getVkDevice() != VK_NULL_HANDLE) { + vkDeviceWaitIdle(gfx->getVkDevice()); } + if (vulkanInitialized) { + ImGui_ImplVulkan_Shutdown(); + vulkanInitialized = false; + } + if (sdlInitialized) { + ImGui_ImplSDL2_Shutdown(); + sdlInitialized = false; + } + if (contextCreated) { + ImGui::DestroyContext(); + contextCreated = false; + } + + if (descriptorPool != VK_NULL_HANDLE && gfx != nullptr && + gfx->getVkDevice() != VK_NULL_HANDLE) { + vkDestroyDescriptorPool(gfx->getVkDevice(), descriptorPool, nullptr); + } + descriptorPool = VK_NULL_HANDLE; + gfx = nullptr; sdlWindow = nullptr; colorFormat = VK_FORMAT_UNDEFINED; diff --git a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp index 0bb5851..aff552b 100644 --- a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp @@ -10,6 +10,7 @@ #include #include +#include "volk.h" #include "spdlog/spdlog.h" MeshPipeline::MeshPipeline() = default; @@ -22,6 +23,7 @@ void MeshPipeline::init( VkFormat drawImageFormat, VkFormat depthImageFormat) { + try { const auto vertexShader = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/mesh.vert"); @@ -49,7 +51,7 @@ void MeshPipeline::init( pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data(); if (vkCreatePipelineLayout( - gfxDevice.getDevice().device, + gfxDevice.getDevice(), &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS) @@ -69,12 +71,16 @@ void MeshPipeline::init( pipelineConfig.colorAttachments = {drawImageFormat}; pipelineConfig.depthAttachment = depthImageFormat; - m_pipeline = std::make_unique( + m_pipeline = std::make_unique( gfxDevice, vertexShader.string(), fragShader.string(), pipelineConfig - ); + ); + } catch (...) { + cleanup(gfxDevice.getDevice()); + throw; + } } void MeshPipeline::draw( @@ -188,8 +194,8 @@ void MeshPipeline::cleanup(VkDevice device) { m_pipeline.reset(); - if (m_pipelineLayout != VK_NULL_HANDLE) { + if (m_pipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) { vkDestroyPipelineLayout(device, m_pipelineLayout, nullptr); - m_pipelineLayout = VK_NULL_HANDLE; } -} \ No newline at end of file + m_pipelineLayout = VK_NULL_HANDLE; +} diff --git a/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp b/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp index 557c05e..69c1df1 100644 --- a/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/SkinningPipeline.cpp @@ -3,6 +3,7 @@ #include "destrum/FS/AssetFS.h" #include "../../../include/destrum/Graphics/Caches/MeshCache.h" #include "destrum/Graphics/MeshDrawCommand.h" +#include "destrum/Graphics/Util.h" #include #include @@ -10,7 +11,10 @@ #include #include +#include "volk.h" + void SkinningPipeline::init(GfxDevice& gfxDevice) { + try { const auto& device = gfxDevice.getDevice(); const auto pushConstant = VkPushConstantRange{ @@ -26,7 +30,7 @@ void SkinningPipeline::init(GfxDevice& gfxDevice) { pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = pushConstants.data(); - if (vkCreatePipelineLayout(device.device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS) { + if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS) { throw std::runtime_error("Could not make pipeline layout"); } @@ -42,21 +46,31 @@ void SkinningPipeline::init(GfxDevice& gfxDevice) { pipelineConfig ); - for (std::size_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { + for (std::size_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { auto& jointMatricesBuffer = framesData[i].jointMatricesBuffer; jointMatricesBuffer.capacity = MAX_JOINT_MATRICES; jointMatricesBuffer.buffer = gfxDevice.createBuffer( MAX_JOINT_MATRICES * sizeof(glm::mat4), - VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + } + } catch (...) { + cleanup(gfxDevice); + throw; } } void SkinningPipeline::cleanup(GfxDevice& gfxDevice) { for (auto& frame : framesData) { gfxDevice.destroyBuffer(frame.jointMatricesBuffer.buffer); + frame.jointMatricesBuffer.buffer = {}; + frame.jointMatricesBuffer.size = 0; } - vkDestroyPipelineLayout(gfxDevice.getDevice().device, m_pipelineLayout, nullptr); + if (m_pipelineLayout != VK_NULL_HANDLE && gfxDevice.getDevice() != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(gfxDevice.getDevice(), m_pipelineLayout, nullptr); + } + m_pipelineLayout = VK_NULL_HANDLE; skinningPipeline.reset(); } @@ -94,31 +108,45 @@ void SkinningPipeline::doSkinning(VkCommandBuffer cmd, vkCmdDispatch(cmd, groupSizeX, 1, 1); - // Required before the graphics pass reads skinnedVertexBuffer as a vertex buffer. - // Without this, the draw can see stale/partial data from before the compute dispatch. - VkBufferMemoryBarrier skinnedVertexBarrier{}; - skinnedVertexBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; - skinnedVertexBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; - skinnedVertexBarrier.dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT; - skinnedVertexBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - skinnedVertexBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - skinnedVertexBarrier.buffer = dc.skinnedMesh->skinnedVertexBuffer.buffer; - skinnedVertexBarrier.offset = 0; - skinnedVertexBarrier.size = VK_WHOLE_SIZE; + const VkBufferMemoryBarrier2 skinnedVertexBarrier{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT, + .dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = dc.skinnedMesh->skinnedVertexBuffer.buffer, + .offset = 0, + .size = VK_WHOLE_SIZE, + }; - vkCmdPipelineBarrier(cmd, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, - 0, - 0, nullptr, - 1, &skinnedVertexBarrier, - 0, nullptr); + const VkDependencyInfo dependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &skinnedVertexBarrier, + }; + vkCmdPipelineBarrier2(cmd, &dependencyInfo); } void SkinningPipeline::beginDrawing(std::size_t frameIndex) { getCurrentFrameData(frameIndex).jointMatricesBuffer.clear(); } +void SkinningPipeline::flushCurrentFrame( + VkCommandBuffer cmd, + GfxDevice& gfxDevice, + std::size_t frameIndex) +{ + auto& buffer = getCurrentFrameData(frameIndex).jointMatricesBuffer.buffer; + gfxDevice.getMemoryManager().flushAllocation(buffer, 0, VK_WHOLE_SIZE); + vkutil::bufferHostWriteToShaderReadBarrier( + cmd, + buffer.buffer, + 0, + static_cast(MAX_JOINT_MATRICES * sizeof(glm::mat4))); +} + std::size_t SkinningPipeline::appendJointMatrices(std::span jointMatrices, std::size_t frameIndex) { auto& jointMatricesBuffer = getCurrentFrameData(frameIndex).jointMatricesBuffer; diff --git a/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp b/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp index 1f9e331..7b53623 100644 --- a/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp @@ -4,6 +4,7 @@ #include #include +#include "volk.h" #include "glm/ext/matrix_transform.hpp" #include "spdlog/spdlog.h" @@ -19,6 +20,7 @@ void SkyboxPipeline::init( VkFormat drawImageFormat, VkFormat depthImageFormat) { + try { const auto vertexShader = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/fullscreen_triangle.vert"); @@ -45,7 +47,7 @@ void SkyboxPipeline::init( pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data(); if (vkCreatePipelineLayout( - gfxDevice.getDevice().device, + gfxDevice.getDevice(), &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) @@ -71,22 +73,26 @@ void SkyboxPipeline::init( pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE; pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; - pipeline = std::make_unique( + pipeline = std::make_unique( gfxDevice, vertexShader.string(), fragShader.string(), pipelineConfig - ); + ); + } catch (...) { + cleanup(gfxDevice.getDevice()); + throw; + } } void SkyboxPipeline::cleanup(VkDevice device) { pipeline.reset(); - if (pipelineLayout != VK_NULL_HANDLE) { + if (pipelineLayout != VK_NULL_HANDLE && device != VK_NULL_HANDLE) { vkDestroyPipelineLayout(device, pipelineLayout, nullptr); - pipelineLayout = VK_NULL_HANDLE; } + pipelineLayout = VK_NULL_HANDLE; } void SkyboxPipeline::draw( diff --git a/destrum/src/Graphics/RenderResources.cpp b/destrum/src/Graphics/RenderResources.cpp index 427b850..561496a 100644 --- a/destrum/src/Graphics/RenderResources.cpp +++ b/destrum/src/Graphics/RenderResources.cpp @@ -3,24 +3,31 @@ #include #include +#include "volk.h" #include "spdlog/spdlog.h" RenderResources::RenderResources() = default; void RenderResources::init(GfxDevice& gfxDevice) { - imageCache = std::make_unique(gfxDevice); - meshCache = std::make_unique(); - materialCache = std::make_unique(); + if (imageCache || meshCache || materialCache) { + cleanup(gfxDevice); + } - VkPhysicalDeviceProperties props{}; - vkGetPhysicalDeviceProperties(gfxDevice.getVkPhysicalDevice(), &props); + try { + imageCache = std::make_unique(gfxDevice); + meshCache = std::make_unique(); + materialCache = std::make_unique(); - imageCache->bindlessSetManager.init( - gfxDevice.getVkDevice(), - props.limits.maxSamplerAnisotropy); + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(gfxDevice.getVkPhysicalDevice(), &props); - { + imageCache->bindlessSetManager.init( + gfxDevice.getVkDevice(), + gfxDevice.getVkPhysicalDevice(), + props.limits.maxSamplerAnisotropy); + + { std::uint32_t white = 0xFFFFFFFF; whiteImageId = createImage( @@ -35,7 +42,7 @@ void RenderResources::init(GfxDevice& gfxDevice) &white); } - { + { std::uint32_t normal = 0xFFFF8080; // tangent-space normal: 0.5, 0.5, 1.0, 1.0 defaultNormalImageId = createImage( @@ -50,7 +57,7 @@ void RenderResources::init(GfxDevice& gfxDevice) &normal); } - { + { constexpr auto black = 0xFF000000; constexpr auto magenta = 0xFFFF00FF; @@ -74,14 +81,18 @@ void RenderResources::init(GfxDevice& gfxDevice) imageCache->setErrorImageId(errorImageId); } - materialCache->init( - gfxDevice, - MaterialDefaultTextures{ - .white = whiteImageId, - .normal = defaultNormalImageId, - .metallicRoughness = whiteImageId, - .emissive = whiteImageId, - }); + materialCache->init( + gfxDevice, + MaterialDefaultTextures{ + .white = whiteImageId, + .normal = defaultNormalImageId, + .metallicRoughness = whiteImageId, + .emissive = whiteImageId, + }); + } catch (...) { + cleanup(gfxDevice); + throw; + } } ImageID RenderResources::createImage( @@ -152,6 +163,7 @@ ImageID RenderResources::loadImageFromFile( bool mipMap, TextureIntent intent) { + (void)gfxDevice; return imageCache->loadImageFromFile(path, usage, mipMap, intent); } @@ -188,6 +200,33 @@ void RenderResources::bindBindlessDescSet(VkCommandBuffer cmd, VkPipelineLayout ); } +void RenderResources::cleanup(GfxDevice& gfxDevice) +{ + if (!imageCache && !meshCache && !materialCache) { + return; + } + + gfxDevice.waitIdle(); + + if (materialCache) { + materialCache->cleanup(gfxDevice); + } + if (meshCache) { + meshCache->cleanup(gfxDevice); + } + if (imageCache) { + imageCache->bindlessSetManager.cleanup(gfxDevice.getVkDevice()); + imageCache->destroyImages(); + } + + materialCache.reset(); + meshCache.reset(); + imageCache.reset(); + whiteImageId = NULL_IMAGE_ID; + errorImageId = NULL_IMAGE_ID; + defaultNormalImageId = NULL_IMAGE_ID; +} + std::uint32_t RenderResources::BytesPerTexel(VkFormat fmt) { switch (fmt) { @@ -208,4 +247,4 @@ std::uint32_t RenderResources::BytesPerTexel(VkFormat fmt) default: throw std::runtime_error("RenderResources::BytesPerTexel: unsupported format"); } -} \ No newline at end of file +} diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index a1b023b..d22537b 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -2,6 +2,10 @@ #include +#include +#include + +#include "volk.h" #include "destrum/Util/GameState.h" #include "spdlog/spdlog.h" @@ -13,26 +17,31 @@ GameRenderer::GameRenderer() void GameRenderer::init(GfxDevice& gfxDevice, RenderResources& _resources, glm::ivec2 drawImageSize) { - resources = &_resources; - sceneDataBuffer.init( - gfxDevice, - VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, - sizeof(GPUSceneData), - "scene data"); + try { + resources = &_resources; + sceneDataBuffer.init( + gfxDevice, + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + sizeof(GPUSceneData), + "scene data"); - createDrawImage(gfxDevice, drawImageSize, true); + createDrawImage(gfxDevice, drawImageSize, true); - meshPipeline = std::make_unique(); - meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); + meshPipeline = std::make_unique(); + meshPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); - skyboxPipeline = std::make_unique(); - skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); + skyboxPipeline = std::make_unique(); + skyboxPipeline->init(gfxDevice, _resources, drawImageFormat, depthImageFormat); - skinningPipeline = std::make_unique(); - skinningPipeline->init(gfxDevice); + skinningPipeline = std::make_unique(); + skinningPipeline->init(gfxDevice); - - GameState::GetInstance().SetRenderer(this); + GameState::GetInstance().SetRenderer(this); + initialized = true; + } catch (...) { + cleanup(gfxDevice); + throw; + } } void GameRenderer::beginDrawing(GfxDevice& gfxDevice) @@ -44,7 +53,23 @@ void GameRenderer::beginDrawing(GfxDevice& gfxDevice) void GameRenderer::endDrawing() { - //Sort the drawlist + sortedMeshDrawCommands.resize(meshDrawCommands.size()); + std::iota(sortedMeshDrawCommands.begin(), sortedMeshDrawCommands.end(), 0); + + std::stable_sort( + sortedMeshDrawCommands.begin(), + sortedMeshDrawCommands.end(), + [this](std::size_t left, std::size_t right) { + const auto& lhs = meshDrawCommands[left]; + const auto& rhs = meshDrawCommands[right]; + if (lhs.meshId != rhs.meshId) { + return lhs.meshId < rhs.meshId; + } + if (lhs.materialId != rhs.materialId) { + return lhs.materialId < rhs.materialId; + } + return (lhs.skinnedMesh != nullptr) < (rhs.skinnedMesh != nullptr); + }); } void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) @@ -55,6 +80,11 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& { TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Skinning"); + skinningPipeline->flushCurrentFrame( + cmd, + gfxDevice, + gfxDevice.getCurrentFrameIndex()); + for (const auto& dc : meshDrawCommands) { if (!dc.skinnedMesh) @@ -108,8 +138,9 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& vkutil::transitionImage( cmd, drawImage.image, - VK_IMAGE_LAYOUT_UNDEFINED, + drawImage.layout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + drawImage.setLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); } { @@ -118,17 +149,20 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& vkutil::transitionImage( cmd, depthImage.image, - VK_IMAGE_LAYOUT_UNDEFINED, + depthImage.layout, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + depthImage.setLayout(VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); } - const auto renderInfo = vkutil::createRenderingInfo({ + auto renderInfo = vkutil::createRenderingInfo({ .renderExtent = drawImage.getExtent2D(), .colorImageView = drawImage.imageView, .colorImageClearValue = glm::vec4{0.f, 0.f, 0.f, 1.f}, .depthImageView = depthImage.imageView, .depthImageClearValue = 1.f, }); + renderInfo.renderingInfo.pColorAttachments = &renderInfo.colorAttachment; + renderInfo.renderingInfo.pDepthAttachment = &renderInfo.depthAttachment; { TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "vkCmdBeginRendering"); @@ -165,9 +199,11 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& void GameRenderer::cleanup(GfxDevice& gfxDevice) { - VkDevice device = gfxDevice.getDevice().device; + VkDevice device = gfxDevice.getDevice(); - vkDeviceWaitIdle(device); + if (device != VK_NULL_HANDLE) { + vkDeviceWaitIdle(device); + } if (skinningPipeline) skinningPipeline->cleanup(gfxDevice); @@ -188,6 +224,15 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice) drawImageId = NULL_IMAGE_ID; depthImageId = NULL_IMAGE_ID; + resources = nullptr; + meshPipeline.reset(); + skyboxPipeline.reset(); + skinningPipeline.reset(); + pendingMaterialUploads.clear(); + meshDrawCommands.clear(); + sortedMeshDrawCommands.clear(); + initialized = false; + GameState::GetInstance().SetRenderer(nullptr); } void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) @@ -199,6 +244,7 @@ void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID ma meshDrawCommands.push_back(MeshDrawCommand{ .meshId = id, .transformMatrix = transform, + .worldBoundingSphere = worldBoundingSphere, .materialId = materialId, }); } @@ -210,13 +256,14 @@ void GameRenderer::drawSkinnedMesh(MeshID id, std::size_t jointMatricesStartIndex) { const auto& mesh = resources->meshes().getMesh(id); - const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false); + const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, true); assert(materialId != NULL_MATERIAL_ID); assert(skinnedMesh != nullptr); meshDrawCommands.push_back(MeshDrawCommand{ .meshId = id, .transformMatrix = transform, + .worldBoundingSphere = worldBoundingSphere, .materialId = materialId, .skinnedMesh = skinnedMesh, .jointMatricesStartIndex = static_cast(jointMatricesStartIndex), @@ -249,10 +296,20 @@ void GameRenderer::setSkyboxTexture(ImageID skyboxImageId) void GameRenderer::flushMaterialUpdates(GfxDevice& gfxDevice) { + if (!pendingMaterialUploads.empty()) { + // Material data is currently shared by all frame contexts. Wait before + // mutating it so an older in-flight frame cannot read the overwritten + // range. This can later be replaced with per-frame material buffers. + gfxDevice.waitIdle(); + } + for (MaterialID id : pendingMaterialUploads) { resources->materials().updateMaterialGPU(id); - // if non-coherent: flush mapped range for that id here + gfxDevice.getMemoryManager().flushAllocation( + resources->materials().getMaterialDataBuffer(), + static_cast(id) * sizeof(MaterialData), + sizeof(MaterialData)); } pendingMaterialUploads.clear(); } diff --git a/destrum/src/Graphics/Resources/Cubemap.cpp b/destrum/src/Graphics/Resources/Cubemap.cpp index b505d35..444d1e1 100644 --- a/destrum/src/Graphics/Resources/Cubemap.cpp +++ b/destrum/src/Graphics/Resources/Cubemap.cpp @@ -11,6 +11,7 @@ #include #include +#include "volk.h" #include "spdlog/spdlog.h" CubeMap::CubeMap() @@ -69,7 +70,7 @@ void CubeMap::RenderToCubemap( ); } - gfxDevice.GetImmediateExecuter().immediateSubmit([&](VkCommandBuffer cmd) { + gfxDevice.getImmediateExecuter().immediateSubmit([&](VkCommandBuffer cmd) { VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; @@ -201,45 +202,57 @@ void CubeMap::CreateCubeMap( std::array faceViews{}; - for (std::uint32_t face = 0; face < 6; ++face) { - VkImageViewCreateInfo viewInfo{}; - viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - viewInfo.image = cubeMapImage.image; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = VK_FORMAT_R32G32B32A32_SFLOAT; - viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - viewInfo.subresourceRange.baseMipLevel = 0; - viewInfo.subresourceRange.levelCount = 1; - viewInfo.subresourceRange.baseArrayLayer = face; - viewInfo.subresourceRange.layerCount = 1; + try { + for (std::uint32_t face = 0; face < 6; ++face) { + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = cubeMapImage.image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = VK_FORMAT_R32G32B32A32_SFLOAT; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = face; + viewInfo.subresourceRange.layerCount = 1; - if (vkCreateImageView( + VK_CHECK(vkCreateImageView( gfxDevice.getDevice(), &viewInfo, nullptr, - &faceViews[face]) != VK_SUCCESS) - { - throw std::runtime_error("Failed to create cubemap face image view."); + &faceViews[face])); } - } - spdlog::info("HDRI image id = {}", m_hdrImage); + spdlog::info("HDRI image id = {}", m_hdrImage); - RenderToCubemap( - gfxDevice, - resources, - m_hdrImage, - cubeMapImage.image, - faceViews, - m_cubeMapSize - ); + RenderToCubemap( + gfxDevice, + resources, + m_hdrImage, + cubeMapImage.image, + faceViews, + m_cubeMapSize + ); - m_cubemapImageID = resources.addImageToCache(std::move(cubeMapImage)); - - for (VkImageView view : faceViews) { - if (view != VK_NULL_HANDLE) { - vkDestroyImageView(gfxDevice.getDevice(), view, nullptr); + for (std::uint32_t face = 0; face < 6; ++face) { + cubeMapImage.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, face); } + + m_cubemapImageID = resources.addImageToCache(std::move(cubeMapImage)); + + for (VkImageView& view : faceViews) { + if (view != VK_NULL_HANDLE) { + vkDestroyImageView(gfxDevice.getDevice(), view, nullptr); + view = VK_NULL_HANDLE; + } + } + } catch (...) { + for (VkImageView& view : faceViews) { + if (view != VK_NULL_HANDLE) { + vkDestroyImageView(gfxDevice.getDevice(), view, nullptr); + } + } + gfxDevice.destroyImage(cubeMapImage); + throw; } } @@ -308,4 +321,4 @@ void CubeMap::InitCubemapPipeline( fragPath, pipelineConfig ); -} \ No newline at end of file +} diff --git a/destrum/src/Graphics/Resources/NBuffer.cpp b/destrum/src/Graphics/Resources/NBuffer.cpp index af12e31..d1259da 100644 --- a/destrum/src/Graphics/Resources/NBuffer.cpp +++ b/destrum/src/Graphics/Resources/NBuffer.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -18,29 +19,37 @@ void NBuffer::init( assert(FRAMES_IN_FLIGHT > 0); assert(dataSize > 0); - framesInFlight = FRAMES_IN_FLIGHT; - gpuBufferSize = dataSize; + try { + framesInFlight = FRAMES_IN_FLIGHT; + gpuBufferSize = dataSize; + memoryManager = &gfxDevice.getMemoryManager(); - gpuBuffer = gfxDevice.createBuffer( - dataSize, usage | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE); - vkutil::addDebugLabel(gfxDevice.getDevice(), gpuBuffer.buffer, debugName.c_str()); + gpuBuffer = gfxDevice.createBuffer( + dataSize, usage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE); + vkutil::addDebugLabel(gfxDevice.getDevice(), gpuBuffer.buffer, debugName.c_str()); - for (std::size_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { - stagingBuffers.push_back(gfxDevice.createBuffer( - dataSize, usage | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_AUTO_PREFER_HOST)); + for (std::size_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { + stagingBuffers.push_back(gfxDevice.createBuffer( + dataSize, usage | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_AUTO_PREFER_HOST)); + } + + initialized = true; + } catch (...) { + cleanup(gfxDevice); + throw; } - - initialized = true; } void NBuffer::cleanup(GfxDevice& device) { - for (const auto& stagingBuffer : stagingBuffers) { + for (auto& stagingBuffer : stagingBuffers) { device.destroyBuffer(stagingBuffer); } stagingBuffers.clear(); device.destroyBuffer(gpuBuffer); + gpuBuffer = {}; + memoryManager = nullptr; initialized = false; } @@ -66,7 +75,7 @@ void NBuffer::uploadNewData( const auto bufferBarrier = VkBufferMemoryBarrier2{ .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, - .srcAccessMask = VK_ACCESS_2_MEMORY_READ_BIT, + .srcAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT, .dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, .dstAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT, .buffer = gpuBuffer.buffer, @@ -84,6 +93,10 @@ void NBuffer::uploadNewData( auto& staging = stagingBuffers[frameIndex]; auto* mappedData = reinterpret_cast(staging.info.pMappedData); memcpy((void*)&mappedData[offset], newData, dataSize); + memoryManager->flushAllocation( + staging, + static_cast(offset), + static_cast(dataSize)); const auto region = VkBufferCopy2{ .sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2, @@ -91,6 +104,12 @@ void NBuffer::uploadNewData( .dstOffset = (VkDeviceSize)offset, .size = dataSize, }; + vkutil::bufferHostWriteToTransferReadBarrier( + cmd, + staging.buffer, + static_cast(offset), + static_cast(dataSize)); + const auto bufCopyInfo = VkCopyBufferInfo2{ .sType = VK_STRUCTURE_TYPE_COPY_BUFFER_INFO_2, .srcBuffer = staging.buffer, diff --git a/destrum/src/Graphics/Swapchain.cpp b/destrum/src/Graphics/Swapchain.cpp index df2ee10..4f3ea5f 100644 --- a/destrum/src/Graphics/Swapchain.cpp +++ b/destrum/src/Graphics/Swapchain.cpp @@ -1,12 +1,13 @@ #include +#include +#include #include #include -#include #include -#include "volk.h" -#include "tracy/Tracy.hpp" +#include +#include void Swapchain::initSync(VkDevice device) { @@ -23,31 +24,39 @@ void Swapchain::initSync(VkDevice device) { } } -void Swapchain::createSwapchain(GfxDevice* gfxDevice, VkFormat format, std::uint32_t width, std::uint32_t height, bool vSync) { +void Swapchain::createSwapchain( + VkDevice, + vkb::Device vkbDevice, + VkSurfaceKHR surf, + VkFormat format, + std::uint32_t width, + std::uint32_t height, + bool vSync) +{ ZoneScopedN("Swapchain::createSwapchain"); - m_gfxDevice = gfxDevice; - assert(format == VK_FORMAT_B8G8R8A8_SRGB && "TODO: test other formats"); - // vSync = true; + surface = surf; { ZoneScopedN("vkb::SwapchainBuilder::build"); - auto res = vkb::SwapchainBuilder{gfxDevice->getDevice()} + auto res = vkb::SwapchainBuilder{vkbDevice, surface} .set_desired_format(VkSurfaceFormatKHR{ .format = format, .colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, }) - .add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) + .add_image_usage_flags( + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) .set_desired_present_mode( - vSync ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR) + vSync ? VK_PRESENT_MODE_FIFO_KHR + : VK_PRESENT_MODE_IMMEDIATE_KHR) .set_desired_extent(width, height) .build(); + if (!res.has_value()) { - // throw std::runtime_error(std::format( - // "failed to create swapchain: error = {}, vk result = {}", - // res.full_error().type.message(), - // string_VkResult(res.full_error().vk_result))); + throw std::runtime_error( + "Failed to create swapchain: " + res.error().message()); } m_swapchain = res.value(); } @@ -55,8 +64,13 @@ void Swapchain::createSwapchain(GfxDevice* gfxDevice, VkFormat format, std::uint { ZoneScopedN("Get Swapchain Images / Views"); - images = m_swapchain.get_images().value(); - imageViews = m_swapchain.get_image_views().value(); + const auto imageResult = m_swapchain.get_images(); + const auto viewResult = m_swapchain.get_image_views(); + if (!imageResult.has_value() || !viewResult.has_value()) { + throw std::runtime_error("Failed to retrieve swapchain images or views"); + } + images = imageResult.value(); + imageViews = viewResult.value(); } imageRenderSemaphores.resize(images.size()); @@ -65,20 +79,20 @@ void Swapchain::createSwapchain(GfxDevice* gfxDevice, VkFormat format, std::uint .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; - for (auto& sem: imageRenderSemaphores) { + for (auto& sem : imageRenderSemaphores) { ZoneScopedN("Create Image Render Semaphore"); - VK_CHECK(vkCreateSemaphore(m_gfxDevice->getDevice(), &sci, nullptr, &sem)); + VK_CHECK(vkCreateSemaphore(vkbDevice, &sci, nullptr, &sem)); } - // TODO: if re-creation of swapchain is supported, don't forget to call - // vkutil::initSwapchainViews here. - extent = m_swapchain.extent; + dirty = false; } void Swapchain::recreateSwapchain( - const GfxDevice& gfxDevice, + VkDevice, + vkb::Device vkbDevice, + VkSurfaceKHR surf, VkFormat format, std::uint32_t width, std::uint32_t height, @@ -91,11 +105,11 @@ void Swapchain::recreateSwapchain( return; } - VkDevice device = gfxDevice.getDevice(); + surface = surf; { ZoneScopedN("vkDeviceWaitIdle"); - vkDeviceWaitIdle(device); + vkDeviceWaitIdle(vkbDevice); } auto oldSwapchain = m_swapchain; @@ -103,23 +117,24 @@ void Swapchain::recreateSwapchain( { ZoneScopedN("vkb::SwapchainBuilder::rebuild"); - auto res = vkb::SwapchainBuilder{gfxDevice.getVkbDevice()} + auto res = vkb::SwapchainBuilder{vkbDevice, surface} .set_old_swapchain(oldSwapchain) .set_desired_format(VkSurfaceFormatKHR{ .format = format, .colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, }) - .add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) + .add_image_usage_flags( + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) .set_desired_present_mode( - vSync ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR) + vSync ? VK_PRESENT_MODE_FIFO_KHR + : VK_PRESENT_MODE_IMMEDIATE_KHR) .set_desired_extent(width, height) .build(); if (!res.has_value()) { - // throw std::runtime_error(std::format( - // "failed to create swapchain: error = {}, vk result = {}", - // res.full_error().type.message(), - // string_VkResult(res.full_error().vk_result))); + throw std::runtime_error( + "Failed to recreate swapchain: " + res.error().message()); } m_swapchain = res.value(); @@ -129,7 +144,7 @@ void Swapchain::recreateSwapchain( ZoneScopedN("Destroy Old Image Render Semaphores"); for (auto sem : imageRenderSemaphores) { - vkDestroySemaphore(device, sem, nullptr); + vkDestroySemaphore(vkbDevice, sem, nullptr); } imageRenderSemaphores.clear(); } @@ -138,7 +153,7 @@ void Swapchain::recreateSwapchain( ZoneScopedN("Destroy Old Image Views"); for (auto imageView : imageViews) { - vkDestroyImageView(device, imageView, nullptr); + vkDestroyImageView(vkbDevice, imageView, nullptr); } imageViews.clear(); } @@ -151,8 +166,13 @@ void Swapchain::recreateSwapchain( { ZoneScopedN("Get New Swapchain Images / Views"); - images = m_swapchain.get_images().value(); - imageViews = m_swapchain.get_image_views().value(); + const auto imageResult = m_swapchain.get_images(); + const auto viewResult = m_swapchain.get_image_views(); + if (!imageResult.has_value() || !viewResult.has_value()) { + throw std::runtime_error("Failed to retrieve recreated swapchain images or views"); + } + images = imageResult.value(); + imageViews = viewResult.value(); } VkSemaphoreCreateInfo sci{ @@ -164,57 +184,68 @@ void Swapchain::recreateSwapchain( for (auto& sem : imageRenderSemaphores) { ZoneScopedN("Create New Image Render Semaphore"); - VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &sem)); + VK_CHECK(vkCreateSemaphore(vkbDevice, &sci, nullptr, &sem)); } extent = m_swapchain.extent; dirty = false; } -void Swapchain::cleanup() { - for (auto& frame: frames) { - vkDestroyFence(m_gfxDevice->getDevice(), frame.renderFence, nullptr); - vkDestroySemaphore(m_gfxDevice->getDevice(), frame.swapchainSemaphore, nullptr); - } { - // destroy swapchain and its views - for (auto imageView: imageViews) { - vkDestroyImageView(m_gfxDevice->getDevice(), imageView, nullptr); +void Swapchain::cleanup(VkDevice device) { + for (auto& frame : frames) { + if (frame.renderFence != VK_NULL_HANDLE) { + vkDestroyFence(device, frame.renderFence, nullptr); + frame.renderFence = VK_NULL_HANDLE; + } + if (frame.swapchainSemaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(device, frame.swapchainSemaphore, nullptr); + frame.swapchainSemaphore = VK_NULL_HANDLE; } - imageViews.clear(); - - vkb::destroy_swapchain(m_swapchain); } + + for (auto& semaphore : imageRenderSemaphores) { + if (semaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(device, semaphore, nullptr); + semaphore = VK_NULL_HANDLE; + } + } + imageRenderSemaphores.clear(); + + for (auto imageView : imageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + imageViews.clear(); + + vkb::destroy_swapchain(m_swapchain); + m_swapchain = {}; + images.clear(); + extent = {}; + dirty = false; } -void Swapchain::beginFrame(int index) const { +void Swapchain::beginFrame(VkDevice device, int index) const { ZoneScopedN("Swapchain::beginFrame"); auto& frame = frames[index]; { ZoneScopedN("vkWaitForFences"); - VK_CHECK(vkWaitForFences(m_gfxDevice->getDevice(), 1, &frame.renderFence, true, std::numeric_limits::max())); + VK_CHECK(vkWaitForFences(device, 1, &frame.renderFence, true, std::numeric_limits::max())); } } -void Swapchain::resetFences(int index) const { +void Swapchain::resetFences(VkDevice device, int index) const { ZoneScopedN("Swapchain::resetFences"); auto& frame = frames[index]; { ZoneScopedN("vkResetFences"); - VK_CHECK(vkResetFences(m_gfxDevice->getDevice(), 1, &frame.renderFence)); + VK_CHECK(vkResetFences(device, 1, &frame.renderFence)); } } -struct SwapchainAcquireResult { - VkResult result = VK_SUCCESS; - VkImage image = VK_NULL_HANDLE; - uint32_t imageIndex = 0; -}; - -std::pair Swapchain::acquireNextImage(int index) { +Swapchain::AcquireResult Swapchain::acquireNextImage(VkDevice device, std::uint32_t index) { ZoneScopedN("Swapchain::acquireNextImage"); std::uint32_t swapchainImageIndex{}; @@ -225,7 +256,7 @@ std::pair Swapchain::acquireNextImage(int index) { ZoneScopedN("vkAcquireNextImageKHR"); result = vkAcquireNextImageKHR( - m_gfxDevice->getDevice(), + device, m_swapchain, std::numeric_limits::max(), frames[index].swapchainSemaphore, @@ -233,29 +264,42 @@ std::pair Swapchain::acquireNextImage(int index) { &swapchainImageIndex); } - if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { + if (result == VK_ERROR_OUT_OF_DATE_KHR) { dirty = true; - return {images[swapchainImageIndex], swapchainImageIndex}; - } else if (result != VK_SUCCESS) { + return {.result = result}; + } + if (result == VK_SUBOPTIMAL_KHR) { + dirty = true; + return { + .result = result, + .image = images.at(swapchainImageIndex), + .imageIndex = swapchainImageIndex, + }; + } + if (result != VK_SUCCESS) { throw std::runtime_error("failed to acquire swap chain image!"); } - return {images[swapchainImageIndex], swapchainImageIndex}; + return { + .result = result, + .image = images.at(swapchainImageIndex), + .imageIndex = swapchainImageIndex, + }; } void Swapchain::submitAndPresent( + VkDevice device, VkCommandBuffer cmd, VkQueue graphicsQueue, - uint32_t imageIndex, // from vkAcquireNextImageKHR - uint32_t frameIndex) // 0..FRAMES_IN_FLIGHT-1 + VkQueue presentQueue, + std::uint32_t imageIndex, + std::uint32_t frameIndex) { ZoneScopedN("Swapchain::submitAndPresent"); - auto& frame = frames[frameIndex]; // ✅ per-frame + auto& frame = frames[frameIndex]; + VkSemaphore renderFinished = imageRenderSemaphores[imageIndex]; - VkSemaphore renderFinished = imageRenderSemaphores[imageIndex]; // ✅ per-image - - // submit VkCommandBufferSubmitInfo cmdInfo{ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = cmd, @@ -263,38 +307,51 @@ void Swapchain::submitAndPresent( VkSemaphoreSubmitInfo waitInfo = vkinit::semaphoreSubmitInfo( - VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT_KHR, - frame.swapchainSemaphore); // ✅ acquire semaphore (per-frame) + VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + frame.swapchainSemaphore); VkSemaphoreSubmitInfo signalInfo = vkinit::semaphoreSubmitInfo( - VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT, - renderFinished); // ✅ signal semaphore (per-image) + VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + renderFinished); VkSubmitInfo2 submit = vkinit::submitInfo(&cmdInfo, &waitInfo, &signalInfo); + VK_CHECK(vkResetFences(device, 1, &frame.renderFence)); + { ZoneScopedN("vkQueueSubmit2"); - VK_CHECK(vkQueueSubmit2(graphicsQueue, 1, &submit, frame.renderFence)); // ✅ fence (per-frame) + const VkResult submitResult = vkQueueSubmit2(graphicsQueue, 1, &submit, frame.renderFence); + if (submitResult != VK_SUCCESS) { + vkDestroyFence(device, frame.renderFence, nullptr); + constexpr VkFenceCreateInfo fenceInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .flags = VK_FENCE_CREATE_SIGNALED_BIT, + }; + VK_CHECK(vkCreateFence(device, &fenceInfo, nullptr, &frame.renderFence)); + checkVkResult(submitResult, "vkQueueSubmit2", __FILE__, __LINE__); + } } - // present VkPresentInfoKHR presentInfo{ .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, .waitSemaphoreCount = 1, .pWaitSemaphores = &renderFinished, .swapchainCount = 1, .pSwapchains = &m_swapchain.swapchain, - .pImageIndices = &imageIndex, // ✅ imageIndex, NOT frameIndex + .pImageIndices = &imageIndex, }; VkResult res = VK_SUCCESS; { ZoneScopedN("vkQueuePresentKHR"); - res = vkQueuePresentKHR(graphicsQueue, &presentInfo); + res = vkQueuePresentKHR(presentQueue, &presentInfo); } - if (res == VK_ERROR_OUT_OF_DATE_KHR || res == VK_SUBOPTIMAL_KHR) dirty = true; - else if (res != VK_SUCCESS) dirty = true; + if (res == VK_ERROR_OUT_OF_DATE_KHR || res == VK_SUBOPTIMAL_KHR) { + dirty = true; + } else if (res != VK_SUCCESS) { + throw std::runtime_error("failed to present swap chain image"); + } } diff --git a/destrum/src/Graphics/Util.cpp b/destrum/src/Graphics/Util.cpp index b24a734..d461f96 100644 --- a/destrum/src/Graphics/Util.cpp +++ b/destrum/src/Graphics/Util.cpp @@ -6,23 +6,87 @@ #include "spdlog/spdlog.h" -void vkutil::transitionImage(VkCommandBuffer cmd, VkImage image, VkImageLayout currentLayout, VkImageLayout newLayout) { - VkImageAspectFlags aspectMask = - (currentLayout == VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL || - newLayout == VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL || - newLayout == VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL) - ? VK_IMAGE_ASPECT_DEPTH_BIT - : VK_IMAGE_ASPECT_COLOR_BIT; +namespace { + VkImageAspectFlags AspectForLayout(VkImageLayout currentLayout, VkImageLayout newLayout) + { + return (currentLayout == VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL || + newLayout == VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL || + newLayout == VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL) + ? VK_IMAGE_ASPECT_DEPTH_BIT + : VK_IMAGE_ASPECT_COLOR_BIT; + } + + struct LayoutSync { + VkPipelineStageFlags2 stage{VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT}; + VkAccessFlags2 access{VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT}; + }; + + LayoutSync SyncForLayout(VkImageLayout layout, bool source) + { + switch (layout) { + case VK_IMAGE_LAYOUT_UNDEFINED: + return {VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, 0}; + case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: + return {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}; + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + return {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + return { + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT}; + case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL: + return { + VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT, + VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT}; + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: + case VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL: + return {VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_READ_BIT}; + case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: + return {VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_READ_BIT}; + default: + return source + ? LayoutSync{} + : LayoutSync{VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT}; + } + } +} + +void vkutil::transitionImage( + VkCommandBuffer cmd, + VkImage image, + VkImageLayout currentLayout, + VkImageLayout newLayout) +{ + transitionImage( + cmd, + image, + currentLayout, + newLayout, + vkinit::imageSubresourceRange(AspectForLayout(currentLayout, newLayout))); +} + +void vkutil::transitionImage( + VkCommandBuffer cmd, + VkImage image, + VkImageLayout currentLayout, + VkImageLayout newLayout, + const VkImageSubresourceRange& subresourceRange) +{ + const LayoutSync sourceSync = SyncForLayout(currentLayout, true); + const LayoutSync destinationSync = SyncForLayout(newLayout, false); VkImageMemoryBarrier2 imageBarrier{ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, - .srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT, - .dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, - .dstAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT, + .srcStageMask = sourceSync.stage, + .srcAccessMask = sourceSync.access, + .dstStageMask = destinationSync.stage, + .dstAccessMask = destinationSync.access, .oldLayout = currentLayout, .newLayout = newLayout, .image = image, - .subresourceRange = vkinit::imageSubresourceRange(aspectMask), + .subresourceRange = subresourceRange, }; VkDependencyInfo depInfo{ @@ -101,6 +165,28 @@ void vkutil::bufferHostWriteToShaderReadBarrier( vkCmdPipelineBarrier2(cmd, &dep); } +void vkutil::bufferHostWriteToTransferReadBarrier( + VkCommandBuffer cmd, + VkBuffer buffer, + VkDeviceSize offset, + VkDeviceSize size) +{ + VkBufferMemoryBarrier2 barrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2}; + barrier.srcStageMask = VK_PIPELINE_STAGE_2_HOST_BIT; + barrier.srcAccessMask = VK_ACCESS_2_HOST_WRITE_BIT; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT; + barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT; + barrier.buffer = buffer; + barrier.offset = offset; + barrier.size = size; + + VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO}; + dep.bufferMemoryBarrierCount = 1; + dep.pBufferMemoryBarriers = &barrier; + + vkCmdPipelineBarrier2(cmd, &dep); +} + void vkutil::addDebugLabel(VkDevice device, VkImage image, const char* label) { const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ @@ -109,7 +195,7 @@ void vkutil::addDebugLabel(VkDevice device, VkImage image, const char* label) { .objectHandle = (std::uint64_t)image, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void vkutil::addDebugLabel(VkDevice device, VkShaderModule shaderModule, const char* label) { @@ -119,7 +205,7 @@ void vkutil::addDebugLabel(VkDevice device, VkShaderModule shaderModule, const c .objectHandle = (std::uint64_t)shaderModule, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void vkutil::addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) { @@ -129,7 +215,7 @@ void vkutil::addDebugLabel(VkDevice device, VkPipeline pipeline, const char* lab .objectHandle = (std::uint64_t)pipeline, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) { @@ -139,7 +225,7 @@ void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) .objectHandle = (std::uint64_t)buffer, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void vkutil::addDebugLabel(VkDevice device, VkSampler sampler, const char* label) { @@ -149,7 +235,7 @@ void vkutil::addDebugLabel(VkDevice device, VkSampler sampler, const char* label .objectHandle = (std::uint64_t)sampler, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } vkutil::RenderInfo vkutil::createRenderingInfo(const RenderingInfoParams& params) { @@ -207,15 +293,19 @@ vkutil::RenderInfo vkutil::createRenderingInfo(const RenderingInfoParams& params VkShaderModule vkutil::loadShaderModule(const std::filesystem::path& path, VkDevice device) { std::ifstream file(path, std::ios::ate | std::ios::binary); if (!file.is_open()) { - spdlog::error("failed to open shader"); - std::exit(1); + throw std::runtime_error("Failed to open shader: " + path.string()); } const auto fileSize = file.tellg(); + if (fileSize < 0 || fileSize % static_cast(sizeof(std::uint32_t)) != 0) { + throw std::runtime_error("Shader file has an invalid size: " + path.string()); + } std::vector buffer(fileSize / sizeof(std::uint32_t)); file.seekg(0); - file.read((char*)buffer.data(), fileSize); + if (!file.read(reinterpret_cast(buffer.data()), fileSize)) { + throw std::runtime_error("Failed to read shader: " + path.string()); + } file.close(); auto info = VkShaderModuleCreateInfo{ @@ -224,11 +314,8 @@ VkShaderModule vkutil::loadShaderModule(const std::filesystem::path& path, VkDev .pCode = buffer.data(), }; - VkShaderModule shaderModule; - if (vkCreateShaderModule(device, &info, nullptr, &shaderModule) != VK_SUCCESS) { - spdlog::error("Failed to load"); - std::exit(1); - } + VkShaderModule shaderModule{VK_NULL_HANDLE}; + VK_CHECK(vkCreateShaderModule(device, &info, nullptr, &shaderModule)); return shaderModule; } @@ -239,7 +326,7 @@ void addDebugLabel(VkDevice device, VkImage image, const char* label) { .objectHandle = (std::uint64_t)image, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void addDebugLabel(VkDevice device, VkImageView imageView, const char* label) { @@ -249,7 +336,7 @@ void addDebugLabel(VkDevice device, VkImageView imageView, const char* label) { .objectHandle = (std::uint64_t)imageView, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void addDebugLabel(VkDevice device, VkShaderModule shaderModule, const char* label) { @@ -259,7 +346,7 @@ void addDebugLabel(VkDevice device, VkShaderModule shaderModule, const char* lab .objectHandle = (std::uint64_t)shaderModule, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) { @@ -269,7 +356,7 @@ void addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) { .objectHandle = (std::uint64_t)pipeline, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void addDebugLabel(VkDevice device, VkPipelineLayout layout, const char* label) { @@ -279,7 +366,7 @@ void addDebugLabel(VkDevice device, VkPipelineLayout layout, const char* label) .objectHandle = (std::uint64_t)layout, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) { @@ -289,7 +376,7 @@ void addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) { .objectHandle = (std::uint64_t)buffer, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } void addDebugLabel(VkDevice device, VkSampler sampler, const char* label) { @@ -299,57 +386,9 @@ void addDebugLabel(VkDevice device, VkSampler sampler, const char* label) { .objectHandle = (std::uint64_t)sampler, .pObjectName = label, }; - vkSetDebugUtilsObjectNameEXT(device, &nameInfo); + if (vkSetDebugUtilsObjectNameEXT) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } vkutil::RenderInfo createRenderingInfo(const vkutil::RenderingInfoParams& params) { - assert( - (params.colorImageView || params.depthImageView != nullptr) && - "Either draw or depth image should be present"); - assert( - params.renderExtent.width != 0.f && params.renderExtent.height != 0.f && - "renderExtent not specified"); - - vkutil::RenderInfo ri; - if (params.colorImageView) { - ri.colorAttachment = VkRenderingAttachmentInfo{ - .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - .imageView = params.colorImageView, - .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .loadOp = params.colorImageClearValue ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD, - .storeOp = VK_ATTACHMENT_STORE_OP_STORE, - }; - if (params.colorImageClearValue) { - const auto col = params.colorImageClearValue.value(); - ri.colorAttachment.clearValue.color = {col[0], col[1], col[2], col[3]}; - } - } - - if (params.depthImageView) { - ri.depthAttachment = VkRenderingAttachmentInfo{ - .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - .imageView = params.depthImageView, - .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, - .loadOp = params.depthImageClearValue ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD, - .storeOp = VK_ATTACHMENT_STORE_OP_STORE, - }; - if (params.depthImageClearValue) { - ri.depthAttachment.clearValue.depthStencil.depth = params.depthImageClearValue.value(); - } - } - - ri.renderingInfo = VkRenderingInfo{ - .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, - .renderArea = - VkRect2D{ - .offset = {}, - .extent = params.renderExtent, - }, - .layerCount = 1, - .colorAttachmentCount = params.colorImageView ? 1u : 0u, - .pColorAttachments = params.colorImageView ? &ri.colorAttachment : nullptr, - .pDepthAttachment = params.depthImageView ? &ri.depthAttachment : nullptr, - }; - - return ri; + return vkutil::createRenderingInfo(params); } diff --git a/destrum/src/ObjectModel/Component.cpp b/destrum/src/ObjectModel/Component.cpp index f7aebaf..34b1611 100644 --- a/destrum/src/ObjectModel/Component.cpp +++ b/destrum/src/ObjectModel/Component.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include Component::Component(GameObject& pParent, const std::string& name) : Object(name), @@ -14,6 +16,7 @@ Transform& Component::GetTransform() const { void Component::Destroy() { Object::Destroy(); + NotifyPhysicsChanged(); } void Component::Start() { @@ -21,12 +24,30 @@ void Component::Start() { void Component::SetEnabled(bool enabled) { m_IsEnabled = enabled; + NotifyPhysicsChanged(); } -void Component::LateUpdate() { +void Component::NotifyPhysicsChanged() { + if (m_ParentGameObjectPtr != nullptr) { + if (auto* rigidbody = m_ParentGameObjectPtr->GetComponent(); + rigidbody != nullptr && rigidbody->GetPhysicsWorld() != nullptr) { + rigidbody->GetPhysicsWorld()->RefreshRigidbody(*rigidbody); + } else { + m_ParentGameObjectPtr->RefreshPhysics(); + } + } } -void Component::FixedUpdate() { +void Component::Update(float dt) { + m_LastUpdateDeltaTime = dt; +} + +void Component::LateUpdate(float dt) { + m_LastUpdateDeltaTime = dt; +} + +void Component::FixedUpdate(float fixedDt) { + m_LastFixedDeltaTime = fixedDt; } void Component::ImGuiInspector() { @@ -35,5 +56,5 @@ void Component::ImGuiInspector() { void Component::ImGuiRender() { } -void Component::Render(const RenderContext& ctx) { -} \ No newline at end of file +void Component::Render(const RenderContext&) { +} diff --git a/destrum/src/ObjectModel/GameObject.cpp b/destrum/src/ObjectModel/GameObject.cpp index 0646cd4..184a910 100644 --- a/destrum/src/ObjectModel/GameObject.cpp +++ b/destrum/src/ObjectModel/GameObject.cpp @@ -1,32 +1,72 @@ #include -#include -#include +#include +#include +#include -#include "spdlog/spdlog.h" +#include +#include +#include + +namespace { + [[nodiscard]] std::vector SnapshotComponents(const GameObject& object) { + std::vector components; + components.reserve(object.GetComponents().size()); + for (const auto& component : object.GetComponents()) { + if (component != nullptr) { + components.push_back(component.get()); + } + } + return components; + } + + bool EnsureComponentStarted(Component& component) { + if (!component.HasStarted) { + component.Start(); + component.HasStarted = true; + } + return !component.IsBeingDestroyed(); + } +} GameObject::~GameObject() { - // spdlog::debug("GameObject destroyed: {}", GetName()); + if (!IsBeingDestroyed()) { + Destroy(); + } } GameObject::GameObject(const std::string& name) : Object(name), - m_Id(s_NextId++) { + m_Id(AllocateId()) { } void GameObject::SetIdForDeserialization(ObjectId id) { + if (id == InvalidObjectId || id >= std::numeric_limits::max() - 1) { + throw std::out_of_range("Object ID is reserved or cannot be incremented"); + } + m_Id = id; // Prevent newly created objects from reusing loaded IDs. - if (id >= s_NextId) { + if (id != std::numeric_limits::max() && id >= s_NextId) { s_NextId = id + 1; } } +ObjectId GameObject::AllocateId() { + if (s_NextId == InvalidObjectId || + s_NextId >= std::numeric_limits::max() - 1) { + throw std::overflow_error("Object ID range exhausted"); + } + return s_NextId++; +} + void GameObject::SetActiveDirty() { m_ActiveDirty = true; for (const Transform* child : m_TransformPtr.GetChildren()) { - child->GetOwner()->SetActiveDirty(); + if (child != nullptr && child->GetOwner() != nullptr) { + child->GetOwner()->SetActiveDirty(); + } } } @@ -35,8 +75,10 @@ void GameObject::UpdateActiveState() { if (parentPtr == nullptr) { m_ActiveInHierarchy = m_Active; - } else { + } else if (parentPtr->GetOwner() != nullptr) { m_ActiveInHierarchy = m_Active && parentPtr->GetOwner()->IsActiveInHierarchy(); + } else { + m_ActiveInHierarchy = m_Active; } m_ActiveDirty = false; @@ -50,61 +92,112 @@ bool GameObject::IsActiveInHierarchy() { return m_ActiveInHierarchy; } -void GameObject::Update() { - for (const auto& component : m_Components) { +void GameObject::Update(float dt) { + for (Component* component : SnapshotComponents(*this)) { + if (component->IsBeingDestroyed()) { + continue; + } + if (!component->isEnabled()) { continue; } - if (!component->HasStarted) { - component->Start(); - component->HasStarted = true; + if (!EnsureComponentStarted(*component)) { + continue; } - component->Update(); + component->Update(dt); } } -void GameObject::LateUpdate() { - for (const auto& component : m_Components) { - if (component->isEnabled()) { - component->LateUpdate(); +void GameObject::LateUpdate(float dt) { + for (Component* component : SnapshotComponents(*this)) { + if (component->isEnabled() && !component->IsBeingDestroyed()) { + if (!EnsureComponentStarted(*component)) { + continue; + } + component->LateUpdate(dt); } } } -void GameObject::FixedUpdate() { - for (const auto& component : m_Components) { - if (component->isEnabled()) { - component->FixedUpdate(); +void GameObject::FixedUpdate(float fixedDt) { + for (Component* component : SnapshotComponents(*this)) { + if (component->isEnabled() && !component->IsBeingDestroyed()) { + if (!EnsureComponentStarted(*component)) { + continue; + } + component->FixedUpdate(fixedDt); } } } void GameObject::Render(const RenderContext& ctx) const { - for (const auto& component : m_Components) { - if (component->isEnabled()) { + for (Component* component : SnapshotComponents(*this)) { + if (component->isEnabled() && !component->IsBeingDestroyed()) { + if (!EnsureComponentStarted(*component)) { + continue; + } component->Render(ctx); } } } void GameObject::Destroy() { + if (IsBeingDestroyed()) { + return; + } + Object::Destroy(); + if (m_Scene != nullptr) { + m_Scene->GetPhysics().UnregisterGameObject(*this); + } + + for (Component* component : SnapshotComponents(*this)) { + if (auto* rigidbody = dynamic_cast(component); + rigidbody != nullptr && rigidbody->GetPhysicsWorld() != nullptr) { + rigidbody->GetPhysicsWorld()->UnregisterRigidbody(*rigidbody); + } + } + m_TransformPtr.SetParent(nullptr); - for (const auto& component : m_Components) { - component->Destroy(); + for (Component* component : SnapshotComponents(*this)) { + if (component != nullptr) { + component->Destroy(); + } } - for (const auto child : m_TransformPtr.GetChildren()) { - child->GetOwner()->Destroy(); + // Destroying a child detaches it from this transform, so iterate over a + // snapshot rather than the live child vector. + const std::vector children = m_TransformPtr.GetChildren(); + for (Transform* child : children) { + if (child != nullptr && child->GetOwner() != nullptr) { + child->GetOwner()->Destroy(); + } } } void GameObject::CleanupComponents() { + for (const auto& component : m_Components) { + if (!component || !component->IsBeingDestroyed()) { + continue; + } + + if (auto* rigidbody = dynamic_cast(component.get()); rigidbody != nullptr && + rigidbody->GetPhysicsWorld() != nullptr) { + rigidbody->GetPhysicsWorld()->UnregisterRigidbody(*rigidbody); + } + } + std::erase_if(m_Components, [](const std::unique_ptr& component) { - return component->IsBeingDestroyed(); + return component == nullptr || component->IsBeingDestroyed(); }); -} \ No newline at end of file +} + +void GameObject::RefreshPhysics() { + if (m_Scene != nullptr) { + m_Scene->RefreshPhysics(); + } +} diff --git a/destrum/src/ObjectModel/Object.cpp b/destrum/src/ObjectModel/Object.cpp index 4c5f245..1c7ab26 100644 --- a/destrum/src/ObjectModel/Object.cpp +++ b/destrum/src/ObjectModel/Object.cpp @@ -1,19 +1,13 @@ #include -#include -#include - -#include "spdlog/spdlog.h" - Object::~Object() { - if (!m_BeingDestroyed) { - assert(false && "Objects destructor called before destroy"); - } - // spdlog::debug("Object Destroyed: {}", m_Name); } void Object::Destroy() { - // spdlog::debug("Object marked for destruction: {}", m_Name); + if (m_BeingDestroyed) { + return; + } + m_BeingDestroyed = true; } diff --git a/destrum/src/ObjectModel/Transform.cpp b/destrum/src/ObjectModel/Transform.cpp index ca35a76..e14aae6 100644 --- a/destrum/src/ObjectModel/Transform.cpp +++ b/destrum/src/ObjectModel/Transform.cpp @@ -1,53 +1,119 @@ #include #include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + bool IsChildRecursive(const Transform* parent, + const Transform* target, + std::unordered_set& visited) { + if (parent == nullptr || !visited.insert(parent).second) { + return false; + } + for (const Transform* candidate : parent->GetChildren()) { + if (candidate == target || IsChildRecursive(candidate, target, visited)) { + return true; + } + } + return false; + } + + [[nodiscard]] glm::mat4 ComposeMatrix(const glm::vec3& position, + const glm::quat& rotation, + const glm::vec3& scale) { + return glm::translate(glm::mat4{1.0f}, position) + * glm::mat4_cast(rotation) + * glm::scale(glm::mat4{1.0f}, scale); + } + + [[nodiscard]] bool DecomposeMatrix(const glm::mat4& matrix, + glm::vec3& position, + glm::quat& rotation, + glm::vec3& scale) { + glm::vec3 skew{}; + glm::vec4 perspective{}; + if (!glm::decompose(matrix, scale, rotation, position, skew, perspective)) { + return false; + } + + if (glm::length(skew) > 0.0001f || + glm::length(perspective - glm::vec4{0.0f, 0.0f, 0.0f, 1.0f}) > 0.0001f) { + return false; + } + + const float rotationLength = glm::length(rotation); + if (rotationLength <= 0.000001f || !std::isfinite(rotationLength)) { + return false; + } + + rotation = glm::normalize(rotation); + return true; + } +} + Transform::Transform(GameObject* owner): m_Owner(owner) { } Transform::~Transform() { - SetParent(nullptr); + if (m_Parent != nullptr) { + Transform* parent = m_Parent; + m_Parent = nullptr; + parent->RemoveChild(this); + } - for (auto it = m_Children.begin(); it != m_Children.end();) { - Transform* child = *it; - it = m_Children.erase(it); - child->SetParent(nullptr); + // Do not call SetParent while the owning object is being torn down. The + // parent may already be in its destructor, so update the links directly. + const std::vector children = std::move(m_Children); + m_Children.clear(); + for (Transform* child : children) { + if (child == nullptr || child->m_Parent != this) { + continue; + } + + child->m_Parent = nullptr; + child->SetPositionDirty(); } } const glm::vec3& Transform::GetWorldPosition() { - if (m_PositionDirty) { - UpdateWorldPosition(); + if (m_PositionDirty || m_RotationDirty || m_ScaleDirty || m_MatrixDirty) { + UpdateWorldCache(); } return m_WorldPosition; } const glm::quat& Transform::GetWorldRotation() { - if (m_RotationDirty) { - UpdateWorldRotation(); + if (m_PositionDirty || m_RotationDirty || m_ScaleDirty || m_MatrixDirty) { + UpdateWorldCache(); } return m_WorldRotation; } const glm::vec3& Transform::GetWorldScale() { - if (m_ScaleDirty) { - UpdateWorldScale(); + if (m_PositionDirty || m_RotationDirty || m_ScaleDirty || m_MatrixDirty) { + UpdateWorldCache(); } return m_WorldScale; } const glm::mat4& Transform::GetWorldMatrix() { - if (m_MatrixDirty) { - UpdateWorldMatrix(); + if (m_PositionDirty || m_RotationDirty || m_ScaleDirty || m_MatrixDirty) { + UpdateWorldCache(); } return m_WorldMatrix; } void Transform::SetWorldPosition(const glm::vec3& position) { - if (m_Parent == nullptr) { - SetLocalPosition(position); - } else { - SetLocalPosition(position - m_Parent->GetWorldPosition()); - } + glm::mat4 worldMatrix = GetWorldMatrix(); + worldMatrix[3] = glm::vec4(position, 1.0f); + SetLocalFromWorldMatrix(worldMatrix); } void Transform::SetWorldPosition(float x, float y, float z) { @@ -55,11 +121,10 @@ void Transform::SetWorldPosition(float x, float y, float z) { } void Transform::SetWorldRotation(const glm::quat& rotation) { - if(m_Parent == nullptr) { - SetLocalRotation(rotation); - } else { - SetLocalRotation(glm::inverse(m_Parent->GetWorldRotation()) * rotation); - } + const glm::vec3 worldPosition = GetWorldPosition(); + const glm::vec3 worldScale = GetWorldScale(); + const glm::mat4 worldMatrix = ComposeMatrix(worldPosition, rotation, worldScale); + SetLocalFromWorldMatrix(worldMatrix); } void Transform::SetWorldRotation(const glm::vec3& rotation) { @@ -75,11 +140,10 @@ void Transform::SetWorldScale(double x, double y, double z) { } void Transform::SetWorldScale(const glm::vec3& scale) { - if (m_Parent == nullptr) { - SetLocalScale(scale); - } else { - SetLocalScale(scale - m_Parent->GetWorldScale()); - } + const glm::vec3 worldPosition = GetWorldPosition(); + const glm::quat worldRotation = GetWorldRotation(); + const glm::mat4 worldMatrix = ComposeMatrix(worldPosition, worldRotation, scale); + SetLocalFromWorldMatrix(worldMatrix); } void Transform::Move(const glm::vec3& move) { @@ -87,7 +151,7 @@ void Transform::Move(const glm::vec3& move) { } void Transform::Move(double x, double y, double z) { - this->Move(glm::vec3(x, y, z)); + Move(glm::vec3(x, y, z)); } void Transform::SetLocalPosition(const glm::vec3& position) { @@ -108,7 +172,10 @@ void Transform::SetLocalRotation(const glm::vec3& rotation) { } void Transform::SetLocalRotation(const glm::quat& rotation) { - m_LocalRotation = rotation; + const float length = glm::length(rotation); + m_LocalRotation = length > 0.000001f + ? glm::normalize(rotation) + : glm::quat{1.0f, 0.0f, 0.0f, 0.0f}; SetRotationDirty(); } @@ -122,132 +189,196 @@ void Transform::SetLocalScale(const glm::vec3& scale) { } void Transform::RemoveChild(Transform* transform) { - std::erase(m_Children, transform); + const auto it = std::find(m_Children.begin(), m_Children.end(), transform); + if (it != m_Children.end()) { + m_Children.erase(it); + } } void Transform::AddChild(Transform* transform) { - // if (transform == this or transform == nullptr) { - // return; - // } - // - // if (transform->m_Parent) { - // transform->m_Parent->RemoveChild(transform); - // } - // - // transform->SetParent(this); - m_Children.push_back(transform); - // - // transform->SetPositionDirty(); -} - -void Transform::SetParent(Transform* parent, bool useWorldPosition) { - if (parent == m_Parent or parent == this or IsChild(parent)) { + if (transform == nullptr || transform == this) { return; } - if (parent == nullptr) { - SetLocalPosition(GetWorldPosition()); - } else { + if (std::find(m_Children.begin(), m_Children.end(), transform) != m_Children.end()) { + return; + } + + m_Children.push_back(transform); +} + +void Transform::SetParent(Transform* parent, bool useWorldPosition) { + if (parent == m_Parent) { + return; + } + + // A transform cannot be parented to itself or to one of its descendants. + if (parent == this || IsChild(parent)) { + return; + } + + if (parent != nullptr) { + GameObject* parentOwner = parent->GetOwner(); + if (parentOwner == nullptr || parentOwner->IsBeingDestroyed() || + (m_Owner != nullptr && m_Owner->GetScene() != parentOwner->GetScene())) { + return; + } + } + + glm::mat4 worldMatrix{1.0f}; + if (useWorldPosition) { + worldMatrix = GetWorldMatrix(); + } + + Transform* previousParent = m_Parent; + if (previousParent != nullptr) { + previousParent->RemoveChild(this); + } + + m_Parent = parent; + if (m_Parent != nullptr) { + m_Parent->AddChild(this); + } + + if (m_Owner != nullptr) { + m_Owner->SetActiveDirty(); + m_Owner->RefreshPhysics(); + } + + try { if (useWorldPosition) { - SetLocalPosition(GetWorldPosition() - parent->GetWorldPosition()); + SetLocalFromWorldMatrix(worldMatrix); + } else { + SetPositionDirty(); + } + } catch (...) { + if (m_Parent != nullptr) { + m_Parent->RemoveChild(this); + } + m_Parent = previousParent; + if (m_Parent != nullptr) { + m_Parent->AddChild(this); } SetPositionDirty(); - } - if (m_Parent) { - m_Parent->RemoveChild(this); - } - m_Parent = parent; - if (m_Parent) { - m_Parent->AddChild(this); + throw; } } - bool Transform::IsChild(Transform* child) const { - return std::ranges::find(m_Children, child) != m_Children.end(); + if (child == nullptr) { + return false; + } + std::unordered_set visited; + return IsChildRecursive(this, child, visited); } const std::vector& Transform::GetChildren() const { - // std::vector validChildren; - // for (auto* child : m_Children) { - // if (child && !child->GetOwner()->IsBeingDestroyed()) { - // validChildren.push_back(child); - // } - // } - // return validChildren; return m_Children; } -GameObject *Transform::GetOwner() const { +GameObject* Transform::GetOwner() const { return m_Owner; } void Transform::SetPositionDirty() { m_PositionDirty = true; + m_RotationDirty = true; + m_ScaleDirty = true; m_MatrixDirty = true; - for (const auto child: m_Children) { - child->SetPositionDirty(); + + for (Transform* child : m_Children) { + if (child != nullptr) { + child->SetPositionDirty(); + } } } void Transform::SetRotationDirty() { + m_PositionDirty = true; m_RotationDirty = true; + m_ScaleDirty = true; m_MatrixDirty = true; - for(Transform* childPtr : m_Children) { - if(not childPtr->m_RotationDirty) { - childPtr->SetRotationDirty(); + for (Transform* child : m_Children) { + if (child != nullptr) { + child->SetRotationDirty(); } } } void Transform::SetScaleDirty() { + m_PositionDirty = true; + m_RotationDirty = true; m_ScaleDirty = true; m_MatrixDirty = true; - for(Transform* childPtr : m_Children) { - if(not childPtr->m_ScaleDirty) { - childPtr->SetScaleDirty(); + for (Transform* child : m_Children) { + if (child != nullptr) { + child->SetScaleDirty(); } } } void Transform::UpdateWorldPosition() { - if (m_Parent) { - m_WorldPosition = m_Parent->GetWorldPosition() + m_LocalPosition; - } else { - m_WorldPosition = m_LocalPosition; - } - m_PositionDirty = false; + UpdateWorldCache(); } void Transform::UpdateWorldRotation() { - if (m_Parent == nullptr) { - m_WorldRotation = m_LocalRotation; - } else { - m_WorldRotation = m_LocalRotation * m_Parent->GetWorldRotation(); - } - - m_RotationDirty = false; + UpdateWorldCache(); } void Transform::UpdateWorldScale() { - if(m_Parent == nullptr) { - m_WorldScale = m_LocalScale; - } else { - m_WorldScale = m_LocalScale * m_Parent->GetWorldScale(); - } - - m_ScaleDirty = false; + UpdateWorldCache(); } void Transform::UpdateWorldMatrix() { - const glm::mat4 trans = glm::translate(glm::mat4(1.0f), GetWorldPosition()); - const glm::mat4 rot = glm::mat4_cast(GetWorldRotation()); - const glm::mat4 scale = glm::scale(glm::mat4(1.0f), GetWorldScale()); - - m_WorldMatrix = trans * rot * scale; - m_MatrixDirty = false; + UpdateWorldCache(); } +void Transform::SetLocalFromWorldMatrix(const glm::mat4& worldMatrix) { + glm::mat4 localMatrix = worldMatrix; + if (m_Parent != nullptr) { + const glm::mat4 parentMatrix = m_Parent->GetWorldMatrix(); + if (std::abs(glm::determinant(parentMatrix)) <= 0.000001f) { + throw std::runtime_error("Cannot set a world transform under a singular parent"); + } + localMatrix = glm::inverse(parentMatrix) * worldMatrix; + } + glm::vec3 position{}; + glm::quat rotation{}; + glm::vec3 scale{}; + if (DecomposeMatrix(localMatrix, position, rotation, scale)) { + m_LocalPosition = position; + m_LocalRotation = rotation; + m_LocalScale = scale; + } else { + throw std::runtime_error("World transform cannot be represented as position/rotation/scale"); + } + + SetPositionDirty(); +} + +void Transform::UpdateWorldCache() { + if (!m_PositionDirty && !m_RotationDirty && !m_ScaleDirty && !m_MatrixDirty) { + return; + } + + const glm::mat4 localMatrix = ComposeMatrix(m_LocalPosition, m_LocalRotation, m_LocalScale); + m_WorldMatrix = m_Parent != nullptr + ? m_Parent->GetWorldMatrix() * localMatrix + : localMatrix; + + if (!DecomposeMatrix(m_WorldMatrix, m_WorldPosition, m_WorldRotation, m_WorldScale)) { + m_WorldPosition = glm::vec3(m_WorldMatrix[3]); + m_WorldRotation = m_Parent != nullptr + ? glm::normalize(m_Parent->GetWorldRotation() * m_LocalRotation) + : m_LocalRotation; + m_WorldScale = m_LocalScale; + } + + m_PositionDirty = false; + m_RotationDirty = false; + m_ScaleDirty = false; + m_MatrixDirty = false; +} diff --git a/destrum/src/Physics/JoltPhysicsWorld.cpp b/destrum/src/Physics/JoltPhysicsWorld.cpp index 83b9c1c..ac82aed 100644 --- a/destrum/src/Physics/JoltPhysicsWorld.cpp +++ b/destrum/src/Physics/JoltPhysicsWorld.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -333,8 +334,24 @@ public: { ShapeRefC shape = CreateJoltShape(desc.shape); - // This first backend ignores centerOffset for now. - // Later, use JPH::OffsetCenterOfMassShapeSettings for offset colliders. + // Jolt's offset shape stores the offset from the shape to the center + // of mass, so negate the component's center offset. This keeps the + // body transform aligned with the owning GameObject. + if (glm::length2(desc.shape.centerOffset) > 0.0000001f) + { + OffsetCenterOfMassShapeSettings offsetSettings( + ToJoltVec3(-desc.shape.centerOffset), + shape.GetPtr()); + ShapeSettings::ShapeResult result = offsetSettings.Create(); + + if (!result.IsValid()) + { + throw std::runtime_error( + std::string("Failed to create Jolt offset shape: ") + std::string(result.GetError())); + } + + shape = result.Get(); + } BodyCreationSettings settings( shape, @@ -391,6 +408,7 @@ public: bodyInterface.RemoveBody(it->second.bodyID); bodyInterface.DestroyBody(it->second.bodyID); + it->second.desc.owner = nullptr; m_Bodies.erase(it); } @@ -416,7 +434,8 @@ public: for (auto& [handleId, record] : m_Bodies) { - if (!record.alive || record.desc.type != RigidbodyType::Kinematic || !record.desc.owner) + if (!record.alive || record.desc.type != RigidbodyType::Kinematic || !record.desc.owner || + record.desc.owner->IsBeingDestroyed() || !record.desc.owner->IsActiveInHierarchy()) { continue; } @@ -437,7 +456,8 @@ public: for (auto& [handleId, record] : m_Bodies) { - if (!record.alive || record.desc.type != RigidbodyType::Dynamic || !record.desc.owner) + if (!record.alive || record.desc.type != RigidbodyType::Dynamic || !record.desc.owner || + record.desc.owner->IsBeingDestroyed() || !record.desc.owner->IsActiveInHierarchy()) { continue; } diff --git a/destrum/src/Physics/PhysicsSceneBridge.cpp b/destrum/src/Physics/PhysicsSceneBridge.cpp index f189645..e5eb7cf 100644 --- a/destrum/src/Physics/PhysicsSceneBridge.cpp +++ b/destrum/src/Physics/PhysicsSceneBridge.cpp @@ -9,7 +9,7 @@ PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr world) void PhysicsSceneBridge::RegisterGameObject(GameObject& object) { if (auto* rb = object.GetComponent()) { - if (!rb->HasPhysicsBody()) { + if (!rb->HasPhysicsBody() || rb->GetPhysicsWorld() != &GetWorld()) { m_World->RegisterRigidbody(*rb); } } @@ -17,12 +17,18 @@ void PhysicsSceneBridge::RegisterGameObject(GameObject& object) { void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) { if (auto* rb = object.GetComponent()) { - if (rb->HasPhysicsBody()) { + if (rb->GetPhysicsWorld() == m_World.get()) { m_World->UnregisterRigidbody(*rb); } } } +void PhysicsSceneBridge::RefreshGameObject(GameObject& object) { + if (auto* rb = object.GetComponent()) { + m_World->RefreshRigidbody(*rb); + } +} + void PhysicsSceneBridge::FixedUpdate(float fixedDt) { m_World->SyncKinematicBodiesToPhysics(); m_World->Step(fixedDt); diff --git a/destrum/src/Physics/PhysicsWorld.cpp b/destrum/src/Physics/PhysicsWorld.cpp index 4915e49..5b25f2e 100644 --- a/destrum/src/Physics/PhysicsWorld.cpp +++ b/destrum/src/Physics/PhysicsWorld.cpp @@ -1,20 +1,59 @@ #include +#include #include -#include #include +#include #include #include -#include + +PhysicsWorld::~PhysicsWorld() { + // Derived backends have already released their body storage by the time + // the base destructor runs. Clear component-side handles without calling + // a virtual backend method from here. + for (Rigidbody* rigidbody : m_RegisteredRigidbodies) { + if (rigidbody != nullptr && rigidbody->GetPhysicsWorld() == this) { + rigidbody->DetachPhysicsBody(); + } + } + m_RegisteredRigidbodies.clear(); +} void PhysicsWorld::RegisterRigidbody(Rigidbody& rigidbody) { + if (rigidbody.IsBeingDestroyed() || !rigidbody.isEnabled()) { + if (rigidbody.GetPhysicsWorld() == this) { + UnregisterRigidbody(rigidbody); + } + return; + } + + if (rigidbody.GetPhysicsWorld() != nullptr) { + if (rigidbody.GetPhysicsWorld() == this && rigidbody.HasPhysicsBody()) { + return; + } + + PhysicsWorld* previousWorld = rigidbody.GetPhysicsWorld(); + if (previousWorld != this) { + previousWorld->UnregisterRigidbody(rigidbody); + } else { + rigidbody.DetachPhysicsBody(); + } + } + GameObject* owner = rigidbody.GetGameObject(); + if (owner == nullptr || owner->IsBeingDestroyed() || !owner->IsActiveInHierarchy()) { + if (rigidbody.GetPhysicsWorld() == this) { + UnregisterRigidbody(rigidbody); + } + return; + } + Transform& transform = owner->GetTransform(); auto* collider = owner->GetComponent(); if (!collider) { - throw std::runtime_error("Rigidbody requires a Collider on the same GameObject for now."); + return; } PhysicsBodyDesc desc{}; @@ -23,6 +62,10 @@ void PhysicsWorld::RegisterRigidbody(Rigidbody& rigidbody) { desc.transform.rotation = transform.GetWorldRotation(); desc.shape = collider->BuildPhysicsShape(); + // Collider dimensions are expressed in world units. Render meshes may + // use a transform scale to normalize imported asset units, so applying + // that scale here would shrink the physics shape a second time. + desc.type = rigidbody.GetType(); desc.mass = rigidbody.GetMass(); desc.useGravity = rigidbody.UsesGravity(); @@ -31,32 +74,52 @@ void PhysicsWorld::RegisterRigidbody(Rigidbody& rigidbody) { desc.material.restitution = rigidbody.GetRestitution(); PhysicsBodyHandle handle = CreateBody(desc); + if (!handle.IsValid()) { + rigidbody.DetachPhysicsBody(); + return; + } + rigidbody.AttachPhysicsBody(this, handle); + m_RegisteredRigidbodies.insert(&rigidbody); +} + +void PhysicsWorld::RefreshRigidbody(Rigidbody& rigidbody) { + const bool hadBody = rigidbody.GetPhysicsWorld() == this && rigidbody.HasPhysicsBody(); + const glm::vec3 previousVelocity = hadBody + ? GetLinearVelocity(rigidbody.GetBody()) + : glm::vec3{0.0f}; + + if (rigidbody.GetPhysicsWorld() == this) { + UnregisterRigidbody(rigidbody); + } + RegisterRigidbody(rigidbody); + + if (hadBody && rigidbody.HasPhysicsBody()) { + SetLinearVelocity(rigidbody.GetBody(), previousVelocity); + } } void PhysicsWorld::UnregisterRigidbody(Rigidbody& rigidbody) { - PhysicsBodyHandle handle = rigidbody.GetBody(); + if (rigidbody.GetPhysicsWorld() != this) { + m_RegisteredRigidbodies.erase(&rigidbody); + return; + } + const PhysicsBodyHandle handle = rigidbody.GetBody(); if (handle.IsValid()) { DestroyBody(handle); } + m_RegisteredRigidbodies.erase(&rigidbody); rigidbody.DetachPhysicsBody(); } void PhysicsWorld::SyncKinematicBodiesToPhysics() { // Backend-independent sync is intentionally not possible here because // PhysicsWorld does not own the list of active rigidbodies. - // - // Use SimplePhysicsWorld as a reference implementation. - // - // If you use Jolt/PhysX/Bullet later, keep a body table in the backend: - // handle -> { GameObject*, Rigidbody* }. } void PhysicsWorld::SyncDynamicBodiesToTransforms() { // Backend-independent sync is intentionally not possible here because // PhysicsWorld does not own the list of active rigidbodies. - // - // Use SimplePhysicsWorld as a reference implementation. } diff --git a/destrum/src/Physics/SimplePhysicsWorld.cpp b/destrum/src/Physics/SimplePhysicsWorld.cpp index 7bbcb43..be408c6 100644 --- a/destrum/src/Physics/SimplePhysicsWorld.cpp +++ b/destrum/src/Physics/SimplePhysicsWorld.cpp @@ -32,6 +32,7 @@ void SimplePhysicsWorld::DestroyBody(PhysicsBodyHandle body) { if (BodyRecord* record = FindBody(body)) { record->alive = false; record->rigidbody = nullptr; + record->desc.owner = nullptr; } } @@ -97,7 +98,9 @@ void SimplePhysicsWorld::Step(float fixedDt) { } for (BodyRecord& body : m_Bodies) { - if (!body.alive) { + if (!body.alive || !body.desc.owner || + body.desc.owner->IsBeingDestroyed() || + !body.desc.owner->IsActiveInHierarchy()) { continue; } @@ -133,7 +136,8 @@ void SimplePhysicsWorld::Step(float fixedDt) { void SimplePhysicsWorld::SyncKinematicBodiesToPhysics() { for (BodyRecord& body : m_Bodies) { - if (!body.alive || body.desc.type != RigidbodyType::Kinematic || !body.desc.owner) { + if (!body.alive || body.desc.type != RigidbodyType::Kinematic || !body.desc.owner || + body.desc.owner->IsBeingDestroyed() || !body.desc.owner->IsActiveInHierarchy()) { continue; } @@ -147,7 +151,8 @@ void SimplePhysicsWorld::SyncKinematicBodiesToPhysics() { void SimplePhysicsWorld::SyncDynamicBodiesToTransforms() { for (BodyRecord& body : m_Bodies) { - if (!body.alive || body.desc.type != RigidbodyType::Dynamic || !body.desc.owner) { + if (!body.alive || body.desc.type != RigidbodyType::Dynamic || !body.desc.owner || + body.desc.owner->IsBeingDestroyed() || !body.desc.owner->IsActiveInHierarchy()) { continue; } @@ -176,13 +181,16 @@ bool SimplePhysicsWorld::Raycast(const glm::vec3& origin, float bestDistance = std::numeric_limits::max(); for (const BodyRecord& body : m_Bodies) { - if (!body.alive || body.desc.shape.type == PhysicsShapeType::None) { + if (!body.alive || body.desc.shape.type == PhysicsShapeType::None || + !body.desc.owner || body.desc.owner->IsBeingDestroyed() || + !body.desc.owner->IsActiveInHierarchy()) { continue; } float distance = 0.0f; const float radius = GetApproxBoundingRadius(body); - const glm::vec3 center = body.currentTransform.position + body.desc.shape.centerOffset; + const glm::vec3 center = body.currentTransform.position + + body.currentTransform.rotation * body.desc.shape.centerOffset; if (RaySphere(origin, dir, center, radius, maxDistance, distance)) { if (distance < bestDistance) { diff --git a/destrum/src/Scene/Scene.cpp b/destrum/src/Scene/Scene.cpp index f81b756..bf41ea7 100644 --- a/destrum/src/Scene/Scene.cpp +++ b/destrum/src/Scene/Scene.cpp @@ -1,163 +1,263 @@ #include #include +#include #include -#include -#include - - -#include -#include - -// #include "ServiceLocator.h" -// #include "Input/InputManager.h" -// #include "Managers/Renderer.h" - unsigned int Scene::m_idCounter = 0; -Scene::Scene(const std::string& name) : m_name(name) { +namespace { + class IterationGuard { + public: + explicit IterationGuard(Scene& scene) : m_Scene(scene) { + m_Scene.BeginIteration(); + } + + ~IterationGuard() { + m_Scene.EndIteration(); + } + + private: + Scene& m_Scene; + }; } -Scene::~Scene() = default; +Scene::Scene(const std::string& name) + : m_name(name), + m_id(++m_idCounter) { +} -// void Scene::Add(std::shared_ptr object) { -// // m_objects.emplace_back(std::move(object)); -// m_pendingAdditions.emplace_back(std::move(object)); -// } +Scene::~Scene() { + Unload(); + RemoveAll(); +} -GameObject* Scene::CreateGameObject(std::string name) -{ - auto obj = std::make_shared(std::move(name)); - - GameObject* rawPtr = obj.get(); - - obj->SetScene(this); - m_pendingAdditions.emplace_back(std::move(obj)); +GameObject* Scene::CreateGameObject(std::string name) { + auto object = std::make_shared(std::move(name)); + GameObject* rawPtr = object.get(); + object->SetScene(this); + m_pendingAdditions.emplace_back(std::move(object)); return rawPtr; } void Scene::Remove(GameObject* object) { - std::erase_if(m_objects, [object](const std::shared_ptr& obj) { - return obj.get() == object; - }); + if (object == nullptr) { + return; + } - std::erase_if(m_pendingAdditions, [object](const std::shared_ptr& obj) { - return obj.get() == object; - }); + const auto containsObject = [object](const auto& objects) { + return std::any_of(objects.begin(), objects.end(), [object](const auto& candidate) { + return candidate != nullptr && candidate.get() == object; + }); + }; + + if (!containsObject(m_objects) && !containsObject(m_pendingAdditions)) { + return; + } + + object->Destroy(); + + if (IsIterating()) { + return; + } + + const auto eraseDestroyed = [](auto& objects) { + for (const auto& candidate : objects) { + if (candidate != nullptr && candidate->IsBeingDestroyed()) { + candidate->SetScene(nullptr); + } + } + + std::erase_if(objects, [](const auto& candidate) { + return candidate == nullptr || candidate->IsBeingDestroyed(); + }); + }; + + // Destroying an object also marks its descendants. Remove all of those + // marked entries together, after destruction has finished. + eraseDestroyed(m_objects); + eraseDestroyed(m_pendingAdditions); } void Scene::RemoveAll() { + for (const auto& object : m_objects) { + if (object != nullptr) { + object->Destroy(); + } + } + for (const auto& object : m_pendingAdditions) { + if (object != nullptr) { + object->Destroy(); + } + } + + if (IsIterating()) { + return; + } + + for (const auto& object : m_objects) { + if (object != nullptr) { + object->SetScene(nullptr); + } + } + for (const auto& object : m_pendingAdditions) { + if (object != nullptr) { + object->SetScene(nullptr); + } + } + m_objects.clear(); m_pendingAdditions.clear(); } void Scene::Load() { - OnSceneLoaded.Invoke(); - if (m_registerBindings) { - m_registerBindings(); - } + LoadBindings(); } -void Scene::Update() { +void Scene::Update(float dt) { CommitPendingAdditions(); + IterationGuard iteration(*this); for (const auto& object : m_objects) { - if (object->IsActiveInHierarchy()) { - object->Update(); + if (object != nullptr && !object->IsBeingDestroyed() && object->IsActiveInHierarchy()) { + object->Update(dt); } } + } void Scene::FixedUpdate(float dt) { - for (const auto& object: m_objects) { - if (object->IsActiveInHierarchy()) { - object->FixedUpdate(); + CommitPendingAdditions(); + IterationGuard iteration(*this); + for (const auto& object : m_objects) { + if (object != nullptr && !object->IsBeingDestroyed() && object->IsActiveInHierarchy()) { + object->FixedUpdate(dt); } } + m_Physics.FixedUpdate(dt); } -void Scene::LateUpdate() { - for (const auto& object: m_objects) { - if (object->IsActiveInHierarchy()) { - object->LateUpdate(); +void Scene::LateUpdate(float dt) { + IterationGuard iteration(*this); + for (const auto& object : m_objects) { + if (object != nullptr && !object->IsBeingDestroyed() && object->IsActiveInHierarchy()) { + object->LateUpdate(dt); } } + } -void Scene::Render(const RenderContext& ctx) const { - for (const auto& object: m_objects) { - if (object->IsActiveInHierarchy()) { +void Scene::Render(const RenderContext& ctx) { + IterationGuard iteration(*this); + for (const auto& object : m_objects) { + if (object != nullptr && !object->IsBeingDestroyed() && object->IsActiveInHierarchy()) { object->Render(ctx); } } + } void Scene::RenderImgui() { - } - void Scene::CleanupDestroyedGameObjects() { - if (m_BeingUnloaded) { - //Scene is gone anyways, kill everything - m_objects.clear(); + if (IsIterating()) { return; } - for (const auto& gameObject: m_objects) { - //First check if a gameobjects components needs to be destroyed - gameObject->CleanupComponents(); + const auto cleanup = [](auto& objects) { + for (const auto& object : objects) { + if (object != nullptr && object->IsBeingDestroyed()) { + object->Destroy(); + } + } + + for (const auto& object : objects) { + if (object != nullptr && !object->IsBeingDestroyed()) { + object->CleanupComponents(); + } + } + + for (const auto& object : objects) { + if (object != nullptr && object->IsBeingDestroyed()) { + object->SetScene(nullptr); + } + } + + std::erase_if(objects, [](const auto& object) { + return object == nullptr || object->IsBeingDestroyed(); + }); + }; + + cleanup(m_objects); + cleanup(m_pendingAdditions); +} + +void Scene::BeginIteration() { + ++m_IterationDepth; +} + +void Scene::EndIteration() { + if (m_IterationDepth == 0) { + return; } - // //Strange for loop since im deleting during looping over it - // for (auto it = m_objects.begin(); it != m_objects.end();) { - // if ((*it)->IsBeingDestroyed()) { - // it = m_objects.erase(it); - // } else { - // ++it; - // } - // } - - std::erase_if(m_objects, [] (const std::shared_ptr& gameObject) { - return gameObject->IsBeingDestroyed(); - }); + --m_IterationDepth; + if (m_IterationDepth == 0) { + CleanupDestroyedGameObjects(); + } } void Scene::Unload() { - if (m_unregisterBindings) { - m_unregisterBindings(); + if (m_BeingUnloaded) { + return; } + + UnloadBindings(); m_BeingUnloaded = true; } void Scene::DestroyGameObjects() { - if (m_BeingUnloaded) { - - for (auto& obj : m_pendingAdditions) { - m_objects.emplace_back(std::move(obj)); + for (const auto& object : m_objects) { + if (object != nullptr) { + object->Destroy(); } - m_pendingAdditions.clear(); - - //Scene is gone anyways, kill everything - for (const auto& gameObject: m_objects) { - gameObject->Destroy(); + } + for (const auto& object : m_pendingAdditions) { + if (object != nullptr) { + object->Destroy(); } - } else { - assert(m_BeingUnloaded && "Scene is being cleared but not unloaded? Weird"); } } void Scene::CommitPendingAdditions() { - if (m_pendingAdditions.empty()) { + if (m_pendingAdditions.empty() || IsIterating()) { return; } - for (auto& obj : m_pendingAdditions) { - m_objects.emplace_back(std::move(obj)); + for (auto& object : m_pendingAdditions) { + if (object != nullptr && !object->IsBeingDestroyed()) { + m_Physics.RegisterGameObject(*object); + m_objects.emplace_back(std::move(object)); + } } - m_pendingAdditions.clear(); -} \ No newline at end of file + std::erase_if(m_pendingAdditions, [](const auto& object) { + return object == nullptr || object->IsBeingDestroyed(); + }); +} + +void Scene::RefreshPhysics() { + for (const auto& object : m_objects) { + if (object != nullptr) { + m_Physics.RefreshGameObject(*object); + } + } + for (const auto& object : m_pendingAdditions) { + if (object != nullptr) { + m_Physics.RefreshGameObject(*object); + } + } +} diff --git a/destrum/src/Scene/SceneManager.cpp b/destrum/src/Scene/SceneManager.cpp index 123ac09..032b268 100644 --- a/destrum/src/Scene/SceneManager.cpp +++ b/destrum/src/Scene/SceneManager.cpp @@ -1,27 +1,57 @@ #include +#include +#include #include #include +#include -void SceneManager::Update() { - m_scenes[m_ActiveSceneIndex]->Update(); +Scene& SceneManager::GetCurrentScene() const { + if (m_scenes.empty()) { + throw std::out_of_range("No scenes are available"); + } + + if (m_ActiveSceneIndex < 0 || m_ActiveSceneIndex >= static_cast(m_scenes.size())) { + throw std::out_of_range("Active scene index is invalid"); + } + + return *m_scenes[static_cast(m_ActiveSceneIndex)]; +} + +void SceneManager::Update(float dt) { + if (m_scenes.empty()) return; + (void)GetCurrentScene(); + const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); + scene->Update(dt); } void SceneManager::FixedUpdate(float dt) { - m_scenes[m_ActiveSceneIndex]->FixedUpdate(dt); + if (m_scenes.empty()) return; + (void)GetCurrentScene(); + const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); + scene->FixedUpdate(dt); } -void SceneManager::LateUpdate() { - m_scenes[m_ActiveSceneIndex]->LateUpdate(); +void SceneManager::LateUpdate(float dt) { + if (m_scenes.empty()) return; + (void)GetCurrentScene(); + const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); + scene->LateUpdate(dt); } void SceneManager::Render(const RenderContext& ctx) { - m_scenes[m_ActiveSceneIndex]->Render(ctx); + if (m_scenes.empty()) return; + (void)GetCurrentScene(); + const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); + scene->Render(ctx); } void SceneManager::RenderImgui() { - m_scenes[m_ActiveSceneIndex]->RenderImgui(); + if (m_scenes.empty()) return; + (void)GetCurrentScene(); + const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); + scene->RenderImgui(); } void SceneManager::HandleGameObjectDestroy() { @@ -43,6 +73,12 @@ void SceneManager::UnloadAllScenes() { } void SceneManager::HandleSceneDestroy() { + const std::shared_ptr activeScene = + m_ActiveSceneIndex >= 0 && + m_ActiveSceneIndex < static_cast(m_scenes.size()) + ? m_scenes[static_cast(m_ActiveSceneIndex)] + : nullptr; + for (auto it = m_scenes.begin(); it != m_scenes.end();) { if ((*it)->IsBeingUnloaded()) { it = m_scenes.erase(it); @@ -50,15 +86,37 @@ void SceneManager::HandleSceneDestroy() { ++it; } } + + if (m_scenes.empty()) { + m_ActiveSceneIndex = 0; + return; + } + + if (activeScene != nullptr) { + const auto activeIt = std::find(m_scenes.begin(), m_scenes.end(), activeScene); + if (activeIt != m_scenes.end()) { + m_ActiveSceneIndex = static_cast(std::distance(m_scenes.begin(), activeIt)); + return; + } + } + + m_ActiveSceneIndex = std::clamp( + m_ActiveSceneIndex, + 0, + static_cast(m_scenes.size()) - 1); } void SceneManager::HandleScene() { - DestroyGameObjects(); HandleGameObjectDestroy(); HandleSceneDestroy(); } void SceneManager::Destroy() { + if (m_scenes.empty()) { + m_ActiveSceneIndex = 0; + return; + } + UnloadAllScenes(); DestroyGameObjects(); HandleGameObjectDestroy(); @@ -71,13 +129,20 @@ void SceneManager::SwitchScene(int index) { if (index < 0 || index >= static_cast(m_scenes.size())) { throw std::out_of_range("Scene index out of range"); } - m_scenes[m_ActiveSceneIndex]->UnloadBindings(); + if (index == m_ActiveSceneIndex) { + return; + } + + m_scenes[static_cast(m_ActiveSceneIndex)]->UnloadBindings(); m_ActiveSceneIndex = index; - m_scenes[m_ActiveSceneIndex]->LoadBindings(); + m_scenes[static_cast(m_ActiveSceneIndex)]->LoadBindings(); } Scene &SceneManager::CreateScene(const std::string &name) { - const auto &scene = std::shared_ptr(new Scene(name)); + const auto scene = std::shared_ptr(new Scene(name)); m_scenes.push_back(scene); + if (m_scenes.size() == 1) { + m_ActiveSceneIndex = 0; + } return *scene; } diff --git a/destrum/src/Serialization/ComponentRegistry.cpp b/destrum/src/Serialization/ComponentRegistry.cpp index dee115a..7eb5feb 100644 --- a/destrum/src/Serialization/ComponentRegistry.cpp +++ b/destrum/src/Serialization/ComponentRegistry.cpp @@ -11,6 +11,7 @@ #include #include +#include #include void RegisterEngineComponents() @@ -22,21 +23,26 @@ void RegisterEngineComponents() registered = true; + ComponentFactory::Register("MeshRendererComponent", [](GameObject& owner) { + return owner.AddComponent(); + }); + // Keep the old serialized spelling readable while writing the canonical + // component name returned by MeshRendererComponent. ComponentFactory::Register("MeshRenderer", [](GameObject& owner) { return owner.AddComponent(); }); - // ComponentFactory::Register("Rotator", [](GameObject& owner) { - // return owner.AddComponent(); - // }); + ComponentFactory::Register("Rotator", [](GameObject& owner) { + return owner.AddComponent(); + }); - // ComponentFactory::Register("Spinner", [](GameObject& owner) { - // return owner.AddComponent(); - // }); + ComponentFactory::Register("Spinner", [](GameObject& owner) { + return owner.AddComponent(); + }); - // ComponentFactory::Register("OrbitAndSpin", [](GameObject& owner) { - // return owner.AddComponent(); - // }); + ComponentFactory::Register("OrbitAndSpin", [](GameObject& owner) { + return owner.AddComponent(); + }); ComponentFactory::Register("Animator", [](GameObject& owner) { return owner.AddComponent(); @@ -45,6 +51,9 @@ void RegisterEngineComponents() ComponentFactory::Register("Rigidbody", [](GameObject& owner) { return owner.AddComponent(); }); + ComponentFactory::Register("RigidBody", [](GameObject& owner) { + return owner.AddComponent(); + }); ComponentFactory::Register("BoxCollider", [](GameObject& owner) { return owner.AddComponent(); @@ -53,4 +62,8 @@ void RegisterEngineComponents() ComponentFactory::Register("SphereCollider", [](GameObject& owner) { return owner.AddComponent(); }); -} \ No newline at end of file + + ComponentFactory::Register("CapsuleCollider", [](GameObject& owner) { + return owner.AddComponent(); + }); +} diff --git a/destrum/src/Serialization/SceneSerializer.cpp b/destrum/src/Serialization/SceneSerializer.cpp index 6d4a5a8..ccf8d5e 100644 --- a/destrum/src/Serialization/SceneSerializer.cpp +++ b/destrum/src/Serialization/SceneSerializer.cpp @@ -1,75 +1,278 @@ #include +#include +#include #include #include +#include #include +#include #include #include #include -#include #include +#include #include +#include +#include +#include #include +#include using json = nlohmann::json; +namespace { + [[nodiscard]] bool IsFiniteNumber(const json& value) { + if (!value.is_number()) { + return false; + } + + return std::isfinite(value.get()); + } + + [[nodiscard]] bool IsFiniteArray(const json& value, std::size_t size) { + if (!value.is_array() || value.size() != size) { + return false; + } + + for (const auto& element : value) { + if (!IsFiniteNumber(element)) { + return false; + } + } + + return true; + } + + [[nodiscard]] bool IsFiniteJson(const json& value) { + if (value.is_number()) { + return IsFiniteNumber(value); + } + if (value.is_array()) { + for (const auto& element : value) { + if (!IsFiniteJson(element)) { + return false; + } + } + return true; + } + if (value.is_object()) { + for (auto it = value.begin(); it != value.end(); ++it) { + if (!IsFiniteJson(it.value())) { + return false; + } + } + return true; + } + return true; + } + + [[nodiscard]] bool ValidateTransform(const json& objectJson, int version) { + if (!objectJson.contains("transform")) { + return true; + } + + const auto& transform = objectJson.at("transform"); + if (!transform.is_object()) { + return false; + } + + // Version 1 wrote an empty placeholder transform object. + if (version <= 1 && transform.empty()) { + return true; + } + + if (!transform.contains("position") || !transform.contains("rotation") || + !transform.contains("scale")) { + return false; + } + + if (!IsFiniteArray(transform.at("position"), 3) || + !IsFiniteArray(transform.at("scale"), 3)) { + return false; + } + + const auto& rotation = transform.at("rotation"); + return IsFiniteArray(rotation, 3) || IsFiniteArray(rotation, 4); + } + + [[nodiscard]] bool ValidateSceneJson(const json& root) { + if (!root.is_object() || !root.contains("objects") || !root.at("objects").is_array()) { + return false; + } + + if (root.contains("name") && !root.at("name").is_string()) { + return false; + } + + int version = 1; + if (root.contains("version")) { + if (!root.at("version").is_number_integer()) { + return false; + } + version = root.at("version").get(); + if (version < 1 || version > 2) { + return false; + } + } + + std::unordered_set ids; + std::unordered_map parents; + + for (const auto& objectJson : root.at("objects")) { + if (!objectJson.is_object() || !objectJson.contains("id")) { + return false; + } + + const ObjectId id = objectJson.at("id").get(); + if (id == InvalidObjectId || + id >= std::numeric_limits::max() - 1 || + !ids.insert(id).second) { + return false; + } + + if (objectJson.contains("name") && !objectJson.at("name").is_string()) { + return false; + } + if (objectJson.contains("active") && !objectJson.at("active").is_boolean()) { + return false; + } + if (!ValidateTransform(objectJson, version)) { + return false; + } + + const ObjectId parent = objectJson.value("parent", InvalidObjectId); + if (parent == id) { + return false; + } + parents.emplace(id, parent); + + if (objectJson.contains("components")) { + const auto& components = objectJson.at("components"); + if (!components.is_array()) { + return false; + } + + for (const auto& componentJson : components) { + if (!componentJson.is_object() || !componentJson.contains("type") || + !componentJson.at("type").is_string()) { + return false; + } + + if (componentJson.contains("enabled") && + !componentJson.at("enabled").is_boolean()) { + return false; + } + + if (componentJson.contains("data") && + (!componentJson.at("data").is_object() || + !IsFiniteJson(componentJson.at("data")))) { + return false; + } + + if (!ComponentFactory::IsRegistered(componentJson.at("type").get())) { + return false; + } + } + } + } + + for (const auto& [id, parent] : parents) { + if (parent != InvalidObjectId && !ids.contains(parent)) { + return false; + } + + std::unordered_set visited; + ObjectId current = id; + while (current != InvalidObjectId) { + if (!visited.insert(current).second) { + return false; + } + + const auto parentIt = parents.find(current); + if (parentIt == parents.end()) { + return false; + } + current = parentIt->second; + } + } + + return true; + } +} + bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) { - scene.CommitPendingAdditions(); + if (scene.IsIterating()) { + std::cerr << "Cannot save a scene during an update or render phase: " + << scene.GetName() << '\n'; + return false; + } + + RegisterEngineComponents(); + try { + scene.CommitPendingAdditions(); + } catch (const std::exception& exception) { + std::cerr << "Failed to commit scene additions before save: " + << exception.what() << '\n'; + return false; + } json root; - - root["version"] = 1; + root["version"] = 2; root["name"] = scene.GetName(); root["objects"] = json::array(); for (const auto& objectPtr : scene.GetObjects()) { - if (!objectPtr) { + if (!objectPtr || objectPtr->IsBeingDestroyed()) { continue; } const GameObject& object = *objectPtr; + if (object.GetComponent() != nullptr && + object.GetComponent() == nullptr) { + std::cerr << "Cannot save object with a Rigidbody but no Collider: " + << object.GetName() << '\n'; + return false; + } + const Transform& transform = object.GetTransform(); + const glm::vec3& position = transform.GetLocalPosition(); + const glm::quat& rotation = transform.GetLocalRotation(); + const glm::vec3& scale = transform.GetLocalScale(); json objectJson; objectJson["id"] = object.GetId(); objectJson["name"] = object.GetName(); objectJson["active"] = object.IsActive(); - - // Transform should be saved separately because in your engine it is not a component. - // Replace this with your actual Transform getters. objectJson["transform"] = { - // Example: - // { "position", { object.GetTransform().GetLocalPosition().x, - // object.GetTransform().GetLocalPosition().y, - // object.GetTransform().GetLocalPosition().z } }, - // { "rotation", { ... } }, - // { "scale", { ... } } + {"position", {position.x, position.y, position.z}}, + {"rotation", {rotation.x, rotation.y, rotation.z, rotation.w}}, + {"scale", {scale.x, scale.y, scale.z}} }; - const Transform* parent = object.GetTransform().GetParent(); - objectJson["parent"] = parent + const Transform* parent = transform.GetParent(); + objectJson["parent"] = parent != nullptr && parent->GetOwner() != nullptr && + parent->GetOwner()->GetScene() == &scene && + !parent->GetOwner()->IsBeingDestroyed() ? parent->GetOwner()->GetId() : InvalidObjectId; objectJson["components"] = json::array(); - for (const auto& componentPtr : object.GetComponents()) { - if (!componentPtr) { + if (!componentPtr || componentPtr->IsBeingDestroyed()) { continue; } const Component& component = *componentPtr; - - json componentJson; - componentJson["type"] = component.GetTypeName(); - componentJson["enabled"] = component.isEnabled(); - componentJson["data"] = component.Serialize(); - - objectJson["components"].push_back(componentJson); + objectJson["components"].push_back({ + {"type", component.GetTypeName()}, + {"enabled", component.isEnabled()}, + {"data", component.Serialize()} + }); } - root["objects"].push_back(objectJson); + root["objects"].push_back(std::move(objectJson)); } std::ofstream file(path); @@ -79,10 +282,23 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) { } file << root.dump(4); + if (!file.good()) { + std::cerr << "Failed to write scene file: " << path << '\n'; + return false; + } + return true; } bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { + if (scene.IsIterating()) { + std::cerr << "Cannot load a scene during an update or render phase: " + << scene.GetName() << '\n'; + return false; + } + + RegisterEngineComponents(); + std::ifstream file(path); if (!file.is_open()) { std::cerr << "Failed to open scene file for reading: " << path << '\n'; @@ -90,134 +306,166 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { } json root; - try { file >> root; - } catch (const std::exception& e) { - std::cerr << "Failed to parse scene file: " << e.what() << '\n'; + if (!ValidateSceneJson(root)) { + std::cerr << "Invalid scene data: " << path << '\n'; + return false; + } + } catch (const std::exception& exception) { + std::cerr << "Failed to parse scene file: " << exception.what() << '\n'; return false; } - scene.RemoveAll(); + if (scene.IsBeingUnloaded()) { + std::cerr << "Cannot load a scene that is being unloaded: " << scene.GetName() << '\n'; + return false; + } + // Build the replacement separately. The current scene is not touched + // until all objects, transforms, and components have loaded successfully. + Scene stagingScene(scene.GetName()); std::unordered_map idMap; std::vector objectJsonList; + std::vector registeredObjects; try { - const auto& objectsJson = root.at("objects"); - - // Pass 1: - // Create all GameObjects first. - for (const auto& objectJson : objectsJson) { - const auto id = objectJson.at("id").get(); + for (const auto& objectJson : root.at("objects")) { + const ObjectId id = objectJson.at("id").get(); const std::string name = objectJson.value("name", "GameObject"); const bool active = objectJson.value("active", true); - GameObject* object = scene.CreateGameObject(name); + GameObject* object = stagingScene.CreateGameObject(name); object->SetIdForDeserialization(id); object->SetActive(active); - idMap[id] = object; + idMap.emplace(id, object); objectJsonList.push_back(&objectJson); } - scene.CommitPendingAdditions(); + stagingScene.CommitPendingAdditions(); - // Pass 2: - // Restore transforms and parent-child hierarchy. for (const json* objectJson : objectJsonList) { - const auto id = objectJson->at("id").get(); - + const ObjectId id = objectJson->at("id").get(); GameObject* object = idMap.at(id); - // Replace this with your actual Transform deserialization. - if (objectJson->contains("transform")) { - const json& transformJson = objectJson->at("transform"); + if (objectJson->contains("transform") && + !objectJson->at("transform").empty()) { + const auto& transformJson = objectJson->at("transform"); + const auto& position = transformJson.at("position"); + const auto& rotation = transformJson.at("rotation"); + const auto& scale = transformJson.at("scale"); - auto position = transformJson.at("position"); object->GetTransform().SetLocalPosition({ - position[0].get(), - position[1].get(), - position[2].get() + position.at(0).get(), + position.at(1).get(), + position.at(2).get() }); - auto rotation = transformJson.at("rotation"); - object->GetTransform().SetLocalRotation({ - rotation[0].get(), - rotation[1].get(), - rotation[2].get() - }); + if (rotation.size() == 4) { + object->GetTransform().SetLocalRotation({ + rotation.at(3).get(), + rotation.at(0).get(), + rotation.at(1).get(), + rotation.at(2).get() + }); + } else { + // Version 1 scene files used three Euler angles in + // degrees. Continue reading that format safely. + object->GetTransform().SetLocalRotation({ + rotation.at(0).get(), + rotation.at(1).get(), + rotation.at(2).get() + }); + } - auto scale = transformJson.at("scale"); object->GetTransform().SetLocalScale({ - scale[0].get(), - scale[1].get(), - scale[2].get() + scale.at(0).get(), + scale.at(1).get(), + scale.at(2).get() }); } const ObjectId parentId = objectJson->value("parent", InvalidObjectId); - if (parentId != InvalidObjectId) { - auto parentIt = idMap.find(parentId); - - if (parentIt != idMap.end()) { - object->GetTransform().SetParent(&parentIt->second->GetTransform()); - } else { - std::cerr << "Parent not found while loading object " << id << '\n'; - } + object->GetTransform().SetParent(&idMap.at(parentId)->GetTransform(), false); } } - // Pass 3: - // Create and deserialize components. for (const json* objectJson : objectJsonList) { - const auto id = objectJson->at("id").get(); - GameObject* object = idMap.at(id); - + GameObject* object = idMap.at(objectJson->at("id").get()); if (!objectJson->contains("components")) { continue; } for (const auto& componentJson : objectJson->at("components")) { const std::string type = componentJson.at("type").get(); - Component* component = ComponentFactory::Create(type, *object); - - if (!component) { - std::cerr << "Unknown component type while loading scene: " << type << '\n'; - continue; + if (component == nullptr) { + throw std::runtime_error("Unknown component type: " + type); } component->SetEnabled(componentJson.value("enabled", true)); - if (componentJson.contains("data")) { component->Deserialize(componentJson.at("data")); } } } - // Pass 4: - // Resolve object references now that all objects and components exist. - for (const auto& objectPtr : scene.GetObjects()) { + for (const auto& objectPtr : stagingScene.GetObjects()) { if (!objectPtr) { continue; } for (const auto& componentPtr : objectPtr->GetComponents()) { - if (!componentPtr) { - continue; + if (componentPtr) { + componentPtr->ResolveReferences(idMap); } - - componentPtr->ResolveReferences(idMap); } } - } catch (const std::exception& e) { - std::cerr << "Failed to load scene: " << e.what() << '\n'; - scene.RemoveAll(); + // Register the replacement bodies in the live physics world before + // touching the current scene. If any body fails, the old scene and + // its physics state can remain intact. + for (const auto& objectPtr : stagingScene.GetObjects()) { + if (objectPtr) { + if (objectPtr->GetComponent() != nullptr && + objectPtr->GetComponent() == nullptr) { + throw std::runtime_error( + "Rigidbody requires a collider on object " + objectPtr->GetName()); + } + scene.GetPhysics().RegisterGameObject(*objectPtr); + registeredObjects.push_back(objectPtr.get()); + } + } + } catch (const std::exception& exception) { + std::cerr << "Failed to load scene: " << exception.what() << '\n'; + for (GameObject* object : registeredObjects) { + if (object != nullptr) { + scene.GetPhysics().UnregisterGameObject(*object); + } + } + stagingScene.RemoveAll(); return false; } + scene.RemoveAll(); + scene.m_objects = std::move(stagingScene.m_objects); + scene.m_pendingAdditions = std::move(stagingScene.m_pendingAdditions); + if (root.contains("name")) { + scene.m_name = root.at("name").get(); + } + + for (const auto& object : scene.m_objects) { + if (object) { + object->SetScene(&scene); + } + } + for (const auto& object : scene.m_pendingAdditions) { + if (object) { + object->SetScene(&scene); + } + } + return true; -} \ No newline at end of file +} diff --git a/lightkeeper/CMakeLists.txt b/lightkeeper/CMakeLists.txt index 0023acd..bec0b79 100644 --- a/lightkeeper/CMakeLists.txt +++ b/lightkeeper/CMakeLists.txt @@ -35,28 +35,52 @@ target_link_libraries(lightkeeper PRIVATE destrum::destrum) set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src") set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime") -set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_CURRENT_BINARY_DIR}/assets/game") +set(OUTPUT_GAME_ASSETS_DIR "$/assets/game") + +if (WIN32) + set(GAME_ASSET_INSTALL_COMMANDS + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${ASSETS_RUNTIME_DIR}" "${OUTPUT_GAME_ASSETS_DIR}") +else() + set(GAME_ASSET_INSTALL_COMMANDS + COMMAND ${CMAKE_COMMAND} -E create_symlink + "${ASSETS_RUNTIME_DIR}" "${OUTPUT_GAME_ASSETS_DIR}") +endif() add_custom_command(TARGET lightkeeper POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/assets" + COMMAND ${CMAKE_COMMAND} -E make_directory "${OUTPUT_GAME_ASSETS_DIR}/.." - COMMAND ${CMAKE_COMMAND} -E rm -rf "${CMAKE_CURRENT_BINARY_DIR}/assets/game" - COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/assets/game" + COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_GAME_ASSETS_DIR}" + ${GAME_ASSET_INSTALL_COMMANDS} VERBATIM ) +file(GLOB_RECURSE GAME_ASSET_SOURCES CONFIGURE_DEPENDS + "${ASSETS_SRC_DIR}/*") + add_custom_target(_internal_clean_game_assets - COMMAND TheChef - --input "${ASSETS_SRC_DIR}" - --output "${ASSETS_RUNTIME_DIR}" - --clean + COMMAND TheChef + --input "${ASSETS_SRC_DIR}" + --output "${ASSETS_RUNTIME_DIR}" + --clean + DEPENDS TheChef ) -add_custom_target(_internal_cook_game_assets ALL - COMMAND TheChef - --input "${ASSETS_SRC_DIR}" - --output "${ASSETS_RUNTIME_DIR}" - DEPENDS TheChef +add_custom_target(_internal_cook_game_assets + COMMAND TheChef + --input "${ASSETS_SRC_DIR}" + --output "${ASSETS_RUNTIME_DIR}" + --clean + COMMAND TheChef + --input "${ASSETS_SRC_DIR}" + --output "${ASSETS_RUNTIME_DIR}" + DEPENDS TheChef ${GAME_ASSET_SOURCES} + VERBATIM ) -destrum_cook_engine_assets(lightkeeper "${CMAKE_CURRENT_BINARY_DIR}") \ No newline at end of file +add_dependencies(lightkeeper _internal_cook_game_assets) + +destrum_cook_engine_assets(lightkeeper "${CMAKE_CURRENT_BINARY_DIR}") + +install(TARGETS lightkeeper RUNTIME DESTINATION bin) +install(DIRECTORY "${ASSETS_RUNTIME_DIR}/" DESTINATION "bin/assets/game") diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index 9f5dbb8..6932074 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -30,6 +30,12 @@ LightKeeper::LightKeeper() : App() LightKeeper::~LightKeeper() { + if (!cleanedUp) { + try { + cleanup(); + } catch (...) { + } + } } void LightKeeper::customInit() @@ -442,8 +448,11 @@ void LightKeeper::customInit() void LightKeeper::customUpdate(float dt) { + // LightKeeper owns a camera that hides App::camera; update this instance + // after SDL events have been consumed. camera.Update(dt); - SceneManager::GetInstance().Update(); + SceneManager::GetInstance().Update(dt); + SceneManager::GetInstance().LateUpdate(dt); if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_1)) { @@ -462,17 +471,22 @@ void LightKeeper::customUpdate(float dt) meshRenderComp->SetMaterialID(sphereMaterial); meshRenderComp->SetMeshID(sphereMesh); - sphere->GetTransform().SetWorldPosition(0, 100, 0); + sphere->GetTransform().SetWorldPosition((std::rand() % 2) - 0.5f, 100, (std::rand() % 2) - 0.5f); sphere->GetTransform().SetWorldScale(glm::vec3{0.015f}); - SceneManager::GetInstance().GetCurrentScene().GetPhysics().RegisterGameObject(*sphere); + SceneManager::GetInstance().GetCurrentScene().GetPhysics().RefreshGameObject(*sphere); } ImGui::End(); } void LightKeeper::customDraw() { + const auto cmd = gfxDevice.beginFrame(); + if (cmd == VK_NULL_HANDLE) { + return; + } + renderer.beginDrawing(gfxDevice); const RenderContext ctx{ @@ -490,7 +504,6 @@ void LightKeeper::customDraw() SceneManager::GetInstance().Render(ctx); renderer.endDrawing(); - const auto cmd = gfxDevice.beginFrame(); const auto& drawImage = renderer.getDrawImage(); renderer.draw( @@ -508,14 +521,17 @@ void LightKeeper::customDraw() void LightKeeper::customCleanup() { - auto device = gfxDevice.getDevice().device; + // auto device = gfxDevice.getDevice().device; - vkDeviceWaitIdle(device); + // vkDeviceWaitIdle(device); + + gfxDevice.waitIdle(); SceneManager::GetInstance().Destroy(); if (skyboxCubemap) { + skyboxCubemap->cleanup(gfxDevice); skyboxCubemap.reset(); } diff --git a/lightkeeper/src/main.cpp b/lightkeeper/src/main.cpp index 65143d0..5703bd7 100644 --- a/lightkeeper/src/main.cpp +++ b/lightkeeper/src/main.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include "Lightkeeper.h" @@ -9,8 +11,13 @@ int main(int argc, char* argv[]) { SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", SDL_GetError(), nullptr); return 1; } - { - std::filesystem::path exeDir = SDL_GetBasePath(); + try { + char* basePath = SDL_GetBasePath(); + if (basePath == nullptr) { + throw std::runtime_error(std::string{"SDL_GetBasePath failed: "} + SDL_GetError()); + } + const std::filesystem::path exeDir = basePath; + SDL_free(basePath); spdlog::set_level(spdlog::level::debug); @@ -24,6 +31,10 @@ int main(int argc, char* argv[]) { }); app.run(); app.cleanup(); + } catch (const std::exception& exception) { + spdlog::critical("Lightkeeper terminated with an error: {}", exception.what()); + SDL_Quit(); + return 1; } SDL_Quit(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..20250ef --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(destrum_tests + destrum_tests.cpp +) + +set_target_properties(destrum_tests PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) + +target_compile_definitions(destrum_tests PRIVATE SDL_MAIN_HANDLED) +target_link_libraries(destrum_tests PRIVATE destrum::destrum) + +destrum_enable_warnings(destrum_tests) + +add_test(NAME destrum_tests COMMAND destrum_tests) diff --git a/tests/destrum_tests.cpp b/tests/destrum_tests.cpp new file mode 100644 index 0000000..0a59ff8 --- /dev/null +++ b/tests/destrum_tests.cpp @@ -0,0 +1,363 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + class TestComponent final : public Component { + public: + explicit TestComponent(GameObject& owner) : Component(owner, "TestComponent") {} + + void Update() override {} + std::string GetTypeName() const override { return "TestComponent"; } + }; + + class RemovingListener final : public EventListener { + public: + explicit RemovingListener(std::unique_ptr* victim) + : victim(victim) {} + + void OnEvent(int) { + if (victim != nullptr) { + victim->reset(); + victim = nullptr; + } + ++calls; + } + + int calls{0}; + + private: + std::unique_ptr* victim; + }; + + void check(bool condition, const char* message) + { + if (!condition) { + std::cerr << "FAILED: " << message << '\n'; + std::exit(EXIT_FAILURE); + } + } + + bool close(float a, float b) + { + return std::abs(a - b) < 0.001f; + } + + void testTransforms() + { + GameObject parent{"parent"}; + GameObject child{"child"}; + + parent.GetTransform().SetLocalPosition({10.f, 2.f, -4.f}); + parent.GetTransform().SetLocalRotation({0.f, 0.f, 90.f}); + parent.GetTransform().SetLocalScale({2.f, 2.f, 2.f}); + child.GetTransform().SetLocalPosition({1.f, 0.f, 0.f}); + + child.GetTransform().SetParent(&parent.GetTransform(), true); + const auto worldPosition = child.GetTransform().GetWorldPosition(); + check(close(worldPosition.x, 1.f) && close(worldPosition.y, 0.f), + "reparenting must preserve world position"); + + child.GetTransform().SetWorldPosition({20.f, 10.f, 0.f}); + const auto movedPosition = child.GetTransform().GetWorldPosition(); + check(close(movedPosition.x, 20.f) && close(movedPosition.y, 10.f), + "world position must account for parent rotation and scale"); + + parent.Destroy(); + check(parent.IsBeingDestroyed() && child.IsBeingDestroyed(), + "destroying a parent must mark descendants"); + } + + void testSceneRoundTrip() + { + Scene& scene = SceneManager::GetInstance().CreateScene("round-trip"); + GameObject* parent = scene.CreateGameObject("parent"); + GameObject* child = scene.CreateGameObject("child"); + child->GetTransform().SetLocalPosition({1.f, 2.f, 3.f}); + child->GetTransform().SetParent(&parent->GetTransform(), false); + scene.CommitPendingAdditions(); + + const auto path = std::filesystem::temp_directory_path() / "destrum_scene_test.json"; + check(SceneSerializer::Save(scene, path), "scene save must succeed"); + check(SceneSerializer::Load(scene, path), "scene load must succeed"); + std::filesystem::remove(path); + + check(scene.GetObjects().size() == 2, "scene round-trip must preserve objects"); + const auto* loadedChild = scene.GetObjects().at(1).get(); + check(loadedChild->GetTransform().GetParent() != nullptr, + "scene round-trip must preserve hierarchy"); + check(close(loadedChild->GetTransform().GetLocalPosition().x, 1.f), + "scene round-trip must preserve local transforms"); + + SceneManager::GetInstance().Destroy(); + } + + void testEventMutation() + { + Event event; + int calls = 0; + event.AddListener([&](int) { ++calls; }); + event.AddListener([&](int) { + ++calls; + event.AddListener([&](int) { ++calls; }); + }); + + event.Invoke(1); + check(calls == 2, "event mutation must not invalidate the current invocation"); + event.Invoke(1); + check(calls == 5, "new event listeners must run on subsequent invocations"); + } + + void testEventRemoval() + { + Event event; + std::unique_ptr victim; + RemovingListener remover{&victim}; + victim = std::make_unique(nullptr); + + event.AddListener(&remover, &RemovingListener::OnEvent); + event.AddListener(victim.get(), &RemovingListener::OnEvent); + event.Invoke(1); + + check(remover.calls == 1, "event remover must run once"); + check(victim == nullptr, "event listener must be removable during invocation"); + } + + void testComponentRemoval() + { + Scene& scene = SceneManager::GetInstance().CreateScene("component-removal"); + GameObject* object = scene.CreateGameObject("object"); + object->AddComponent(); + scene.CommitPendingAdditions(); + + check(object->GetComponent() != nullptr, + "test component must be discoverable before removal"); + check(object->DestroyComponent() != nullptr, + "destroying a component must find it"); + scene.CleanupDestroyedGameObjects(); + check(object->GetComponent() == nullptr, + "destroyed components must be removed at the scene boundary"); + + SceneManager::GetInstance().Destroy(); + } + + void testAssetPathValidation() + { + const auto root = std::filesystem::temp_directory_path() / "destrum_asset_test"; + std::filesystem::create_directories(root / "assets" / "engine"); + std::filesystem::create_directories(root / "assets" / "game"); + std::ofstream{root / "assets" / "engine" / "asset.txt"} << "asset"; + std::ofstream{root / "assets" / "engine" / "cooked.asset"} << "cooked"; + std::ofstream{root / "assets" / "engine" / "manifest.json"} + << R"({"version":1,"assets":[{"src":"asset.txt","out":"cooked.asset","type":"raw","mtime_epoch_ns":-5020137004799419216,"size_bytes":355972}]})"; + + auto& assetFs = AssetFS::GetInstance(); + assetFs.Init(root); + check(std::filesystem::exists(assetFs.GetFullPath("engine://asset.txt")), + "valid virtual asset paths must resolve"); + check(assetFs.GetCookedPathForFile("engine://asset.txt").filename() == "cooked.asset", + "manifest output paths must resolve to cooked assets"); + + bool rejected = false; + try { + (void)assetFs.GetFullPath("engine://../outside.txt"); + } catch (const std::exception&) { + rejected = true; + } + check(rejected, "asset traversal must be rejected"); + + const auto outside = root / "outside"; + const auto link = root / "assets" / "engine" / "link"; + std::filesystem::create_directories(outside); + std::error_code symlinkError; + std::filesystem::create_directory_symlink(outside, link, symlinkError); + if (!symlinkError) { + bool symlinkEscapeRejected = false; + try { + (void)assetFs.GetFullPath("engine://link/missing.txt"); + } catch (const std::exception&) { + symlinkEscapeRejected = true; + } + check(symlinkEscapeRejected, + "symlink traversal must be rejected for missing targets"); + } + + std::filesystem::remove_all(root); + assetFs.Reset(); + } + + void testLegacySceneLoad() + { + Scene& scene = SceneManager::GetInstance().CreateScene("legacy"); + const auto path = std::filesystem::temp_directory_path() / "destrum_legacy_scene.json"; + std::ofstream{path} + << R"({"version":1,"name":"legacy","objects":[{"id":9001,"name":"object","active":true,"transform":{},"parent":0,"components":[]}]})"; + + check(SceneSerializer::Load(scene, path), + "version 1 placeholder transforms must remain loadable"); + check(scene.GetObjects().size() == 1, + "legacy scene load must create its object"); + + std::filesystem::remove(path); + SceneManager::GetInstance().Destroy(); + } + + void testSceneLoadRollback() + { + Scene& scene = SceneManager::GetInstance().CreateScene("rollback"); + scene.CreateGameObject("original"); + scene.CommitPendingAdditions(); + + const auto path = std::filesystem::temp_directory_path() / "destrum_bad_scene.json"; + std::ofstream{path} + << R"({"version":2,"name":"bad","objects":[{"id":9101,"name":"bad","transform":{"position":[0,0,0],"rotation":[0,0,0,1],"scale":[1,1,1]},"parent":0,"components":[{"type":"Rigidbody","enabled":true,"data":{}}]}]})"; + + check(!SceneSerializer::Load(scene, path), + "scene loading must reject a rigidbody without a collider"); + check(scene.GetObjects().size() == 1 && scene.GetObjects().at(0)->GetName() == "original", + "failed scene loading must preserve the live scene"); + + std::filesystem::remove(path); + SceneManager::GetInstance().Destroy(); + } + + void testPhysicsWorldUnits() + { + SimplePhysicsWorld world{glm::vec3{0.0f}}; + GameObject object{"scaled-physics"}; + object.GetTransform().SetLocalScale(glm::vec3{0.5f}); + object.AddComponent(1.0f); + Rigidbody* rigidbody = object.AddComponent(); + world.RegisterRigidbody(*rigidbody); + + PhysicsRaycastHit hit; + check(world.Raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f, hit), + "scaled physics bodies must remain raycastable"); + check(close(hit.distance, 1.0f), + "physics collider dimensions must remain in world units"); + + object.GetTransform().SetLocalPosition({10.0f, 0.0f, 0.0f}); + world.RefreshRigidbody(*rigidbody); + check(world.Raycast({8.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f, hit), + "refreshing a physics body must use the current transform position"); + check(close(hit.distance, 1.0f), + "refreshed physics bodies must remain aligned with rendered transforms"); + } + + void testJoltSphereCollisions() + { + JoltPhysicsWorld world{{ + .workerThreadCount = 1, + .gravity = {0.0f, -9.81f, 0.0f} + }}; + + GameObject floor{"floor"}; + floor.GetTransform().SetLocalPosition({0.0f, -1.0f, 0.0f}); + floor.AddComponent(glm::vec3{10.0f, 0.5f, 10.0f}); + auto* floorBody = floor.AddComponent(); + floorBody->SetType(RigidbodyType::Static); + world.RegisterRigidbody(*floorBody); + + GameObject first{"first"}; + first.AddComponent(1.5f); + auto* firstBody = first.AddComponent(); + world.RegisterRigidbody(*firstBody); + first.GetTransform().SetLocalPosition({0.0f, 100.0f, 0.0f}); + first.GetTransform().SetLocalScale(glm::vec3{0.015f}); + world.RefreshRigidbody(*firstBody); + + GameObject second{"second"}; + second.AddComponent(1.5f); + auto* secondBody = second.AddComponent(); + world.RegisterRigidbody(*secondBody); + second.GetTransform().SetLocalPosition({0.0f, 100.0f, 0.0f}); + second.GetTransform().SetLocalScale(glm::vec3{0.015f}); + world.RefreshRigidbody(*secondBody); + + for (int step = 0; step < 240; ++step) { + world.Step(1.0f / 60.0f); + world.SyncDynamicBodiesToTransforms(); + } + + check(first.GetTransform().GetWorldPosition().y > 0.8f && + second.GetTransform().GetWorldPosition().y > 0.8f, + "Jolt spheres must collide with the ground plane"); + check(glm::distance(first.GetTransform().GetWorldPosition(), + second.GetTransform().GetWorldPosition()) > 2.5f, + "Jolt spheres must collide with one another instead of occupying the same point"); + } + + void testSceneJoltSphereCollisions() + { + Scene& scene = SceneManager::GetInstance().CreateScene("scene-physics"); + + GameObject* floor = scene.CreateGameObject("floor"); + floor->GetTransform().SetLocalPosition({0.0f, -1.0f, 0.0f}); + floor->AddComponent(glm::vec3{10.0f, 0.5f, 10.0f}); + auto* floorBody = floor->AddComponent(); + floorBody->SetType(RigidbodyType::Static); + + GameObject* first = scene.CreateGameObject("first"); + first->AddComponent(1.5f); + first->AddComponent(); + first->GetTransform().SetWorldPosition({0.0f, 100.0f, 0.0f}); + first->GetTransform().SetWorldScale(glm::vec3{0.015f}); + + GameObject* second = scene.CreateGameObject("second"); + second->AddComponent(1.5f); + second->AddComponent(); + second->GetTransform().SetWorldPosition({0.0f, 100.0f, 0.0f}); + second->GetTransform().SetWorldScale(glm::vec3{0.015f}); + + scene.CommitPendingAdditions(); + scene.GetPhysics().RefreshGameObject(*first); + scene.GetPhysics().RefreshGameObject(*second); + + for (int step = 0; step < 240; ++step) { + scene.FixedUpdate(1.0f / 60.0f); + } + + check(first->GetTransform().GetWorldPosition().y > 0.8f && + second->GetTransform().GetWorldPosition().y > 0.8f, + "scene physics spheres must collide with the ground plane"); + check(glm::distance(first->GetTransform().GetWorldPosition(), + second->GetTransform().GetWorldPosition()) > 2.5f, + "scene physics spheres must not overlap after settling"); + + SceneManager::GetInstance().Destroy(); + } +} + +int main() +{ + testTransforms(); + testSceneRoundTrip(); + testEventMutation(); + testEventRemoval(); + testComponentRemoval(); + testAssetPathValidation(); + testLegacySceneLoad(); + testSceneLoadRollback(); + testPhysicsWorldUnits(); + testJoltSphereCollisions(); + testSceneJoltSphereCollisions(); + std::cout << "destrum tests passed\n"; + return EXIT_SUCCESS; +}