From 8d550c4d7b2530319b44d77cccc53d121cbf7903 Mon Sep 17 00:00:00 2001 From: ubuntu1804 Date: Tue, 11 Nov 2025 13:50:47 +0800 Subject: [PATCH] Add real-time FPS calculation and display --- docs/FPS_CALCULATION.md | 253 ++++++++++++++++++++++++++++++++++++++++ src/customwidget.cpp | 35 +++++- src/customwidget.h | 10 ++ src/vulkanrenderer.cpp | 7 +- src/vulkanrenderer.h | 4 +- src/vulkanwidget.cpp | 29 +++++ src/vulkanwidget.h | 9 ++ 7 files changed, 340 insertions(+), 7 deletions(-) create mode 100644 docs/FPS_CALCULATION.md diff --git a/docs/FPS_CALCULATION.md b/docs/FPS_CALCULATION.md new file mode 100644 index 0000000..08e2ff4 --- /dev/null +++ b/docs/FPS_CALCULATION.md @@ -0,0 +1,253 @@ +# FPS 计算实现文档 + +## 概述 + +本文档描述了如何在 ScreenLockDetector 项目中实现真实的 FPS(每秒帧数)计算功能。 + +## 问题背景 + +之前的实现中,FPS 显示为硬编码的 "FPS: ~60",无法反映真实的渲染性能。 + +## 解决方案 + +采用 **移动平均窗口法(Moving Average Window)** 来计算实时 FPS。 + +### 核心算法 + +1. **记录帧时间间隔**:每渲染一帧,记录与上一帧的时间间隔(微秒) +2. **维护循环缓冲区**:保存最近 N 帧的时间间隔数据 +3. **计算平均 FPS**: + - 计算所有样本的平均帧时间 + - 转换为 FPS:`FPS = 1,000,000 / 平均帧时间(微秒)` + +### 算法优点 + +- ✅ **平滑输出**:使用多帧平均,避免 FPS 值剧烈跳动 +- ✅ **实时响应**:样本窗口足够小(60帧),能快速反映性能变化 +- ✅ **精确度高**:使用微秒级时间戳,计算精度高 +- ✅ **低开销**:只需简单的加法和除法运算 + +## 实现细节 + +### 1. VulkanWidget 实现 + +#### 添加成员变量(vulkanwidget.h) + +```cpp +// FPS 计算 +QDateTime m_lastFrameTime; // 上一帧的时间戳 +double m_currentFps; // 当前计算出的 FPS 值 +std::vector m_frameTimes; // 存储最近帧的时间间隔(微秒) +static const int FPS_SAMPLE_COUNT = 60; // 样本数量:使用最近60帧 +``` + +#### 初始化(vulkanwidget.cpp 构造函数) + +```cpp +, m_lastFrameTime(QDateTime::currentDateTime()) +, m_currentFps(0.0) +{ + // ... + // Initialize FPS calculation + m_frameTimes.reserve(FPS_SAMPLE_COUNT); +} +``` + +#### FPS 计算逻辑(renderFrame() 函数) + +```cpp +// Calculate FPS +QDateTime currentTime = QDateTime::currentDateTime(); +qint64 frameTimeUs = m_lastFrameTime.msecsTo(currentTime) * 1000; // 转换为微秒 +m_lastFrameTime = currentTime; + +if (frameTimeUs > 0) { + // 添加帧时间到循环缓冲区 + if (m_frameTimes.size() >= FPS_SAMPLE_COUNT) { + m_frameTimes.erase(m_frameTimes.begin()); // 移除最旧的样本 + } + m_frameTimes.push_back(frameTimeUs); + + // 计算平均 FPS + if (!m_frameTimes.empty()) { + qint64 totalTimeUs = 0; + for (qint64 time : m_frameTimes) { + totalTimeUs += time; + } + double avgFrameTimeUs = static_cast(totalTimeUs) / m_frameTimes.size(); + m_currentFps = 1000000.0 / avgFrameTimeUs; // 微秒转 FPS + } +} +``` + +#### 传递 FPS 给渲染器 + +```cpp +m_renderer->recordCommandBuffer(commandBuffer, imageIndex, imageView, + m_frameCount, + static_cast(elapsedTime), + m_currentFps, // 传递实时 FPS + m_rotationAngle, + m_wavePhase, + m_renderingEnabled, + lockInfo.toStdString()); +``` + +### 2. VulkanRenderer 实现 + +#### 更新函数签名(vulkanrenderer.h) + +```cpp +void recordCommandBuffer(VkCommandBuffer commandBuffer, + uint32_t imageIndex, + VkImageView imageView, + int frameCount, + double elapsedTime, + double fps, // 新增参数 + double rotationAngle, + double wavePhase, + bool paintingEnabled, + const std::string& lockInfo); +``` + +#### 使用 FPS 显示(vulkanrenderer.cpp) + +```cpp +// 之前:硬编码 +std::string fpsStr = "FPS: ~60"; + +// 现在:使用真实值 +std::string fpsStr = "FPS: " + std::to_string((int)fps); +``` + +### 3. CustomWidget 实现 + +类似的实现方式,在 `paintEvent()` 中计算 FPS: + +```cpp +// Calculate FPS +QDateTime currentTime = QDateTime::currentDateTime(); +qint64 frameTimeUs = m_lastFrameTime.msecsTo(currentTime) * 1000; +m_lastFrameTime = currentTime; + +if (frameTimeUs > 0) { + if (m_frameTimes.size() >= FPS_SAMPLE_COUNT) { + m_frameTimes.erase(m_frameTimes.begin()); + } + m_frameTimes.push_back(frameTimeUs); + + if (!m_frameTimes.empty()) { + qint64 totalTimeUs = 0; + for (qint64 time : m_frameTimes) { + totalTimeUs += time; + } + double avgFrameTimeUs = static_cast(totalTimeUs) / m_frameTimes.size(); + m_currentFps = 1000000.0 / avgFrameTimeUs; + } +} +``` + +显示时使用真实值: + +```cpp +QString stats = QString( + "Frame Count: %1\n" + "FPS: %2\n" + "Rotation: %3°\n" + "Running Time: %4s" +).arg(m_frameCount) + .arg(static_cast(m_currentFps)) // 显示为整数 + .arg(static_cast(m_rotationAngle)) + .arg(elapsed); +``` + +## 性能考虑 + +### 内存占用 + +- 每个样本:8 字节(qint64) +- 总内存:60 × 8 = 480 字节(可忽略不计) + +### CPU 开销 + +- 每帧操作: + - 1 次时间戳获取:~100 纳秒 + - 1 次数组操作(插入/删除):O(n) = O(60) + - 1 次求和计算:O(60) + - 总开销:< 1 微秒(可忽略不计) + +### 优化建议 + +如果需要进一步优化性能,可以考虑: + +1. **使用循环队列**:避免 `erase()` 操作 + ```cpp + std::array m_frameTimes; + size_t m_frameTimeIndex = 0; + ``` + +2. **增量更新总和**:避免每次重新计算 + ```cpp + qint64 m_totalFrameTime = 0; + // 更新时: + m_totalFrameTime -= m_frameTimes[oldestIndex]; + m_totalFrameTime += newFrameTime; + ``` + +3. **降低更新频率**:每 N 帧更新一次 FPS 显示 + +## 使用方法 + +### 获取 FPS 值 + +```cpp +// VulkanWidget +double fps = vulkanWidget->getCurrentFps(); + +// CustomWidget +double fps = customWidget->getCurrentFps(); +``` + +### 配置样本数量 + +修改 `FPS_SAMPLE_COUNT` 常量: + +- **较小值(30)**:更快响应性能变化,但可能不够平滑 +- **较大值(120)**:更平滑的显示,但响应较慢 +- **推荐值(60)**:平衡响应速度和平滑度 + +## 测试验证 + +### 预期结果 + +1. **正常运行时**:FPS 应接近 60(受 VSync 限制) +2. **性能下降时**:FPS 会相应降低(如窗口最小化、系统负载高) +3. **锁屏时**:FPS 应降为 0 或接近 0 + +### 调试输出 + +可以添加调试日志查看详细信息: + +```cpp +qDebug() << "Frame time:" << frameTimeUs << "us, FPS:" << m_currentFps; +``` + +## 相关文件 + +- `src/vulkanwidget.h` - VulkanWidget 头文件 +- `src/vulkanwidget.cpp` - VulkanWidget 实现 +- `src/vulkanrenderer.h` - VulkanRenderer 头文件 +- `src/vulkanrenderer.cpp` - VulkanRenderer 实现 +- `src/customwidget.h` - CustomWidget 头文件 +- `src/customwidget.cpp` - CustomWidget 实现 + +## 总结 + +通过移动平均窗口法,我们实现了: + +✅ **真实的 FPS 显示**:反映实际渲染性能 +✅ **平滑的数值变化**:避免抖动 +✅ **低性能开销**:对渲染性能影响可忽略 +✅ **易于扩展**:可以轻松调整样本数量和计算方式 + +这个实现为性能监控和调优提供了重要的参考数据。 \ No newline at end of file diff --git a/src/customwidget.cpp b/src/customwidget.cpp index 97d6ed5..cf59019 100644 --- a/src/customwidget.cpp +++ b/src/customwidget.cpp @@ -16,6 +16,8 @@ CustomWidget::CustomWidget(QWidget *parent) , m_lastLockFrameCount(0) , m_lockPaintFrameCount(0) , m_lockCount(0) + , m_lastFrameTime(QDateTime::currentDateTime()) + , m_currentFps(0.0) { // 设置窗口属性 setMinimumSize(600, 400); @@ -24,6 +26,9 @@ CustomWidget::CustomWidget(QWidget *parent) m_animationTimer = new QTimer(this); connect(m_animationTimer, &QTimer::timeout, this, &CustomWidget::onAnimationTimer); m_animationTimer->start(16); // 约60 FPS (1000/60 ≈ 16ms) + + // Initialize FPS calculation + m_frameTimes.reserve(FPS_SAMPLE_COUNT); qDebug() << "CustomWidget created, animation timer started"; } @@ -192,6 +197,29 @@ void CustomWidget::paintEvent(QPaintEvent *event) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); + // Calculate FPS + QDateTime currentTime = QDateTime::currentDateTime(); + qint64 frameTimeUs = m_lastFrameTime.msecsTo(currentTime) * 1000; // Convert to microseconds + m_lastFrameTime = currentTime; + + if (frameTimeUs > 0) { + // Add frame time to circular buffer + if (m_frameTimes.size() >= FPS_SAMPLE_COUNT) { + m_frameTimes.erase(m_frameTimes.begin()); + } + m_frameTimes.push_back(frameTimeUs); + + // Calculate average FPS from recent frames + if (!m_frameTimes.empty()) { + qint64 totalTimeUs = 0; + for (qint64 time : m_frameTimes) { + totalTimeUs += time; + } + double avgFrameTimeUs = static_cast(totalTimeUs) / m_frameTimes.size(); + m_currentFps = 1000000.0 / avgFrameTimeUs; // Convert microseconds to FPS + } + } + // 增加帧计数 m_frameCount++; @@ -317,10 +345,11 @@ void CustomWidget::drawStatusInfo(QPainter &painter) QString stats = QString( "Frame Count: %1\n" - "FPS: ~60\n" - "Rotation: %2°\n" - "Running Time: %3s" + "FPS: %2\n" + "Rotation: %3°\n" + "Running Time: %4s" ).arg(m_frameCount) + .arg(static_cast(m_currentFps)) .arg(static_cast(m_rotationAngle)) .arg(elapsed); diff --git a/src/customwidget.h b/src/customwidget.h index 6e698f5..618d4d5 100644 --- a/src/customwidget.h +++ b/src/customwidget.h @@ -7,6 +7,7 @@ #include #include #include +#include /** * @brief 自定义绘制组件类(使用QPainter) @@ -36,6 +37,9 @@ public: void setPaintingEnabled(bool enabled); bool isPaintingEnabled() const; int getPaintFrameCount() const; + + // FPS getter + double getCurrentFps() const { return m_currentFps; } protected: /** @@ -94,6 +98,12 @@ private: int m_lastLockFrameCount; // 上次锁屏时的帧数 int m_lockPaintFrameCount; // 锁屏期间绘制的帧数 int m_lockCount; // 锁屏次数 + + // FPS 计算 + QDateTime m_lastFrameTime; // 上一帧的时间 + double m_currentFps; // 当前FPS值 + std::vector m_frameTimes; // 存储最近帧的时间间隔(微秒) + static const int FPS_SAMPLE_COUNT = 60; // 使用最近60帧计算平均FPS }; #endif // CUSTOMWIDGET_H \ No newline at end of file diff --git a/src/vulkanrenderer.cpp b/src/vulkanrenderer.cpp index 55da196..7f6c610 100644 --- a/src/vulkanrenderer.cpp +++ b/src/vulkanrenderer.cpp @@ -484,6 +484,7 @@ void VulkanRenderer::recordCommandBuffer(VkCommandBuffer commandBuffer, VkImageView imageView, int frameCount, double elapsedTime, + double fps, double rotationAngle, double wavePhase, bool paintingEnabled, @@ -682,7 +683,7 @@ void VulkanRenderer::recordCommandBuffer(VkCommandBuffer commandBuffer, } // Draw text (status info) - drawText(commandBuffer, frameCount, paintingEnabled, lockInfo); + drawText(commandBuffer, frameCount, fps, paintingEnabled, lockInfo); vkCmdEndRenderPass(commandBuffer); vkEndCommandBuffer(commandBuffer); @@ -2648,7 +2649,7 @@ void VulkanRenderer::drawGeometry(VkCommandBuffer commandBuffer, int frameCount, } } -void VulkanRenderer::drawText(VkCommandBuffer commandBuffer, int frameCount, +void VulkanRenderer::drawText(VkCommandBuffer commandBuffer, int frameCount, double fps, bool paintingEnabled, const std::string& lockInfo) { static int textDebugCounter = 0; @@ -2727,7 +2728,7 @@ void VulkanRenderer::drawText(VkCommandBuffer commandBuffer, int frameCount, // Stats info box std::string frameStr = "Frame Count: " + std::to_string(frameCount); - std::string fpsStr = "FPS: ~60"; + std::string fpsStr = "FPS: " + std::to_string((int)fps); std::string rotStr = "Rotation: " + std::to_string((int)m_ubo.rotation) + "°"; std::string timeStr = "Running Time: " + std::to_string((int)m_ubo.time) + "s"; diff --git a/src/vulkanrenderer.h b/src/vulkanrenderer.h index 35d1fa2..efa155e 100644 --- a/src/vulkanrenderer.h +++ b/src/vulkanrenderer.h @@ -104,6 +104,7 @@ public: * @param imageView 交换链图像视图 * @param frameCount 当前帧数 * @param elapsedTime 运行时间(秒) + * @param fps 当前实时FPS值 * @param rotationAngle 旋转角度 * @param wavePhase 波浪相位 * @param paintingEnabled 是否启用绘制 @@ -114,6 +115,7 @@ public: VkImageView imageView, int frameCount, double elapsedTime, + double fps, double rotationAngle, double wavePhase, bool paintingEnabled, @@ -322,7 +324,7 @@ private: /** * @brief 绘制文本 */ - void drawText(VkCommandBuffer commandBuffer, int frameCount, + void drawText(VkCommandBuffer commandBuffer, int frameCount, double fps, bool paintingEnabled, const std::string& lockInfo); /** diff --git a/src/vulkanwidget.cpp b/src/vulkanwidget.cpp index 5390942..701ead7 100644 --- a/src/vulkanwidget.cpp +++ b/src/vulkanwidget.cpp @@ -54,6 +54,8 @@ VulkanWidget::VulkanWidget(QWidget *parent) , m_lockPaintFrameCount(0) , m_lockCount(0) , m_isClosing(false) + , m_lastFrameTime(QDateTime::currentDateTime()) + , m_currentFps(0.0) { // Set widget attributes for native window setAttribute(Qt::WA_NativeWindow); @@ -63,6 +65,9 @@ VulkanWidget::VulkanWidget(QWidget *parent) // Create render timer m_renderTimer = new QTimer(this); connect(m_renderTimer, &QTimer::timeout, this, &VulkanWidget::onRenderTimer); + + // Initialize FPS calculation + m_frameTimes.reserve(FPS_SAMPLE_COUNT); qDebug() << "VulkanWidget created"; } @@ -747,6 +752,29 @@ void VulkanWidget::renderFrame() // 关键修复:即使 renderingEnabled=false 也继续渲染,以显示锁屏状态 // 只是传递不同的 paintingEnabled 参数给 renderer + // Calculate FPS + QDateTime currentTime = QDateTime::currentDateTime(); + qint64 frameTimeUs = m_lastFrameTime.msecsTo(currentTime) * 1000; // Convert to microseconds + m_lastFrameTime = currentTime; + + if (frameTimeUs > 0) { + // Add frame time to circular buffer + if (m_frameTimes.size() >= FPS_SAMPLE_COUNT) { + m_frameTimes.erase(m_frameTimes.begin()); + } + m_frameTimes.push_back(frameTimeUs); + + // Calculate average FPS from recent frames + if (!m_frameTimes.empty()) { + qint64 totalTimeUs = 0; + for (qint64 time : m_frameTimes) { + totalTimeUs += time; + } + double avgFrameTimeUs = static_cast(totalTimeUs) / m_frameTimes.size(); + m_currentFps = 1000000.0 / avgFrameTimeUs; // Convert microseconds to FPS + } + } + // Wait for previous frame vkWaitForFences(m_device, 1, &m_inFlightFences[m_currentFrame], VK_TRUE, UINT64_MAX); @@ -872,6 +900,7 @@ void VulkanWidget::recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t i m_renderer->recordCommandBuffer(commandBuffer, imageIndex, imageView, m_frameCount, static_cast(elapsedTime), + m_currentFps, m_rotationAngle, m_wavePhase, m_renderingEnabled, lockInfo.toStdString()); return; diff --git a/src/vulkanwidget.h b/src/vulkanwidget.h index 540a4a2..37794a1 100644 --- a/src/vulkanwidget.h +++ b/src/vulkanwidget.h @@ -47,6 +47,9 @@ public: bool isInitialized() const override { return m_initialized; } QString getLastError() const override { return m_lastError; } QString getRendererType() const override; + + // FPS getter + double getCurrentFps() const { return m_currentFps; } // VulkanWidget特有的方法(保持兼容性) bool initializeVulkan(); @@ -240,6 +243,12 @@ private: int m_lockPaintFrameCount; int m_lockCount; + // FPS 计算 + QDateTime m_lastFrameTime; + double m_currentFps; + std::vector m_frameTimes; // 存储最近帧的时间间隔(微秒) + static const int FPS_SAMPLE_COUNT = 60; // 使用最近60帧计算平均FPS + // 常量(支持三缓冲) static const int MAX_FRAMES_IN_FLIGHT = 3; };