feat: 帧率控制——手动限帧 + VSync 开关

- 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 帧)
This commit is contained in:
2026-09-22 20:33:10 +08:00
parent cef32ea1ba
commit d8274e8bdc
7 changed files with 102 additions and 11 deletions
+35 -10
View File
@@ -24,7 +24,6 @@
#include <GLFW/glfw3.h>
#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<TransformComponent>()
.SetPosition(glm::vec3((i - 1) * 1.5f, 0.0f, 0.0f))
.SetScale(glm::vec3(0.5f))
.Compose();
e.AddComponent<TransformComponent>()
.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>(MeshRendererComponent{backpack});
e.AddComponent<MeshRendererComponent>(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<TransformComponent>();
t.position = glm::vec3((static_cast<f32>(i) - 1.0f) * 1.5f, 0.0f, 0.0f);
t.rotation.y = m_Time * 60.0f + static_cast<f32>(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(),
+15
View File
@@ -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;
};
@@ -174,6 +174,10 @@ namespace Engine {
const std::vector<std::string>& GetPassNames() const { return m_PassNames; }
const std::vector<f32>& GetPassGpuMs() const { return m_PassGpuMs; }
// 垂直同步开关(Mailbox ↔ Immediate):切换会重建 swapchain,
// 只应在帧 UI 构建段调用(Render 之前)
void SetVSync(bool vsync);
private:
vk::Extent2D m_Extent{};
+6
View File
@@ -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<u32>(m_Images.size()); }
const std::vector<vk::Image>& 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
+28
View File
@@ -14,6 +14,7 @@
#include <imgui.h>
#include <algorithm>
#include <thread>
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<long long>(
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;
+7
View File
@@ -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();
+7 -1
View File
@@ -151,9 +151,15 @@ namespace Engine
vk::PresentModeKHR Swapchain::ChoosePresentMode(const std::vector<vk::PresentModeKHR>& 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;
}