From d8274e8bdc769f5a3106187c95e5d4a4ed7da44a Mon Sep 17 00:00:00 2001 From: atdunbg Date: Tue, 22 Sep 2026 20:33:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B8=A7=E7=8E=87=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=89=8B=E5=8A=A8=E9=99=90=E5=B8=A7=20+=20VS?= =?UTF-8?q?ync=20=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Application 帧率控制 API:SetLimitFps/SetTargetFps/SetVSync。 限帧目标最低 24fps(防止过低刷新导致交互卡死),无上限; vsync 开启时 present 自带节奏,手动限帧不生效 - 限帧策略:sleep 到剩 ~2ms 后自旋,精度高且不烧 CPU - Swapchain 加 VSync 开关:ChoosePresentMode 按开关选 Mailbox(低延迟垂直同步)/ Immediate(不限速可撕裂), FIFO 兜底;切换经 SetVSync → 重建 swapchain(复用 HandleResize) - Scene Debug 面板新增 Frame 区块(vsync/limit/目标滑条) - Run 循环新增 CPU 归属遥测(update/ui/render/total,每 120 帧) --- App/main.cpp | 45 ++++++++++++++++++----- Engine/include/Engine/Core/Application.h | 15 ++++++++ Engine/include/Engine/Renderer/Renderer.h | 4 ++ Engine/include/Engine/Vulkan/Swapchain.h | 6 +++ Engine/src/Core/Application.cpp | 28 ++++++++++++++ Engine/src/Renderer/Renderer.cpp | 7 ++++ Engine/src/Vulkan/Swapchain.cpp | 8 +++- 7 files changed, 102 insertions(+), 11 deletions(-) diff --git a/App/main.cpp b/App/main.cpp index fca69ea..fbe181d 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -24,7 +24,6 @@ #include -#include "Engine/ImGui/ImGuiLayer.h" using namespace Engine; using namespace Engine::ECS; @@ -66,17 +65,21 @@ protected: camera.camera.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f)); // 背包实体 × 3(悬浮在地面上方) - for (int i = 0; i < 3; ++i) { - auto e = scene.CreateEntity("Backpack" + std::to_string(i)); + auto size = 10; + for (int i = 0; i < size; ++i) { + for (int j = 0 ; j < size; j ++) + { + auto e = scene.CreateEntity("Backpack" + std::to_string(i)); - e.AddComponent() - .SetPosition(glm::vec3((i - 1) * 1.5f, 0.0f, 0.0f)) - .SetScale(glm::vec3(0.5f)) - .Compose(); + e.AddComponent() + .SetPosition(glm::vec3((i - size * 0.5) * 1.5f, 0.0f, (j - size * 0.5) * 1.5f)) + .SetScale(glm::vec3(0.5f)) + .Compose(); - e.AddComponent(MeshRendererComponent{backpack}); + e.AddComponent(MeshRendererComponent{backpack}); - m_Spinners.push_back(e); + m_Spinners.push_back(e); + } } // 灯光实体(第一个灯 = 阴影光源) @@ -102,7 +105,6 @@ protected: // 旋转动画:编辑 Transform 分量后 Compose 重算 world for (size_t i = 0; i < m_Spinners.size(); ++i) { auto& t = m_Spinners[i].GetComponent(); - t.position = glm::vec3((static_cast(i) - 1.0f) * 1.5f, 0.0f, 0.0f); t.rotation.y = m_Time * 60.0f + static_cast(i) * 40.0f; t.scale = glm::vec3(0.5f); t.Compose(); @@ -163,6 +165,29 @@ protected: ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]); } + // 帧率控制 + auto& app = *this; + ImGui::Separator(); + ImGui::Text("Frame"); + bool vsync = app.GetVSync(); + if (ImGui::Checkbox("VSync", &vsync)) { + app.SetVSync(vsync); + } + bool limit = app.GetLimitFps(); + if (ImGui::Checkbox("Limit FPS", &limit)) { + app.SetLimitFps(limit); + } + if (limit) { + if (vsync) { + ImGui::TextDisabled("(vsync 开启时限帧无效)"); + } else { + f32 target = app.GetTargetFps(); + if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f")) { + app.SetTargetFps(target); + } + } + } + ImGui::Separator(); ImGui::Text("fps: %.1f frame: %llu", 1.0f / GetDeltaTime(), diff --git a/Engine/include/Engine/Core/Application.h b/Engine/include/Engine/Core/Application.h index a2929f8..5f9a80d 100644 --- a/Engine/include/Engine/Core/Application.h +++ b/Engine/include/Engine/Core/Application.h @@ -54,6 +54,16 @@ namespace Engine { // 主渲染(ImGui pass 把 draw data 叠加到场景上)。 void EnableImGui(); + // ---- 帧率控制 ---- + // vsync 开启时 present 自带节奏,手动限帧不生效; + // 限帧目标最低 24fps(防止过低刷新导致交互卡死),无上限 + void SetVSync(bool vsync); + bool GetVSync() const { return m_VSync; } + void SetLimitFps(bool on) { m_LimitFps = on; } + bool GetLimitFps() const { return m_LimitFps; } + void SetTargetFps(f32 fps) { m_TargetFps = std::max(24.0f, fps); } + f32 GetTargetFps() const { return m_TargetFps; } + // ---- 供派生类访问的模块 ---- Window& GetWindow() const { return *m_Window; } Renderer& GetRenderer() const { return *m_Renderer; } @@ -108,6 +118,11 @@ namespace Engine { f32 m_DeltaTime = 0.0f; u64 m_FrameCount = 0; + // 帧率控制 + bool m_VSync = true; // 垂直同步(present 自带节奏) + bool m_LimitFps = false; // 手动限帧开关(vsync 开启时不生效) + f32 m_TargetFps = 60.0f; // 限帧目标(≥24) + std::chrono::steady_clock::time_point m_LastFrameTime; }; diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 35214da..c175d9d 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -174,6 +174,10 @@ namespace Engine { const std::vector& GetPassNames() const { return m_PassNames; } const std::vector& GetPassGpuMs() const { return m_PassGpuMs; } + // 垂直同步开关(Mailbox ↔ Immediate):切换会重建 swapchain, + // 只应在帧 UI 构建段调用(Render 之前) + void SetVSync(bool vsync); + private: vk::Extent2D m_Extent{}; diff --git a/Engine/include/Engine/Vulkan/Swapchain.h b/Engine/include/Engine/Vulkan/Swapchain.h index d468f78..daeb6e6 100644 --- a/Engine/include/Engine/Vulkan/Swapchain.h +++ b/Engine/include/Engine/Vulkan/Swapchain.h @@ -28,6 +28,11 @@ namespace Engine vk::SwapchainKHR Get() const { return m_Swapchain; } vk::Format GetFormat() const { return m_Format; } vk::Extent2D GetExtent() const { return m_Extent; } + + // 垂直同步开关:影响 present mode(Mailbox ↔ Immediate), + // 切换后需 Recreate 才生效 + void SetVSync(bool vsync) { m_VSync = vsync; } + bool GetVSync() const { return m_VSync; } uint32_t GetImageCount() const { return static_cast(m_Images.size()); } const std::vector& GetImages() const { return m_Images; } @@ -56,6 +61,7 @@ namespace Engine vk::Format m_Format; vk::Extent2D m_Extent; + bool m_VSync = true; }; } // namespace Engine diff --git a/Engine/src/Core/Application.cpp b/Engine/src/Core/Application.cpp index e2eef75..6ac666f 100644 --- a/Engine/src/Core/Application.cpp +++ b/Engine/src/Core/Application.cpp @@ -14,6 +14,7 @@ #include #include +#include namespace Engine { @@ -67,6 +68,13 @@ namespace Engine { [] { return ImGui::GetIO().WantCaptureMouse; }); } + void Application::SetVSync(bool vsync) + { + if (m_VSync == vsync) return; + m_VSync = vsync; + m_Renderer->SetVSync(vsync); + } + void Application::Run() { ENGINE_INFO("Application starting"); @@ -110,6 +118,26 @@ namespace Engine { // Present,draw data 在 ImGui pass 里才真正提交 GPU ---- m_Renderer->Render(*m_Scene, *m_Assets); + // ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ---- + // 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU + if (!m_VSync && m_LimitFps) { + const auto deadline = frameStart + + std::chrono::microseconds(static_cast( + 1e6 / m_TargetFps)); + auto now2 = std::chrono::steady_clock::now(); + if (now2 < deadline) { + const auto remaining = std::chrono::duration_cast< + std::chrono::milliseconds>(deadline - now2); + if (remaining.count() > 2) { + std::this_thread::sleep_for( + remaining - std::chrono::milliseconds(2)); + } + while (std::chrono::steady_clock::now() < deadline) { + // 自旋等待最后 ~2ms + } + } + } + // CPU 侧遥测(每 120 帧一行):定位帧耗时的 CPU 归属 const auto frameEnd = std::chrono::steady_clock::now(); static u32 cpuTelemetryCount = 0; diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 066210f..ce57579 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -754,6 +754,13 @@ namespace Engine m_RenderGraph->SetTargetExtent(m_Extent); // 触发首次 Compile } + void Renderer::SetVSync(bool vsync) + { + if (m_Swapchain->GetVSync() == vsync) return; + m_Swapchain->SetVSync(vsync); + HandleResize(); // present mode 变化需要重建 swapchain + } + void Renderer::HandleResize() { m_Device->GetDevice().waitIdle(); diff --git a/Engine/src/Vulkan/Swapchain.cpp b/Engine/src/Vulkan/Swapchain.cpp index 38e1291..e4639b1 100644 --- a/Engine/src/Vulkan/Swapchain.cpp +++ b/Engine/src/Vulkan/Swapchain.cpp @@ -151,9 +151,15 @@ namespace Engine vk::PresentModeKHR Swapchain::ChoosePresentMode(const std::vector& modes) { + // vsync 开:Mailbox(三缓冲垂直同步,低延迟);关:Immediate + //(不限速,可能撕裂)。FIFO 是所有实现必须支持的兜底 + const vk::PresentModeKHR preferred = m_VSync + ? vk::PresentModeKHR::eMailbox + : vk::PresentModeKHR::eImmediate; + for (const auto& m : modes) { - if (m == vk::PresentModeKHR::eMailbox) + if (m == preferred) { return m; }