From 9d3d14a54dad83ddceba81c4428ecc6578084ef7 Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Wed, 24 Jun 2026 03:00:53 +0200 Subject: [PATCH] feat: add tracy / various fixes --- .gitmodules | 3 + destrum/CMakeLists.txt | 4 +- destrum/assets_src/238882.png | Bin 0 -> 51358 bytes destrum/assets_src/cube.fbx | Bin 11820 -> 18364 bytes destrum/include/destrum/App.h | 8 + destrum/include/destrum/Graphics/GfxDevice.h | 13 + .../destrum/Physics/JoltPhysicsWorld.h | 61 ++ .../include/destrum/Physics/PhysicsWorld.h | 4 +- destrum/include/destrum/Scene/Scene.h | 3 +- destrum/src/App.cpp | 294 ++++++-- destrum/src/Graphics/GfxDevice.cpp | 105 ++- .../src/Graphics/Pipelines/MeshPipeline.cpp | 6 +- destrum/src/Graphics/Renderer.cpp | 104 ++- destrum/src/Graphics/Swapchain.cpp | 187 +++-- destrum/src/Physics/JoltPhysicsWorld.cpp | 692 ++++++++++++++++++ destrum/src/Physics/PhysicsSceneBridge.cpp | 9 +- destrum/third_party/CMakeLists.txt | 7 + destrum/third_party/assimp | 2 +- destrum/third_party/fmt | 2 +- destrum/third_party/freetype | 2 +- destrum/third_party/glfw | 2 +- destrum/third_party/glm | 2 +- destrum/third_party/jolt | 2 +- destrum/third_party/json | 2 +- destrum/third_party/sdl | 2 +- destrum/third_party/spdlog | 2 +- destrum/third_party/tinyexr | 2 +- destrum/third_party/tinygltf | 2 +- destrum/third_party/tracy | 1 + destrum/third_party/vk-bootstrap | 2 +- destrum/third_party/vma | 2 +- destrum/third_party/volk | 2 +- lightkeeper/src/Lightkeeper.cpp | 299 ++++---- 33 files changed, 1492 insertions(+), 336 deletions(-) create mode 100644 destrum/assets_src/238882.png create mode 100644 destrum/include/destrum/Physics/JoltPhysicsWorld.h create mode 100644 destrum/src/Physics/JoltPhysicsWorld.cpp create mode 160000 destrum/third_party/tracy diff --git a/.gitmodules b/.gitmodules index eb037e1..b172a35 100644 --- a/.gitmodules +++ b/.gitmodules @@ -47,3 +47,6 @@ [submodule "destrum/third_party/jolt"] path = destrum/third_party/jolt url = https://github.com/jrouwe/JoltPhysics.git +[submodule "destrum/third_party/tracy"] + path = destrum/third_party/tracy + url = https://github.com/wolfpld/tracy.git diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index eec0721..c6136b6 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -56,7 +56,7 @@ set(SRC_FILES "src/Physics/PhysicsWorld.cpp" "src/Physics/SimplePhysicsWorld.cpp" "src/Physics/PhysicsSceneBridge.cpp" - src/Components/Physics/BoxCollider.cpp + "src/Physics/JoltPhysicsWorld.cpp" ) add_library(destrum ${SRC_FILES}) @@ -89,6 +89,7 @@ target_link_libraries(destrum assimp imgui Jolt + Tracy::TracyClient PRIVATE freetype::freetype @@ -99,6 +100,7 @@ target_compile_definitions(destrum PUBLIC VK_NO_PROTOTYPES VMA_VULKAN_VERSION=1003000 + TRACY_VK_USE_SYMBOL_TABLE # VOLK_DEFAULT_VISIBILITY # FIXME: doesn't work for some reason ) diff --git a/destrum/assets_src/238882.png b/destrum/assets_src/238882.png new file mode 100644 index 0000000000000000000000000000000000000000..594f6314247baee7aac6fd539918861ee7217533 GIT binary patch literal 51358 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 @@ -126,12 +133,16 @@ public: return swapchain.getImageView(imageIndex); } + DestrumTracyVkCtx getTracyVkCtx() const { return tracyVkCtx; } + private: vkb::Instance instance; vkb::PhysicalDevice physicalDevice; vkb::Device device; VmaAllocator allocator; + DestrumTracyVkCtx tracyVkCtx = nullptr; + std::uint32_t graphicsQueueFamily; VkQueue graphicsQueue; @@ -149,6 +160,8 @@ private: ImageCache imageCache; + bool m_vSync = false; + static uint32_t BytesPerTexel(VkFormat fmt) { switch (fmt) { case VK_FORMAT_R8_UNORM: return 1; diff --git a/destrum/include/destrum/Physics/JoltPhysicsWorld.h b/destrum/include/destrum/Physics/JoltPhysicsWorld.h new file mode 100644 index 0000000..f7b16a2 --- /dev/null +++ b/destrum/include/destrum/Physics/JoltPhysicsWorld.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +#include + +#include + +class JoltPhysicsWorld final : public PhysicsWorld { +public: + struct Settings { + std::uint32_t maxBodies{65536}; + std::uint32_t numBodyMutexes{0}; + std::uint32_t maxBodyPairs{65536}; + std::uint32_t maxContactConstraints{10240}; + + // Jolt recommends a temp allocator for physics update allocations. + // 10 MB is the value used in their HelloWorld sample. + std::uint32_t tempAllocatorSizeBytes{10u * 1024u * 1024u}; + + // 0 means "hardware_concurrency - 1". + std::uint32_t workerThreadCount{0}; + + glm::vec3 gravity{0.0f, -9.81f, 0.0f}; + }; + + explicit JoltPhysicsWorld(const Settings& settings = Settings{}); + ~JoltPhysicsWorld() override; + + JoltPhysicsWorld(const JoltPhysicsWorld&) = delete; + JoltPhysicsWorld(JoltPhysicsWorld&&) noexcept = delete; + JoltPhysicsWorld& operator=(const JoltPhysicsWorld&) = delete; + JoltPhysicsWorld& operator=(JoltPhysicsWorld&&) noexcept = delete; + + void Step(float fixedDt) override; + + void SyncKinematicBodiesToPhysics() override; + void SyncDynamicBodiesToTransforms() override; + + PhysicsBodyHandle CreateBody(const PhysicsBodyDesc& desc) override; + void DestroyBody(PhysicsBodyHandle body) override; + + void SetBodyTransform(PhysicsBodyHandle body, const PhysicsTransform& transform) override; + PhysicsTransform GetBodyTransform(PhysicsBodyHandle body) const override; + + void SetLinearVelocity(PhysicsBodyHandle body, const glm::vec3& velocity) override; + glm::vec3 GetLinearVelocity(PhysicsBodyHandle body) const override; + + void AddForce(PhysicsBodyHandle body, const glm::vec3& force) override; + void AddImpulse(PhysicsBodyHandle body, const glm::vec3& impulse) override; + + bool Raycast(const glm::vec3& origin, + const glm::vec3& direction, + float maxDistance, + PhysicsRaycastHit& hit) const override; + +private: + class Impl; + std::unique_ptr m_Impl; +}; diff --git a/destrum/include/destrum/Physics/PhysicsWorld.h b/destrum/include/destrum/Physics/PhysicsWorld.h index 2dd84e9..c966f9c 100644 --- a/destrum/include/destrum/Physics/PhysicsWorld.h +++ b/destrum/include/destrum/Physics/PhysicsWorld.h @@ -25,8 +25,8 @@ public: // - Kinematic: Transform -> physics body before Step() // - Dynamic: physics body -> Transform after Step() - void SyncKinematicBodiesToPhysics(); - void SyncDynamicBodiesToTransforms(); + virtual void SyncKinematicBodiesToPhysics(); + virtual void SyncDynamicBodiesToTransforms(); virtual PhysicsBodyHandle CreateBody(const PhysicsBodyDesc& desc) = 0; virtual void DestroyBody(PhysicsBodyHandle body) = 0; diff --git a/destrum/include/destrum/Scene/Scene.h b/destrum/include/destrum/Scene/Scene.h index 6c491d1..25403cb 100644 --- a/destrum/include/destrum/Scene/Scene.h +++ b/destrum/include/destrum/Scene/Scene.h @@ -6,6 +6,7 @@ #include #include +#include "destrum/Physics/JoltPhysicsWorld.h" #include "destrum/Physics/PhysicsSceneBridge.h" class GameObject; @@ -68,7 +69,7 @@ public: private: explicit Scene(const std::string& name); - PhysicsSceneBridge m_Physics{std::make_unique()}; + PhysicsSceneBridge m_Physics{std::make_unique()}; std::string m_name; diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index cde4d64..c722488 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -10,7 +10,16 @@ #include "glm/gtx/transform.hpp" #include "spdlog/spdlog.h" -#include +#include +#include + +struct TracyFrameScope +{ + ~TracyFrameScope() + { + FrameMark; + } +}; App::App() { @@ -19,7 +28,9 @@ App::App() 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"); @@ -53,16 +64,21 @@ void App::init(const AppParams& params) void App::run() { - Time::GetInstance().Update(); // initialize delta timing + ZoneScopedN("App::run"); + + Time::GetInstance().Update(); const float fixedDt = static_cast(Time::GetInstance().FixedDeltaTime()); - const int maxSteps = 5; // prevent spiral of death + const int maxSteps = 5; float accumulator = 0.0f; isRunning = true; + while (isRunning) { - // ---- Update timing --- + TracyFrameScope tracyFrame; + ZoneScopedN("App::Frame"); + Time::GetInstance().Update(); float dt = static_cast(Time::GetInstance().DeltaTime()); @@ -72,117 +88,193 @@ void App::run() { float newFPS = 1.0f / dt; avgFPS = std::lerp(avgFPS, newFPS, 0.1f); + TracyPlot("FPS", avgFPS); + TracyPlot("Delta Time ms", dt * 1000.0f); } accumulator += dt; - InputManager::GetInstance().BeginFrame(); - camera.Update(dt); - - - SDL_Event event; - while (SDL_PollEvent(&event)) { - imguiPass.handleEvent(event); - if (event.type == SDL_QUIT) + ZoneScopedN("Input BeginFrame + Camera"); + InputManager::GetInstance().BeginFrame(); + camera.Update(dt); + } + + { + ZoneScopedN("SDL Events"); + + SDL_Event event; + while (SDL_PollEvent(&event)) { - isRunning = false; - break; - } - if (event.type == SDL_WINDOWEVENT) - { - switch (event.window.event) + ZoneScopedN("SDL Event"); + + imguiPass.handleEvent(event); + + if (event.type == SDL_QUIT) { - case SDL_WINDOWEVENT_SIZE_CHANGED: - case SDL_WINDOWEVENT_RESIZED: + isRunning = false; + break; + } + + if (event.type == SDL_WINDOWEVENT) + { + switch (event.window.event) { + case SDL_WINDOWEVENT_SIZE_CHANGED: + case SDL_WINDOWEVENT_RESIZED: resizePending = true; lastResizeTime = std::chrono::steady_clock::now(); break; } } - } - const bool mouseEvent = - event.type == SDL_MOUSEBUTTONDOWN || - event.type == SDL_MOUSEBUTTONUP || - event.type == SDL_MOUSEMOTION || - event.type == SDL_MOUSEWHEEL; - const bool keyboardEvent = - event.type == SDL_KEYDOWN || - event.type == SDL_KEYUP || - event.type == SDL_TEXTINPUT; + const bool mouseEvent = + event.type == SDL_MOUSEBUTTONDOWN || + event.type == SDL_MOUSEBUTTONUP || + event.type == SDL_MOUSEMOTION || + event.type == SDL_MOUSEWHEEL; - const bool capturedByImgui = - (mouseEvent && imguiPass.wantsMouse()) || - (keyboardEvent && imguiPass.wantsKeyboard()); + const bool keyboardEvent = + event.type == SDL_KEYDOWN || + event.type == SDL_KEYUP || + event.type == SDL_TEXTINPUT; - if (!capturedByImgui) - { - if (InputManager::GetInstance().ProcessEvent(event)) + const bool capturedByImgui = + (mouseEvent && imguiPass.wantsMouse()) || + (keyboardEvent && imguiPass.wantsKeyboard()); + + if (!capturedByImgui) { - isRunning = false; + ZoneScopedN("Input ProcessEvent"); + + if (InputManager::GetInstance().ProcessEvent(event)) + { + isRunning = false; + } } } } + if (!isRunning) break; - imguiPass.beginFrame(); - customUpdate(dt); - - ImGui::Begin("Debug"); - ImGui::Text("FPS: %.2f", avgFPS); - ImGui::End(); - - imguiPass.endFrame(); - int steps = 0; - while (accumulator >= fixedDt && steps < maxSteps) { - // physics.Update(fixedDt); - customFixedUpdate(fixedDt); - // physics.Step(fixedDt); - // physics.SyncTransforms(); + ZoneScopedN("ImGui BeginFrame"); + imguiPass.beginFrame(); + } - accumulator -= fixedDt; - steps++; + { + ZoneScopedN("customUpdate"); + customUpdate(dt); + } + + { + ZoneScopedN("Debug ImGui"); + ImGui::Begin("Debug"); + ImGui::Text("FPS: %.2f", avgFPS); + ImGui::End(); + + drawPhysicsPanel(dt, fixedDt, accumulator); + + TracyPlot("FPS", avgFPS); + TracyPlot("Frame Time ms", dt * 1000.0f); + TracyPlot("Accumulator ms", accumulator * 1000.0f); + } + + { + ZoneScopedN("ImGui EndFrame"); + imguiPass.endFrame(); + } + + { + ZoneScopedN("FixedUpdate"); + + int steps = 0; + + if (m_PhysicsPaused) + { + // Important: prevent physics from building up a huge backlog while paused. + accumulator = 0.0f; + + if (m_PhysicsStepOnce) + { + ZoneScopedN("Single Physics Step"); + + customFixedUpdate(fixedDt * m_PhysicsTimeScale); + + steps = 1; + m_PhysicsStepOnce = false; + } + } + else + { + while (accumulator >= fixedDt && steps < maxSteps) + { + ZoneScopedN("Fixed Step"); + + customFixedUpdate(fixedDt * m_PhysicsTimeScale); + + accumulator -= fixedDt; + steps++; + } + + if (steps == maxSteps) + { + accumulator = 0.0f; + } + } + + m_PhysicsStepsLastFrame = steps; + + TracyPlot("Fixed Steps", static_cast(steps)); } - if (steps == maxSteps) accumulator = 0.0f; const float alpha = accumulator / fixedDt; + (void)alpha; - if (gfxDevice.needsSwapchainRecreate() || resizePending) { - auto now = std::chrono::steady_clock::now(); + ZoneScopedN("Swapchain Resize Check"); - if (resizePending && - now - lastResizeTime < std::chrono::milliseconds(100)) + if (gfxDevice.needsSwapchainRecreate() || resizePending) { + auto now = std::chrono::steady_clock::now(); + + if (resizePending && + now - lastResizeTime < std::chrono::milliseconds(100)) + { + continue; + } + + int w = 0; + int h = 0; + SDL_Vulkan_GetDrawableSize(window, &w, &h); + + if (w == 0 || h == 0) + { + continue; + } + + spdlog::info("Recreating swapchain to size: {}x{}", w, h); + + { + ZoneScopedN("Recreate Swapchain"); + gfxDevice.recreateSwapchain(w, h); + onWindowResize(w, h); + } + + resizePending = false; continue; } - - int w = 0; - int h = 0; - SDL_Vulkan_GetDrawableSize(window, &w, &h); - - if (w == 0 || h == 0) - { - continue; - } - - spdlog::info("Recreating swapchain to size: {}x{}", w, h); - - gfxDevice.recreateSwapchain(w, h); - onWindowResize(w, h); - - resizePending = false; - continue; } - customDraw(); - + { + ZoneScopedN("customDraw"); + customDraw(); + } if (frameLimit) { + ZoneScopedN("Frame Limit Sleep"); + auto sleepTime = Time::GetInstance().SleepDuration(); if (sleepTime.count() > 0) { @@ -199,3 +291,49 @@ void App::cleanup() spdlog::info("Cleaning up"); customCleanup(); } + +void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator) +{ + ImGui::Begin("Physics"); + + ImGui::Text("Frame dt: %.3f ms", dt * 1000.0f); + ImGui::Text("Fixed dt: %.3f ms", fixedDt * 1000.0f); + ImGui::Text("Accumulator: %.3f ms", accumulator * 1000.0f); + ImGui::Text("Steps last frame: %d", m_PhysicsStepsLastFrame); + + ImGui::Separator(); + + if (ImGui::Button(m_PhysicsPaused ? "Resume Physics" : "Pause Physics")) + { + m_PhysicsPaused = !m_PhysicsPaused; + } + + ImGui::SameLine(); + + if (!m_PhysicsPaused) + { + ImGui::BeginDisabled(); + ImGui::Button("Step Physics"); + ImGui::EndDisabled(); + } + else + { + if (ImGui::Button("Step Physics")) + { + m_PhysicsStepOnce = true; + } + } + + ImGui::Checkbox("Paused", &m_PhysicsPaused); + + ImGui::Separator(); + + ImGui::SliderFloat("Physics Time Scale", &m_PhysicsTimeScale, 0.0f, 2.0f, "%.2fx"); + + if (ImGui::Button("Reset Time Scale")) + { + m_PhysicsTimeScale = 1.0f; + } + + ImGui::End(); +} \ No newline at end of file diff --git a/destrum/src/Graphics/GfxDevice.cpp b/destrum/src/Graphics/GfxDevice.cpp index 07d6acd..b84c45f 100644 --- a/destrum/src/Graphics/GfxDevice.cpp +++ b/destrum/src/Graphics/GfxDevice.cpp @@ -19,13 +19,17 @@ #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" GfxDevice::GfxDevice(): imageCache(*this) { } 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) @@ -131,6 +135,34 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync) 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( @@ -166,19 +198,30 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync) void GfxDevice::recreateSwapchain(int width, int height) { assert(width != 0 && height != 0); waitIdle(); - swapchain.recreateSwapchain(*this, swapchainFormat, width, height, true); + swapchain.recreateSwapchain(*this, swapchainFormat, width, height, m_vSync); } -VkCommandBuffer GfxDevice::beginFrame() { - swapchain.beginFrame(getCurrentFrameIndex()); +VkCommandBuffer GfxDevice::beginFrame() +{ + ZoneScopedN("GfxDevice::beginFrame"); + + { + ZoneScopedN("Swapchain BeginFrame"); + swapchain.beginFrame(getCurrentFrameIndex()); + } 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, }; - VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo)); + + { + ZoneScopedN("vkBeginCommandBuffer"); + VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo)); + } return cmd; } @@ -188,17 +231,34 @@ VulkanImmediateExecutor& GfxDevice::GetImmediateExecuter() { } void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const EndFrameProps& props) { + ZoneScopedN("GfxDevice::endFrame"); + // get swapchain image - const auto [swapchainImage, swapchainImageIndex] = swapchain.acquireNextImage(getCurrentFrameIndex()); + 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 (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 - swapchain.resetFences(getCurrentFrameIndex()); + { + ZoneScopedN("Swapchain ResetFences"); + swapchain.resetFences(getCurrentFrameIndex()); + } auto swapchainLayout = VK_IMAGE_LAYOUT_UNDEFINED; { + ZoneScopedN("Clear Swapchain Image"); + const VkImageSubresourceRange clearRange = vkinit::imageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT); vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_GENERAL); swapchainLayout = VK_IMAGE_LAYOUT_GENERAL; @@ -208,6 +268,8 @@ 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, @@ -248,6 +310,8 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E // swapchainLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; if (props.imguiPass) { + ZoneScopedN("ImGui Render"); + props.imguiPass->render( cmd, swapchainImage, @@ -258,18 +322,37 @@ void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const E } // prepare for present - vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); - swapchainLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + { + ZoneScopedN("Transition Swapchain To Present"); - VK_CHECK(vkEndCommandBuffer(cmd)); + vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + swapchainLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + } +#if defined(TRACY_ENABLE) + TracyVkCollect(tracyVkCtx, cmd); +#endif + { + ZoneScopedN("vkEndCommandBuffer"); + VK_CHECK(vkEndCommandBuffer(cmd)); + } // swapchain.submitAndPresent(cmd, graphicsQueue, getCurrentFrameIndex(), swapchainImageIndex); - swapchain.submitAndPresent(cmd, graphicsQueue, swapchainImageIndex, getCurrentFrameIndex()); + { + ZoneScopedN("Swapchain SubmitAndPresent"); + swapchain.submitAndPresent(cmd, graphicsQueue, swapchainImageIndex, getCurrentFrameIndex()); + } frameNumber++; + FrameMark; } void GfxDevice::cleanup() { +#if defined(TRACY_ENABLE) + if (tracyVkCtx) { + TracyVkDestroy(tracyVkCtx); + tracyVkCtx = nullptr; + } +#endif } void GfxDevice::waitIdle() { diff --git a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp index bf256c7..d5a5b83 100644 --- a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp @@ -98,9 +98,9 @@ void MeshPipeline::draw(VkCommandBuffer cmd, for (const auto& dcIdx : drawCommands) { const auto& dc = dcIdx; - // if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) { - // continue; - // } + if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) { + continue; + } ActualDrawCalls++; diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index 96b5dc2..b9a621e 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -5,6 +5,8 @@ #include "destrum/Util/GameState.h" #include "spdlog/spdlog.h" +#include "tracy/TracyVulkan.hpp" + GameRenderer::GameRenderer(MeshCache& meshCache, MaterialCache& matCache): meshCache{meshCache}, materialCache{matCache} { } @@ -41,13 +43,20 @@ void GameRenderer::endDrawing() { } void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) { + ZoneScopedN("GameRenderer::draw"); + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "GameRenderer::draw"); - for (const auto& dc : meshDrawCommands) { - if (!dc.skinnedMesh) { - continue; + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Skinning"); + + for (const auto& dc : meshDrawCommands) { + if (!dc.skinnedMesh) { + continue; + } + skinningPipeline->doSkinning(cmd, gfxDevice.getCurrentFrameIndex(), meshCache, dc); } - skinningPipeline->doSkinning(cmd, gfxDevice.getCurrentFrameIndex(), meshCache, dc); } + const auto gpuSceneData = GPUSceneData{ .view = sceneData.camera.GetViewMatrix(), .proj = sceneData.camera.GetProjectionMatrix(), @@ -59,40 +68,71 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& .fogDensity = sceneData.fogDensity, .materialsBuffer = materialCache.getMaterialDataBufferAddress(), }; - vkutil::bufferHostWriteToShaderReadBarrier( - cmd, - materialCache.getMaterialDataBuffer().buffer, - 0, - VK_WHOLE_SIZE - ); - sceneDataBuffer.uploadNewData(cmd, gfxDevice.getCurrentFrameIndex(), (void*)&gpuSceneData, sizeof(GPUSceneData)); + + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Material Buffer Barrier"); + + vkutil::bufferHostWriteToShaderReadBarrier( + cmd, + materialCache.getMaterialDataBuffer().buffer, + 0, + VK_WHOLE_SIZE + ); + } + + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Upload Scene Data"); + + sceneDataBuffer.uploadNewData( + cmd, + gfxDevice.getCurrentFrameIndex(), + (void*)&gpuSceneData, + sizeof(GPUSceneData) + ); + } const auto& drawImage = gfxDevice.getImage(drawImageId); const auto& depthImage = gfxDevice.getImage(depthImageId); // vkutil::cmdBeginLabel(cmd, "Geometry"); - vkutil::transitionImage( + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Transition Draw Image"); + + vkutil::transitionImage( cmd, drawImage.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + } - vkutil::transitionImage( - cmd, - depthImage.image, - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "Transition Depth Image"); + + vkutil::transitionImage( + cmd, + depthImage.image, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + } const 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, - }); + .renderExtent = drawImage.getExtent2D(), + .colorImageView = drawImage.imageView, + .colorImageClearValue = glm::vec4{0.f, 0.f, 0.f, 1.f}, + .depthImageView = depthImage.imageView, + .depthImageClearValue = 1.f, + }); - vkCmdBeginRendering(cmd, &renderInfo.renderingInfo); - meshPipeline->draw( + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "vkCmdBeginRendering"); + + vkCmdBeginRendering(cmd, &renderInfo.renderingInfo); + } + + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "MeshPipeline::draw"); + + meshPipeline->draw( cmd, drawImage.getExtent2D(), gfxDevice, @@ -102,14 +142,20 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& sceneDataBuffer.getBuffer(), meshDrawCommands, sortedMeshDrawCommands); + } - skyboxPipeline->draw(cmd, gfxDevice, camera); + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "SkyboxPipeline::draw"); + skyboxPipeline->draw(cmd, gfxDevice, camera); + } - vkCmdEndRendering(cmd); + { + TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "vkCmdEndRendering"); + + vkCmdEndRendering(cmd); + } // vkutil::cmdEndLabel(cmd); - - } void GameRenderer::cleanup(GfxDevice& gfxDevice) { diff --git a/destrum/src/Graphics/Swapchain.cpp b/destrum/src/Graphics/Swapchain.cpp index 43f8ebc..df2ee10 100644 --- a/destrum/src/Graphics/Swapchain.cpp +++ b/destrum/src/Graphics/Swapchain.cpp @@ -6,6 +6,7 @@ #include #include "volk.h" +#include "tracy/Tracy.hpp" void Swapchain::initSync(VkDevice device) { @@ -23,30 +24,40 @@ void Swapchain::initSync(VkDevice device) { } void Swapchain::createSwapchain(GfxDevice* gfxDevice, 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; + // vSync = true; - auto res = vkb::SwapchainBuilder{gfxDevice->getDevice()} - .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) - .set_desired_present_mode( - 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))); + { + ZoneScopedN("vkb::SwapchainBuilder::build"); + + auto res = vkb::SwapchainBuilder{gfxDevice->getDevice()} + .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) + .set_desired_present_mode( + 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))); + } + m_swapchain = res.value(); } - m_swapchain = res.value(); - images = m_swapchain.get_images().value(); - imageViews = m_swapchain.get_image_views().value(); + { + ZoneScopedN("Get Swapchain Images / Views"); + + images = m_swapchain.get_images().value(); + imageViews = m_swapchain.get_image_views().value(); + } imageRenderSemaphores.resize(images.size()); @@ -55,6 +66,8 @@ void Swapchain::createSwapchain(GfxDevice* gfxDevice, VkFormat format, std::uint }; for (auto& sem: imageRenderSemaphores) { + ZoneScopedN("Create Image Render Semaphore"); + VK_CHECK(vkCreateSemaphore(m_gfxDevice->getDevice(), &sci, nullptr, &sem)); } @@ -71,6 +84,8 @@ void Swapchain::recreateSwapchain( std::uint32_t height, bool vSync) { + ZoneScopedN("Swapchain::recreateSwapchain"); + if (width == 0 || height == 0) { dirty = true; return; @@ -78,45 +93,67 @@ void Swapchain::recreateSwapchain( VkDevice device = gfxDevice.getDevice(); - vkDeviceWaitIdle(device); + { + ZoneScopedN("vkDeviceWaitIdle"); + vkDeviceWaitIdle(device); + } auto oldSwapchain = m_swapchain; - auto res = vkb::SwapchainBuilder{gfxDevice.getVkbDevice()} - .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) - .set_desired_present_mode( - vSync ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR) - .set_desired_extent(width, height) - .build(); + { + ZoneScopedN("vkb::SwapchainBuilder::rebuild"); - 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))); + auto res = vkb::SwapchainBuilder{gfxDevice.getVkbDevice()} + .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) + .set_desired_present_mode( + 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))); + } + + m_swapchain = res.value(); } - for (auto sem : imageRenderSemaphores) { - vkDestroySemaphore(device, sem, nullptr); + { + ZoneScopedN("Destroy Old Image Render Semaphores"); + + for (auto sem : imageRenderSemaphores) { + vkDestroySemaphore(device, sem, nullptr); + } + imageRenderSemaphores.clear(); } - imageRenderSemaphores.clear(); - for (auto imageView : imageViews) { - vkDestroyImageView(device, imageView, nullptr); + { + ZoneScopedN("Destroy Old Image Views"); + + for (auto imageView : imageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + imageViews.clear(); } - imageViews.clear(); - vkb::destroy_swapchain(oldSwapchain); + { + ZoneScopedN("Destroy Old Swapchain"); + vkb::destroy_swapchain(oldSwapchain); + } - m_swapchain = res.value(); + { + ZoneScopedN("Get New Swapchain Images / Views"); - images = m_swapchain.get_images().value(); - imageViews = m_swapchain.get_image_views().value(); + images = m_swapchain.get_images().value(); + imageViews = m_swapchain.get_image_views().value(); + } VkSemaphoreCreateInfo sci{ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, @@ -125,6 +162,8 @@ void Swapchain::recreateSwapchain( imageRenderSemaphores.resize(images.size()); for (auto& sem : imageRenderSemaphores) { + ZoneScopedN("Create New Image Render Semaphore"); + VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &sem)); } @@ -148,13 +187,25 @@ void Swapchain::cleanup() { } void Swapchain::beginFrame(int index) const { + ZoneScopedN("Swapchain::beginFrame"); + auto& frame = frames[index]; - VK_CHECK(vkWaitForFences(m_gfxDevice->getDevice(), 1, &frame.renderFence, true, std::numeric_limits::max())); + + { + ZoneScopedN("vkWaitForFences"); + VK_CHECK(vkWaitForFences(m_gfxDevice->getDevice(), 1, &frame.renderFence, true, std::numeric_limits::max())); + } } void Swapchain::resetFences(int index) const { + ZoneScopedN("Swapchain::resetFences"); + auto& frame = frames[index]; - VK_CHECK(vkResetFences(m_gfxDevice->getDevice(), 1, &frame.renderFence)); + + { + ZoneScopedN("vkResetFences"); + VK_CHECK(vkResetFences(m_gfxDevice->getDevice(), 1, &frame.renderFence)); + } } struct SwapchainAcquireResult { @@ -164,14 +215,24 @@ struct SwapchainAcquireResult { }; std::pair Swapchain::acquireNextImage(int index) { + ZoneScopedN("Swapchain::acquireNextImage"); + std::uint32_t swapchainImageIndex{}; - const auto result = vkAcquireNextImageKHR( - m_gfxDevice->getDevice(), - m_swapchain, - std::numeric_limits::max(), - frames[index].swapchainSemaphore, - VK_NULL_HANDLE, - &swapchainImageIndex); + + VkResult result = VK_SUCCESS; + + { + ZoneScopedN("vkAcquireNextImageKHR"); + + result = vkAcquireNextImageKHR( + m_gfxDevice->getDevice(), + m_swapchain, + std::numeric_limits::max(), + frames[index].swapchainSemaphore, + VK_NULL_HANDLE, + &swapchainImageIndex); + } + if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { dirty = true; return {images[swapchainImageIndex], swapchainImageIndex}; @@ -188,6 +249,8 @@ void Swapchain::submitAndPresent( uint32_t imageIndex, // from vkAcquireNextImageKHR uint32_t frameIndex) // 0..FRAMES_IN_FLIGHT-1 { + ZoneScopedN("Swapchain::submitAndPresent"); + auto& frame = frames[frameIndex]; // ✅ per-frame VkSemaphore renderFinished = imageRenderSemaphores[imageIndex]; // ✅ per-image @@ -209,7 +272,11 @@ void Swapchain::submitAndPresent( renderFinished); // ✅ signal semaphore (per-image) VkSubmitInfo2 submit = vkinit::submitInfo(&cmdInfo, &waitInfo, &signalInfo); - VK_CHECK(vkQueueSubmit2(graphicsQueue, 1, &submit, frame.renderFence)); // ✅ fence (per-frame) + + { + ZoneScopedN("vkQueueSubmit2"); + VK_CHECK(vkQueueSubmit2(graphicsQueue, 1, &submit, frame.renderFence)); // ✅ fence (per-frame) + } // present VkPresentInfoKHR presentInfo{ @@ -221,7 +288,13 @@ void Swapchain::submitAndPresent( .pImageIndices = &imageIndex, // ✅ imageIndex, NOT frameIndex }; - VkResult res = vkQueuePresentKHR(graphicsQueue, &presentInfo); + VkResult res = VK_SUCCESS; + + { + ZoneScopedN("vkQueuePresentKHR"); + res = vkQueuePresentKHR(graphicsQueue, &presentInfo); + } + if (res == VK_ERROR_OUT_OF_DATE_KHR || res == VK_SUBOPTIMAL_KHR) dirty = true; else if (res != VK_SUCCESS) dirty = true; } diff --git a/destrum/src/Physics/JoltPhysicsWorld.cpp b/destrum/src/Physics/JoltPhysicsWorld.cpp new file mode 100644 index 0000000..83b9c1c --- /dev/null +++ b/destrum/src/Physics/JoltPhysicsWorld.cpp @@ -0,0 +1,692 @@ +#include + +// Jolt/Jolt.h must be included before other Jolt headers. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace +{ + using namespace JPH; + + static std::atomic_uint32_t g_JoltInstanceCount{0}; + + static void TraceImpl(const char* inFMT, ...) + { + va_list list; + va_start(list, inFMT); + std::vprintf(inFMT, list); + std::printf("\n"); + va_end(list); + } + +#ifdef JPH_ENABLE_ASSERTS + static bool AssertFailedImpl(const char* inExpression, + const char* inMessage, + const char* inFile, + JPH::uint inLine) + { + std::printf("%s:%u: (%s) %s\n", + inFile, + inLine, + inExpression, + inMessage != nullptr ? inMessage : ""); + return true; + } +#endif + + void InitJoltGlobals() + { + if (g_JoltInstanceCount.fetch_add(1) == 0) + { + RegisterDefaultAllocator(); + + Trace = TraceImpl; + JPH_IF_ENABLE_ASSERTS(AssertFailed = AssertFailedImpl;) + + Factory::sInstance = new Factory(); + + RegisterTypes(); + } + } + + void ShutdownJoltGlobals() + { + if (g_JoltInstanceCount.fetch_sub(1) == 1) + { + UnregisterTypes(); + + delete Factory::sInstance; + Factory::sInstance = nullptr; + } + } + + namespace Layers + { + static constexpr ObjectLayer NON_MOVING = 0; + static constexpr ObjectLayer MOVING = 1; + static constexpr ObjectLayer NUM_LAYERS = 2; + } + + namespace BroadPhaseLayers + { + static constexpr BroadPhaseLayer NON_MOVING(0); + static constexpr BroadPhaseLayer MOVING(1); + static constexpr JPH::uint NUM_LAYERS(2); + } + + class ObjectLayerPairFilterImpl final : public ObjectLayerPairFilter + { + public: + bool ShouldCollide(ObjectLayer inObject1, ObjectLayer inObject2) const override + { + switch (inObject1) + { + case Layers::NON_MOVING: + return inObject2 == Layers::MOVING; + + case Layers::MOVING: + return true; + + default: + JPH_ASSERT(false); + return false; + } + } + }; + + class BPLayerInterfaceImpl final : public BroadPhaseLayerInterface + { + public: + BPLayerInterfaceImpl() + { + m_ObjectToBroadPhase[Layers::NON_MOVING] = BroadPhaseLayers::NON_MOVING; + m_ObjectToBroadPhase[Layers::MOVING] = BroadPhaseLayers::MOVING; + } + + JPH::uint GetNumBroadPhaseLayers() const override + { + return BroadPhaseLayers::NUM_LAYERS; + } + + BroadPhaseLayer GetBroadPhaseLayer(ObjectLayer inLayer) const override + { + JPH_ASSERT(inLayer < Layers::NUM_LAYERS); + return m_ObjectToBroadPhase[inLayer]; + } + +#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED) + const char* GetBroadPhaseLayerName(BroadPhaseLayer inLayer) const override + { + switch ((BroadPhaseLayer::Type)inLayer) + { + case (BroadPhaseLayer::Type)BroadPhaseLayers::NON_MOVING: + return "NON_MOVING"; + + case (BroadPhaseLayer::Type)BroadPhaseLayers::MOVING: + return "MOVING"; + + default: + JPH_ASSERT(false); + return "INVALID"; + } + } +#endif + + private: + BroadPhaseLayer m_ObjectToBroadPhase[Layers::NUM_LAYERS]; + }; + + class ObjectVsBroadPhaseLayerFilterImpl final : public ObjectVsBroadPhaseLayerFilter + { + public: + bool ShouldCollide(ObjectLayer inLayer1, BroadPhaseLayer inLayer2) const override + { + switch (inLayer1) + { + case Layers::NON_MOVING: + return inLayer2 == BroadPhaseLayers::MOVING; + + case Layers::MOVING: + return true; + + default: + JPH_ASSERT(false); + return false; + } + } + }; + + [[nodiscard]] Vec3 ToJoltVec3(const glm::vec3& v) + { + return Vec3(v.x, v.y, v.z); + } + + [[nodiscard]] RVec3 ToJoltRVec3(const glm::vec3& v) + { + return RVec3(v.x, v.y, v.z); + } + + [[nodiscard]] Quat ToJoltQuat(const glm::quat& q) + { + return Quat(q.x, q.y, q.z, q.w); + } + + [[nodiscard]] glm::vec3 FromJoltVec3(Vec3Arg v) + { + return glm::vec3(v.GetX(), v.GetY(), v.GetZ()); + } + + [[nodiscard]] glm::vec3 FromJoltRVec3(RVec3Arg v) + { + return glm::vec3(static_cast(v.GetX()), + static_cast(v.GetY()), + static_cast(v.GetZ())); + } + + [[nodiscard]] glm::quat FromJoltQuat(QuatArg q) + { + return glm::quat(q.GetW(), q.GetX(), q.GetY(), q.GetZ()); + } + + [[nodiscard]] EMotionType ToJoltMotionType(RigidbodyType type) + { + switch (type) + { + case RigidbodyType::Static: + return EMotionType::Static; + + case RigidbodyType::Dynamic: + return EMotionType::Dynamic; + + case RigidbodyType::Kinematic: + return EMotionType::Kinematic; + + default: + return EMotionType::Static; + } + } + + [[nodiscard]] ObjectLayer ToJoltObjectLayer(RigidbodyType type) + { + return type == RigidbodyType::Static ? Layers::NON_MOVING : Layers::MOVING; + } + + [[nodiscard]] ShapeRefC CreateJoltShape(const PhysicsShapeDesc& desc) + { + switch (desc.type) + { + case PhysicsShapeType::Box: + { + BoxShapeSettings settings(ToJoltVec3(desc.halfExtents)); + ShapeSettings::ShapeResult result = settings.Create(); + + if (!result.IsValid()) + { + throw std::runtime_error( + std::string("Failed to create Jolt box shape: ") + std::string(result.GetError())); + } + + return result.Get(); + } + + case PhysicsShapeType::Sphere: + return new SphereShape(desc.radius); + + case PhysicsShapeType::Capsule: + { + // Jolt expects half-height of the cylindrical section, not full capsule height. + const float halfCylinderHeight = std::max(0.0f, (desc.height * 0.5f) - desc.radius); + return new CapsuleShape(halfCylinderHeight, desc.radius); + } + + case PhysicsShapeType::None: + default: + throw std::runtime_error("Cannot create Jolt body without a valid shape."); + } + } +} // namespace + +class JoltPhysicsWorld::Impl +{ +public: + explicit Impl(const Settings& settings) + : m_Settings(settings) + { + InitJoltGlobals(); + + const JPH::uint hardwareThreads = std::thread::hardware_concurrency(); + const JPH::uint workerThreads = + settings.workerThreadCount != 0 + ? settings.workerThreadCount + : std::max(1, hardwareThreads > 0 ? hardwareThreads - 1 : 1); + + m_TempAllocator = std::make_unique(settings.tempAllocatorSizeBytes); + m_JobSystem = std::make_unique( + cMaxPhysicsJobs, + cMaxPhysicsBarriers, + static_cast(workerThreads)); + + m_PhysicsSystem.Init( + settings.maxBodies, + settings.numBodyMutexes, + settings.maxBodyPairs, + settings.maxContactConstraints, + m_BPLayerInterface, + m_ObjectVsBroadPhaseLayerFilter, + m_ObjectLayerPairFilter); + + m_PhysicsSystem.SetGravity(ToJoltVec3(settings.gravity)); + } + + ~Impl() + { + BodyInterface& bodyInterface = m_PhysicsSystem.GetBodyInterface(); + + for (auto& [handleId, record] : m_Bodies) + { + if (!record.alive) + { + continue; + } + + bodyInterface.RemoveBody(record.bodyID); + bodyInterface.DestroyBody(record.bodyID); + } + + m_Bodies.clear(); + + ShutdownJoltGlobals(); + } + + PhysicsBodyHandle CreateBody(const PhysicsBodyDesc& desc) + { + ShapeRefC shape = CreateJoltShape(desc.shape); + + // This first backend ignores centerOffset for now. + // Later, use JPH::OffsetCenterOfMassShapeSettings for offset colliders. + + BodyCreationSettings settings( + shape, + ToJoltRVec3(desc.transform.position), + ToJoltQuat(desc.transform.rotation), + ToJoltMotionType(desc.type), + ToJoltObjectLayer(desc.type)); + + settings.mFriction = desc.material.friction; + settings.mRestitution = desc.material.restitution; + settings.mIsSensor = desc.shape.isTrigger; + settings.mAllowSleeping = desc.allowSleep; + settings.mGravityFactor = desc.useGravity ? 1.0f : 0.0f; + settings.mUserData = reinterpret_cast(desc.owner); + + if (desc.type == RigidbodyType::Dynamic) + { + settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia; + settings.mMassPropertiesOverride.mMass = std::max(0.0001f, desc.mass); + } + + const EActivation activation = + desc.type == RigidbodyType::Dynamic ? EActivation::Activate : EActivation::DontActivate; + + BodyInterface& bodyInterface = m_PhysicsSystem.GetBodyInterface(); + BodyID bodyID = bodyInterface.CreateAndAddBody(settings, activation); + + if (bodyID.IsInvalid()) + { + return {}; + } + + PhysicsBodyHandle handle{m_NextHandle++}; + + BodyRecord record{}; + record.alive = true; + record.bodyID = bodyID; + record.desc = desc; + + m_Bodies.emplace(handle.id, record); + + return handle; + } + + void DestroyBody(PhysicsBodyHandle body) + { + auto it = m_Bodies.find(body.id); + if (it == m_Bodies.end() || !it->second.alive) + { + return; + } + + BodyInterface& bodyInterface = m_PhysicsSystem.GetBodyInterface(); + bodyInterface.RemoveBody(it->second.bodyID); + bodyInterface.DestroyBody(it->second.bodyID); + + m_Bodies.erase(it); + } + + void Step(float fixedDt) + { + if (fixedDt <= 0.0f) + { + return; + } + + const int collisionSteps = std::max(1, static_cast(std::ceil(fixedDt / (1.0f / 60.0f)))); + + m_PhysicsSystem.Update( + fixedDt, + collisionSteps, + m_TempAllocator.get(), + m_JobSystem.get()); + } + + void SyncKinematicBodiesToPhysics() + { + BodyInterface& bodyInterface = m_PhysicsSystem.GetBodyInterface(); + + for (auto& [handleId, record] : m_Bodies) + { + if (!record.alive || record.desc.type != RigidbodyType::Kinematic || !record.desc.owner) + { + continue; + } + + Transform& transform = record.desc.owner->GetTransform(); + + bodyInterface.SetPositionAndRotation( + record.bodyID, + ToJoltRVec3(transform.GetWorldPosition()), + ToJoltQuat(transform.GetWorldRotation()), + EActivation::Activate); + } + } + + void SyncDynamicBodiesToTransforms() + { + BodyInterface& bodyInterface = m_PhysicsSystem.GetBodyInterface(); + + for (auto& [handleId, record] : m_Bodies) + { + if (!record.alive || record.desc.type != RigidbodyType::Dynamic || !record.desc.owner) + { + continue; + } + + RVec3 position{}; + Quat rotation{}; + bodyInterface.GetPositionAndRotation(record.bodyID, position, rotation); + + Transform& transform = record.desc.owner->GetTransform(); + transform.SetWorldPosition(FromJoltRVec3(position)); + transform.SetWorldRotation(FromJoltQuat(rotation)); + } + } + + void SetBodyTransform(PhysicsBodyHandle body, const PhysicsTransform& transform) + { + if (BodyRecord* record = FindBody(body)) + { + m_PhysicsSystem.GetBodyInterface().SetPositionAndRotation( + record->bodyID, + ToJoltRVec3(transform.position), + ToJoltQuat(transform.rotation), + EActivation::Activate); + } + } + + PhysicsTransform GetBodyTransform(PhysicsBodyHandle body) const + { + const BodyRecord* record = FindBody(body); + if (!record) + { + return {}; + } + + RVec3 position{}; + Quat rotation{}; + m_PhysicsSystem.GetBodyInterface().GetPositionAndRotation(record->bodyID, position, rotation); + + return PhysicsTransform{ + .position = FromJoltRVec3(position), + .rotation = FromJoltQuat(rotation) + }; + } + + void SetLinearVelocity(PhysicsBodyHandle body, const glm::vec3& velocity) + { + if (BodyRecord* record = FindBody(body)) + { + m_PhysicsSystem.GetBodyInterface().SetLinearVelocity(record->bodyID, ToJoltVec3(velocity)); + } + } + + glm::vec3 GetLinearVelocity(PhysicsBodyHandle body) const + { + const BodyRecord* record = FindBody(body); + if (!record) + { + return glm::vec3{0.0f}; + } + + return FromJoltVec3(m_PhysicsSystem.GetBodyInterface().GetLinearVelocity(record->bodyID)); + } + + void AddForce(PhysicsBodyHandle body, const glm::vec3& force) + { + if (BodyRecord* record = FindBody(body)) + { + m_PhysicsSystem.GetBodyInterface().AddForce(record->bodyID, ToJoltVec3(force)); + } + } + + void AddImpulse(PhysicsBodyHandle body, const glm::vec3& impulse) + { + if (BodyRecord* record = FindBody(body)) + { + m_PhysicsSystem.GetBodyInterface().AddImpulse(record->bodyID, ToJoltVec3(impulse)); + } + } + + bool Raycast(const glm::vec3& origin, + const glm::vec3& direction, + float maxDistance, + PhysicsRaycastHit& hit) const + { + if (maxDistance <= 0.0f || glm::length2(direction) <= 0.0000001f) + { + return false; + } + + const glm::vec3 directionNormalized = glm::normalize(direction); + + RRayCast ray{ + ToJoltRVec3(origin), + ToJoltVec3(directionNormalized * maxDistance) + }; + + RayCastResult result{}; + if (!m_PhysicsSystem.GetNarrowPhaseQuery().CastRay(ray, result)) + { + return false; + } + + hit.body = FindHandle(result.mBodyID); + hit.distance = result.mFraction * maxDistance; + hit.point = origin + directionNormalized * hit.distance; + hit.object = nullptr; + hit.normal = glm::vec3{0.0f, 1.0f, 0.0f}; + + BodyLockRead lock(m_PhysicsSystem.GetBodyLockInterface(), result.mBodyID); + if (lock.Succeeded()) + { + const Body& body = lock.GetBody(); + + hit.object = reinterpret_cast(body.GetUserData()); + hit.normal = FromJoltVec3( + body.GetWorldSpaceSurfaceNormal( + result.mSubShapeID2, + ray.GetPointOnRay(result.mFraction) + ) + ); + } + + return true; + } + +private: + struct BodyRecord + { + bool alive{false}; + BodyID bodyID{}; + PhysicsBodyDesc desc{}; + }; + + BodyRecord* FindBody(PhysicsBodyHandle body) + { + auto it = m_Bodies.find(body.id); + if (it == m_Bodies.end()) + { + return nullptr; + } + + return &it->second; + } + + const BodyRecord* FindBody(PhysicsBodyHandle body) const + { + auto it = m_Bodies.find(body.id); + if (it == m_Bodies.end()) + { + return nullptr; + } + + return &it->second; + } + + PhysicsBodyHandle FindHandle(BodyID bodyID) const + { + for (const auto& [handleId, record] : m_Bodies) + { + if (record.alive && record.bodyID == bodyID) + { + return PhysicsBodyHandle{handleId}; + } + } + + return {}; + } + + Settings m_Settings{}; + + BPLayerInterfaceImpl m_BPLayerInterface{}; + ObjectVsBroadPhaseLayerFilterImpl m_ObjectVsBroadPhaseLayerFilter{}; + ObjectLayerPairFilterImpl m_ObjectLayerPairFilter{}; + + PhysicsSystem m_PhysicsSystem{}; + + std::unique_ptr m_TempAllocator; + std::unique_ptr m_JobSystem; + + std::unordered_map m_Bodies; + std::uint32_t m_NextHandle{0}; +}; + +JoltPhysicsWorld::JoltPhysicsWorld(const Settings& settings) + : m_Impl(std::make_unique(settings)) +{ +} + +JoltPhysicsWorld::~JoltPhysicsWorld() = default; + +void JoltPhysicsWorld::Step(float fixedDt) +{ + m_Impl->Step(fixedDt); +} + +void JoltPhysicsWorld::SyncKinematicBodiesToPhysics() +{ + m_Impl->SyncKinematicBodiesToPhysics(); +} + +void JoltPhysicsWorld::SyncDynamicBodiesToTransforms() +{ + m_Impl->SyncDynamicBodiesToTransforms(); +} + +PhysicsBodyHandle JoltPhysicsWorld::CreateBody(const PhysicsBodyDesc& desc) +{ + return m_Impl->CreateBody(desc); +} + +void JoltPhysicsWorld::DestroyBody(PhysicsBodyHandle body) +{ + m_Impl->DestroyBody(body); +} + +void JoltPhysicsWorld::SetBodyTransform(PhysicsBodyHandle body, const PhysicsTransform& transform) +{ + m_Impl->SetBodyTransform(body, transform); +} + +PhysicsTransform JoltPhysicsWorld::GetBodyTransform(PhysicsBodyHandle body) const +{ + return m_Impl->GetBodyTransform(body); +} + +void JoltPhysicsWorld::SetLinearVelocity(PhysicsBodyHandle body, const glm::vec3& velocity) +{ + m_Impl->SetLinearVelocity(body, velocity); +} + +glm::vec3 JoltPhysicsWorld::GetLinearVelocity(PhysicsBodyHandle body) const +{ + return m_Impl->GetLinearVelocity(body); +} + +void JoltPhysicsWorld::AddForce(PhysicsBodyHandle body, const glm::vec3& force) +{ + m_Impl->AddForce(body, force); +} + +void JoltPhysicsWorld::AddImpulse(PhysicsBodyHandle body, const glm::vec3& impulse) +{ + m_Impl->AddImpulse(body, impulse); +} + +bool JoltPhysicsWorld::Raycast(const glm::vec3& origin, + const glm::vec3& direction, + float maxDistance, + PhysicsRaycastHit& hit) const +{ + return m_Impl->Raycast(origin, direction, maxDistance, hit); +} diff --git a/destrum/src/Physics/PhysicsSceneBridge.cpp b/destrum/src/Physics/PhysicsSceneBridge.cpp index 47ac84f..f189645 100644 --- a/destrum/src/Physics/PhysicsSceneBridge.cpp +++ b/destrum/src/Physics/PhysicsSceneBridge.cpp @@ -24,12 +24,7 @@ void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) { } void PhysicsSceneBridge::FixedUpdate(float fixedDt) { - if (auto* simple = dynamic_cast(m_World.get())) { - simple->SyncKinematicBodiesToPhysics(); - simple->Step(fixedDt); - simple->SyncDynamicBodiesToTransforms(); - return; - } - + m_World->SyncKinematicBodiesToPhysics(); m_World->Step(fixedDt); + m_World->SyncDynamicBodiesToTransforms(); } diff --git a/destrum/third_party/CMakeLists.txt b/destrum/third_party/CMakeLists.txt index 97ecf76..4f23f46 100644 --- a/destrum/third_party/CMakeLists.txt +++ b/destrum/third_party/CMakeLists.txt @@ -122,3 +122,10 @@ set(USE_F16C ON CACHE BOOL "" FORCE) set(USE_FMADD ON CACHE BOOL "" FORCE) add_subdirectory(jolt/Build) + +# 3rdparty/tracy is the Tracy repo +option(TRACY_ENABLE "" ON) +option(TRACY_ON_DEMAND "" ON) + +add_subdirectory(tracy) + diff --git a/destrum/third_party/assimp b/destrum/third_party/assimp index e13e0b5..818f73f 160000 --- a/destrum/third_party/assimp +++ b/destrum/third_party/assimp @@ -1 +1 @@ -Subproject commit e13e0b5b7da1d6d80b2ee12b043f1253a34d2ff9 +Subproject commit 818f73f99ab429928d5b5c406352a356939a420b diff --git a/destrum/third_party/fmt b/destrum/third_party/fmt index 7ad8004..588b3a0 160000 --- a/destrum/third_party/fmt +++ b/destrum/third_party/fmt @@ -1 +1 @@ -Subproject commit 7ad8004d575a7eb5aed7b5911c1ae298f7258052 +Subproject commit 588b3a0f8f6a8bcf2a959cae882d5b2703e86737 diff --git a/destrum/third_party/freetype b/destrum/third_party/freetype index 23b6cd2..25a08f2 160000 --- a/destrum/third_party/freetype +++ b/destrum/third_party/freetype @@ -1 +1 @@ -Subproject commit 23b6cd27ff19b70cbf98e058cd2cf0647d5284ff +Subproject commit 25a08f24cfc0da879d1938352d026532f280b77e diff --git a/destrum/third_party/glfw b/destrum/third_party/glfw index dbadda2..567b1ec 160000 --- a/destrum/third_party/glfw +++ b/destrum/third_party/glfw @@ -1 +1 @@ -Subproject commit dbadda26835ec5089ef922e6c290bcf58cf12056 +Subproject commit 567b1ec2442d59525e24c19e8d413df6baf02496 diff --git a/destrum/third_party/glm b/destrum/third_party/glm index 8f6213d..6f14f47 160000 --- a/destrum/third_party/glm +++ b/destrum/third_party/glm @@ -1 +1 @@ -Subproject commit 8f6213d379a904f5ae910e09a114e066e25faf57 +Subproject commit 6f14f4792a0cde5d0cf2c910506724d61cb95834 diff --git a/destrum/third_party/jolt b/destrum/third_party/jolt index 33a8797..f26e382 160000 --- a/destrum/third_party/jolt +++ b/destrum/third_party/jolt @@ -1 +1 @@ -Subproject commit 33a879752128c3740f8277301aefbab06bad76fb +Subproject commit f26e382dd06653d8bf9de71371c4ccd92ab667d1 diff --git a/destrum/third_party/json b/destrum/third_party/json index 30b2817..25c58ac 160000 --- a/destrum/third_party/json +++ b/destrum/third_party/json @@ -1 +1 @@ -Subproject commit 30b28175e46827153d8a51d75540ef888aa83d8b +Subproject commit 25c58ac6bd88ccd55084e341f7ecb6ef88d527af diff --git a/destrum/third_party/sdl b/destrum/third_party/sdl index 3eba0b6..b8b3f5e 160000 --- a/destrum/third_party/sdl +++ b/destrum/third_party/sdl @@ -1 +1 @@ -Subproject commit 3eba0b6f8a21392f47b1b53a476e7633048de9b1 +Subproject commit b8b3f5ef2001cbe7c11f62d41a9bf47d4a2d8b07 diff --git a/destrum/third_party/spdlog b/destrum/third_party/spdlog index 32dd298..f544fce 160000 --- a/destrum/third_party/spdlog +++ b/destrum/third_party/spdlog @@ -1 +1 @@ -Subproject commit 32dd298dc2d60fe0454e70b818d64941392c5b41 +Subproject commit f544fce006850a58063277068948dcda5681a27c diff --git a/destrum/third_party/tinyexr b/destrum/third_party/tinyexr index 90a147c..97b265d 160000 --- a/destrum/third_party/tinyexr +++ b/destrum/third_party/tinyexr @@ -1 +1 @@ -Subproject commit 90a147c7114af250c5ac58cb04950c46e509cb8a +Subproject commit 97b265d8ce5e7b6ed351592add2beeb601ac5463 diff --git a/destrum/third_party/tinygltf b/destrum/third_party/tinygltf index 81bd50c..a434ee0 160000 --- a/destrum/third_party/tinygltf +++ b/destrum/third_party/tinygltf @@ -1 +1 @@ -Subproject commit 81bd50c1062fdb956e878efa2a9234b2b9ec91ec +Subproject commit a434ee02066c2d9b62a3504876aed38e6e399fe0 diff --git a/destrum/third_party/tracy b/destrum/third_party/tracy new file mode 160000 index 0000000..05cceee --- /dev/null +++ b/destrum/third_party/tracy @@ -0,0 +1 @@ +Subproject commit 05cceee0df3b8d7c6fa87e9638af311dbabc63cb diff --git a/destrum/third_party/vk-bootstrap b/destrum/third_party/vk-bootstrap index 7028d6f..9539275 160000 --- a/destrum/third_party/vk-bootstrap +++ b/destrum/third_party/vk-bootstrap @@ -1 +1 @@ -Subproject commit 7028d6f652fdca9da332b4b4df25860ae19302fb +Subproject commit 95392756ec055c6887e5a4e9cdaae0456e900673 diff --git a/destrum/third_party/vma b/destrum/third_party/vma index e722e57..3aa9212 160000 --- a/destrum/third_party/vma +++ b/destrum/third_party/vma @@ -1 +1 @@ -Subproject commit e722e57c891a8fbe3cc73ca56c19dd76be242759 +Subproject commit 3aa921224c154a0d2c43912bc88e1c42ce1f7607 diff --git a/destrum/third_party/volk b/destrum/third_party/volk index bf84ce9..477a354 160000 --- a/destrum/third_party/volk +++ b/destrum/third_party/volk @@ -1 +1 @@ -Subproject commit bf84ce9573cffcaa0dae4e5f4d77e44e0dfb9d8b +Subproject commit 477a354c50c233ac95fb52c4250815d08f6a1571 diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index d8a3b87..0d9271f 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -23,13 +23,16 @@ #include "destrum/Util/ModelDocUtils.h" -LightKeeper::LightKeeper() : App(), renderer(meshCache, materialCache) { +LightKeeper::LightKeeper() : App(), renderer(meshCache, materialCache) +{ } -LightKeeper::~LightKeeper() { +LightKeeper::~LightKeeper() +{ } -void LightKeeper::customInit() { +void LightKeeper::customInit() +{ materialCache.init(gfxDevice); renderer.init(gfxDevice, m_params.renderSize); @@ -48,7 +51,7 @@ void LightKeeper::customInit() { ); ModelDocUtils::LogModelDocSummary(kittyModel, "kitty.glb"); - const auto &kittyPrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(kittyModel, "game://kitty.glb"); + const auto& kittyPrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(kittyModel, "game://kitty.glb"); testMesh = kittyPrimitive.mesh; testMesh.name = "Test Mesh"; @@ -75,40 +78,7 @@ void LightKeeper::customInit() { camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f))); - auto &scene = SceneManager::GetInstance().CreateScene("Main"); - - // auto testCube = std::make_shared("TestCube"); - // auto meshComp = testCube->AddComponent(); - // meshComp->SetMeshID(testMeshID); - // meshComp->SetMaterialID(testMaterialID); - const int count = 100; - const float radius = 5.0f; - - const float orbitRadius = 5.0f; - - for (int i = 0; i < count; ++i) { - // auto childCube = std::make_shared(fmt::format("ChildCube{}", i)); - // - // auto childMeshComp = childCube->AddComponent(); - // childMeshComp->SetMeshID(testMeshID); - // childMeshComp->SetMaterialID(testMaterialID); - // - // childCube->GetTransform().SetWorldScale(glm::vec3(0.1f)); - // - // // Add orbit + self spin - // auto orbit = childCube->AddComponent(orbitRadius, glm::vec3(0.0f)); - // orbit->Randomize(1337u + (uint32_t)i); // stable random per index - // - // scene.Add(childCube); - } - // testCube->AddComponent(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec - //rotate 180 around X axis - // testCube->GetTransform().SetLocalRotation(glm::quat(glm::vec3(glm::radians(180.0f), 0.0f, 0.0f))); - // - auto globeRoot = std::make_shared("GlobeRoot"); - globeRoot->GetTransform().SetWorldPosition(glm::vec3(0.0f)); - globeRoot->AddComponent(glm::vec3(0, 1, 0), 1.0f); // spin around Y, rad/sec - scene.Add(globeRoot); + auto& scene = SceneManager::GetInstance().CreateScene("Main"); // scene.Add(testCube); @@ -137,7 +107,7 @@ void LightKeeper::customInit() { ); ModelDocUtils::LogModelDocSummary(planeModel, "plane.glb"); - const auto &planePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(planeModel, "game://plane.glb"); + const auto& planePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(planeModel, "game://plane.glb"); const auto planeMeshID = meshCache.addMesh(gfxDevice, planePrimitive.mesh); const auto planeTexturePath = ModelDocUtils::PickTexturePath( @@ -160,6 +130,11 @@ void LightKeeper::customInit() { planeMeshComp->SetMaterialID(planeMaterialID); planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f)); planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f)); + planeObj->AddComponent(glm::vec3{10.0f, 0.5f, 10.0f}); + + auto* floorRb = planeObj->AddComponent(); + floorRb->SetType(RigidbodyType::Static); + scene.GetPhysics().RegisterGameObject(*planeObj); scene.Add(planeObj); const auto CharObj = std::make_shared("Character"); @@ -284,112 +259,167 @@ void LightKeeper::customInit() { // } + { + const auto CharObj = std::make_shared("Character"); + + ModelDoc::LoadOptions characterOptions{}; + characterOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; + characterOptions.loadMaterials = true; + characterOptions.loadSkeleton = true; + characterOptions.loadAnimations = false; + + auto charModel = ModelDoc::LoadModel( + AssetFS::GetInstance() + .GetFullPath("engine://characterMedium.fbx") + .generic_string(), + characterOptions + ); + + const auto &charPrimitive = + ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow( + charModel, + "engine://characterMedium.fbx" + ); + + const auto charMeshID = meshCache.addMesh(gfxDevice, charPrimitive.mesh); + + const auto charTextureID = gfxDevice.loadImageFromFile( + AssetFS::GetInstance().GetFullPath("engine://textures/criminalMaleA.png") + ); + + const auto charMaterialID = materialCache.addMaterial(gfxDevice, { + .baseColor = ModelDocUtils::GetImportedBaseColor( + charModel, charPrimitive), + .diffuseTexture = charTextureID, + .name = ModelDocUtils::GetImportedMaterialName( + charModel, + charPrimitive, + "CharacterMaterial" + ), + }); + + const auto charMeshComp = CharObj->AddComponent(); + charMeshComp->SetMeshID(charMeshID); + charMeshComp->SetMaterialID(charMaterialID); + + const auto animator = CharObj->AddComponent(); + animator->setSkeleton(charModel.skeleton); + + auto runClips = ModelDoc::LoadAnimationClips( + AssetFS::GetInstance() + .GetFullPath("engine://run.fbx") + .generic_string(), + charModel.skeleton + ); + + for (auto &clip: runClips) { + spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); + animator->addClip(std::make_shared(std::move(clip))); + } + + if (!runClips.empty()) { + animator->play("Root|Run"); + } + + CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f)); + CharObj->GetTransform().SetWorldPosition(glm::vec3(5, 0, 0)); + scene.Add(CharObj); + } + + auto cubeModel = ModelDoc::LoadModel( + AssetFS::GetInstance() + .GetFullPath("engine://cube.fbx") + .generic_string(), + staticModelOptions + ); + ModelDocUtils::LogModelDocSummary(cubeModel, "cube.fbx"); + + const auto& cubePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(cubeModel, "game://cube.fbx"); + + + const auto eliasTextuerPath = ModelDocUtils::PickTexturePath( + cubeModel, + cubePrimitive, + AssetFS::GetInstance().GetFullPath("game://grass.png") + ); + + const auto eliasTextueID = gfxDevice.loadImageFromFile(eliasTextuerPath); + const auto eliasMaterialID = materialCache.addMaterial(gfxDevice, { + .baseColor = ModelDocUtils::GetImportedBaseColor( + planeModel, planePrimitive), + .textureFilteringMode = + TextureFilteringMode::Anisotropic, + .diffuseTexture = eliasTextueID, + .name = ModelDocUtils::GetImportedMaterialName( + planeModel, planePrimitive, "GroundPlaneMaterial"), + }); + + const auto cubeMeshID = meshCache.addMesh(gfxDevice, cubePrimitive.mesh); + // + // for (int i{0}; i < 100; i++) // { - // const auto CharObj = std::make_shared("Character"); + // auto cube = std::make_shared("Cube"); // - // ModelDoc::LoadOptions characterOptions{}; - // characterOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; - // characterOptions.loadMaterials = true; - // characterOptions.loadSkeleton = true; - // characterOptions.loadAnimations = false; + // cube->AddComponent(glm::vec3{0.5f}); + // cube->AddComponent(); // - // auto charModel = ModelDoc::LoadModel( - // AssetFS::GetInstance() - // .GetFullPath("engine://characterMedium.fbx") - // .generic_string(), - // characterOptions - // ); + // auto meshComp = cube->AddComponent(); + // meshComp->SetMeshID(cubeMeshID); + // meshComp->SetMaterialID(eliasMaterialID); // - // const auto &charPrimitive = - // ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow( - // charModel, - // "engine://characterMedium.fbx" - // ); + // cube->GetTransform().SetWorldPosition(glm::vec3(0.0f, i, 0.0f)); + // cube->GetTransform().SetWorldScale(glm::vec3(0.005f)); // - // const auto charMeshID = meshCache.addMesh(gfxDevice, charPrimitive.mesh); - // - // const auto charTextureID = gfxDevice.loadImageFromFile( - // AssetFS::GetInstance().GetFullPath("engine://textures/criminalMaleA.png") - // ); - // - // const auto charMaterialID = materialCache.addMaterial(gfxDevice, { - // .baseColor = ModelDocUtils::GetImportedBaseColor( - // charModel, charPrimitive), - // .diffuseTexture = charTextureID, - // .name = ModelDocUtils::GetImportedMaterialName( - // charModel, - // charPrimitive, - // "CharacterMaterial" - // ), - // }); - // - // const auto charMeshComp = CharObj->AddComponent(); - // charMeshComp->SetMeshID(charMeshID); - // charMeshComp->SetMaterialID(charMaterialID); - // - // const auto animator = CharObj->AddComponent(); - // animator->setSkeleton(charModel.skeleton); - // - // auto runClips = ModelDoc::LoadAnimationClips( - // AssetFS::GetInstance() - // .GetFullPath("engine://run.fbx") - // .generic_string(), - // charModel.skeleton - // ); - // - // for (auto &clip: runClips) { - // spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); - // animator->addClip(std::make_shared(std::move(clip))); - // } - // - // if (!runClips.empty()) { - // animator->play("Root|Run"); - // } - // - // CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f)); - // CharObj->GetTransform().SetWorldPosition(glm::vec3(5, 0, 0)); - // scene.Add(CharObj); + // scene.Add(cube); + // scene.GetPhysics().RegisterGameObject(*cube); // } - { - auto cubeModel = ModelDoc::LoadModel( - AssetFS::GetInstance() - .GetFullPath("engine://cube.fbx") - .generic_string(), - staticModelOptions - ); - ModelDocUtils::LogModelDocSummary(cubeModel, "cube.fbx"); - - const auto &cubePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(cubeModel, "game://cube.fbx"); - const auto cubeMeshID = meshCache.addMesh(gfxDevice, cubePrimitive.mesh); - - auto cube = std::make_shared("Cube"); - - cube->AddComponent(glm::vec3{0.5f}); - cube->AddComponent(); - - auto meshComp = cube->AddComponent(); - meshComp->SetMeshID(cubeMeshID); - meshComp->SetMaterialID(charMaterialID); - - cube->GetTransform().SetWorldPosition(glm::vec3(0.0f, 2.0f, 0.0f)); - cube->GetTransform().SetWorldScale(glm::vec3(0.005f)); - - scene.Add(cube); - scene.GetPhysics().RegisterGameObject(*cube); - } + // const int cubeCount = 10; + // const float spacing = 1.0f; + // + // for (int x = 0; x < cubeCount; x++) + // { + // for (int y = 0; y < cubeCount; y++) + // { + // for (int z = 0; z < cubeCount; z++) + // { + // auto cube = std::make_shared("Cube"); + // + // cube->AddComponent(glm::vec3{0.5f}); + // cube->AddComponent(); + // + // auto meshComp = cube->AddComponent(); + // meshComp->SetMeshID(cubeMeshID); + // meshComp->SetMaterialID(eliasMaterialID); + // + // cube->GetTransform().SetWorldPosition(glm::vec3( + // (x - cubeCount / 2.0f) * spacing, + // y * spacing + 5, + // (z - cubeCount / 2.0f) * spacing + // )); + // + // cube->GetTransform().SetWorldScale(glm::vec3(0.005f)); + // + // scene.Add(cube); + // scene.GetPhysics().RegisterGameObject(*cube); + // } + // } + // } } -void LightKeeper::customUpdate(float dt) { +void LightKeeper::customUpdate(float dt) +{ camera.Update(dt); SceneManager::GetInstance().Update(); - if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_1)) { + if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_1)) + { renderer.setRenderWireframe(!renderer.getRenderWireframe()); } } -void LightKeeper::customDraw() { +void LightKeeper::customDraw() +{ renderer.beginDrawing(gfxDevice); const RenderContext ctx{ @@ -408,7 +438,7 @@ void LightKeeper::customDraw() { renderer.endDrawing(); const auto cmd = gfxDevice.beginFrame(); - const auto &drawImage = renderer.getDrawImage(gfxDevice); + const auto& drawImage = renderer.getDrawImage(gfxDevice); renderer.draw( cmd, gfxDevice, camera, GameRenderer::SceneData{ @@ -423,14 +453,16 @@ void LightKeeper::customDraw() { }); } -void LightKeeper::customCleanup() { +void LightKeeper::customCleanup() +{ auto device = gfxDevice.getDevice().device; vkDeviceWaitIdle(device); SceneManager::GetInstance().Destroy(); - if (skyboxCubemap) { + if (skyboxCubemap) + { skyboxCubemap.reset(); } @@ -445,7 +477,8 @@ void LightKeeper::customFixedUpdate(float dt) SceneManager::GetInstance().FixedUpdate(dt); } -void LightKeeper::onWindowResize(int newWidth, int newHeight) { +void LightKeeper::onWindowResize(int newWidth, int newHeight) +{ renderer.resize(gfxDevice, glm::ivec2{newWidth, newHeight}); const float aspectRatio = static_cast(newWidth) / static_cast(newHeight); camera.setAspectRatio(aspectRatio);