From 3247a0868425870ad23e839e0fe2e48444e967f3 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 23 Jul 2026 13:41:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/FAQ.md | 156 ++++++++++++++++++ doc/整体架构思路.md | 68 ++++++-- .../BackEnd/IntermediateFrequencyWidget.cpp | 2 - module/radio/BackEnd/RadioWidget.cpp | 4 - module/radio/BackEnd/SweepFrequencyWidget.cpp | 1 - module/radio/main.cpp | 2 +- 6 files changed, 214 insertions(+), 19 deletions(-) create mode 100644 doc/FAQ.md diff --git a/doc/FAQ.md b/doc/FAQ.md new file mode 100644 index 0000000..7a15ada --- /dev/null +++ b/doc/FAQ.md @@ -0,0 +1,156 @@ +# startRender(int fps) 作用流程 FAQ + +## startRender(int fps) 做了什么 + +`startRender(int refreshTimesPreSecond)` 只做两件事: + +```text +保存 mRefreshTimesPreSecond +调用 startRender() +``` + +对应代码流程: + +```text +Plot::startRender(int fps) + ↓ +d->mRefreshTimesPreSecond = fps + ↓ +Plot::startRender() +``` + +## startRender() 做了什么 + +`startRender()` 的作用是启动 Plot 的渲染调度。 + +流程: + +```text +检查 Plot 是否正在析构 + ↓ +mRenderEnabled = true + ↓ +投递任务到 asio scheduler + ↓ +取消旧 mRenderTimer + ↓ +scheduleRenderTimer() + ↓ +submitRender() +``` + +它会立即触发一次 `submitRender()`,不是等下一个 timer 周期。 + +## fps 参数现在是不是严格帧率上限 + +不是。 + +当前 `fps` 只决定 asio timer 的周期: + +```text +interval = round(1000.0 / fps) +``` + +timer 到期后: + +```text +submitRender() +scheduleRenderTimer() +``` + +所以它的真实语义是周期性 heartbeat / 兜底触发,不是硬性 FPS cap。 + +## 为什么不是严格限帧 + +因为当前渲染是事件驱动为主。 + +除了 timer 会触发 `submitRender()`,下面这些路径也会立即触发: + +```text +RenderAble::markStateDirty() + ↓ +Plot::markRenderStateDirty() + ↓ +requestRender() + ↓ +submitRender() +``` + +```text +RenderData::markInputDirty() + ↓ +RenderAble::markRenderDirty() + ↓ +requestRender() + ↓ +submitRender() +``` + +```text +finishRender() + ↓ +pipeline.hasNewState() + ↓ +submitRender() +``` + +只要 State/Input 有新版本,系统可以绕过 timer 周期继续推进下一帧。 + +## asio 里面还有没有定时器 + +有。 + +相关成员和函数: + +```text +PlotPrivate::mRenderTimer +PlotPrivate::mRefreshTimesPreSecond +Plot::scheduleRenderTimer() +RenderScheduler::initializePlotOnScheduler() +``` + +`RenderScheduler::initializePlotOnScheduler()` 为每个 Plot 创建 `asio::steady_timer`。 + +`scheduleRenderTimer()` 使用 `mRefreshTimesPreSecond` 计算间隔并注册 `async_wait`。 + +## timer 在当前架构里还有没有必要 + +当前仍然有必要保留。 + +原因: + +```text +保持 startRender(int) 旧调用语义 +保证没有显式 dirty 事件时仍有周期性检查 +作为异常丢事件后的兜底触发 +给性能测试框提供一个稳定的周期参考 +``` + +但它不是渲染正确性的唯一入口。 + +当前正确性主要依赖: + +```text +State dirty +Input dirty +finishRender 后续版本检查 +``` + +## 如果以后要严格限帧应该怎么做 + +应该新增独立调度策略,而不是继续复用 `startRender(int)` 的现有语义。 + +建议拆成: + +```text +Immediate: + dirty 后立即 requestRender() + +FixedRate: + dirty 后只标记 pending,由 timer 周期统一 submitRender() + +Throttle: + dirty 后允许立即渲染,但两帧之间必须满足最小间隔 +``` + +这样 `startRender(int)` 才能变成明确的帧率策略参数。 diff --git a/doc/整体架构思路.md b/doc/整体架构思路.md index 2554439..e6dc0a4 100644 --- a/doc/整体架构思路.md +++ b/doc/整体架构思路.md @@ -275,7 +275,8 @@ RenderCache 是 RenderData 的私有成员,不参与三缓冲复制。 ```text QImage image WaterFallRingBuffer ringBuffer -TimeAxis::mRingBuffer +TimeAxis::Psc::StreamRingBuffer_ST buffer +AudioFrequent::Psc::StreamRingBuffer_ST buffer SweepFrequent::mFrequents Planisphere::dataList Afterglow cachedPowerData / oldCachePowerData / mergedPowerData @@ -443,7 +444,50 @@ Input dirty 不递增 edit_state.version。 它保证流式输入不会导致 RenderState 全量复制。 -## 10. asio timer 和帧率 +## 10. RenderScheduler + +渲染调度使用一个全局 RenderScheduler。 + +RenderScheduler 由两部分组成: + +```text +单线程 asio::io_context +asio::thread_pool +``` + +单线程 asio::io_context 负责: + +```text +Plot 注册/移除 +resize 状态写入 +timer async_wait +submitRender +prepareData +finishRender +jobState 推进 +Color 发布 +``` + +asio::thread_pool 负责: + +```text +renderColor +draw +CPU 像素绘制 +``` + +不允许再引入: + +```text +TimerThread +按 Plot 名称绑定渲染线程 +业务层 bindRenderThread +多个 QThread scheduler +``` + +所有 Plot 共用一个 scheduler。 + +## 11. asio timer 和帧率 Plot 仍保留 asio steady_timer: @@ -454,6 +498,8 @@ Plot::startRender(int refreshTimesPreSecond) Plot::scheduleRenderTimer() ``` +`mRenderTimer` 绑定到全局 RenderScheduler 的 asio::io_context。 + 当前 timer 的语义: ```text @@ -481,7 +527,7 @@ timer 周期触发为辅 如果后续需要严格限帧,应该增加独立的 render trigger 策略,而不是把 startRender(int) 继续解释成硬性 FPS cap。 -## 11. prepareData 边界 +## 12. prepareData 边界 prepareData 的标准流程: @@ -514,7 +560,7 @@ prepareData 不应该触发 QWidget 绘制。 prepareData 不应该把 RenderCache 复制回 State。 -## 12. draw 边界 +## 13. draw 边界 draw 的标准流程: @@ -532,7 +578,7 @@ draw 不修改 ready_state。 draw 不做输入队列交换。 -## 13. 数据分类规则 +## 14. 数据分类规则 判断一个字段应该放哪里: @@ -560,7 +606,7 @@ draw 不做输入队列交换。 把配置项放 InputData。 ``` -## 14. 锁使用边界 +## 15. 锁使用边界 radio 渲染链路不允许使用 RenderData 对象级大锁。 @@ -575,10 +621,10 @@ RenderPipeline: 使用 color buffer version / paintRequestPending / editStateVersion。 只保护帧发布和 Qt update 请求合并。 -Plot / TimerThread: +Plot / RenderScheduler: mRenderEnabled / mDestroying / mActiveRenderTasks / pending render size / paint buffer acquire mode 使用 atomic。 mRenderTaskMutex + mRenderTaskDone 只用于 Plot 析构等待已投递任务结束。 - TimerThread::postAndWait 的局部 mutex + condition_variable 只用于跨线程同步 shutdown/removePlot。 + RenderScheduler::postAndWait 的局部 mutex + condition_variable 只用于跨线程同步 shutdown/removePlot。 PerformanceShower: mMutex 只保护统计 map 和显示列表。 @@ -606,7 +652,7 @@ prepareData 和 draw 的并发边界由调度流保证: submitRender 进入 asio scheduler。 scheduler 串行推进 jobState。 prepareData 在 scheduler 阶段消费 State/Input。 -renderColor 在 CPU 阶段只读取 State_Render 和 RenderCache。 +renderColor 在 asio thread_pool 阶段只读取 State_Render 和 RenderCache。 finishRender 回到 scheduler 发布 Color。 ``` @@ -628,7 +674,7 @@ spdlog / asio / googlepinyin: 这些同步不能向 RenderData、prepareData、draw 路径扩散。 -## 15. 性能统计 +## 16. 性能统计 性能测试框不再维护自己的耗时平滑算法。 @@ -673,7 +719,7 @@ SRTT RTTVAR ``` -## 16. 最终规则 +## 17. 最终规则 ```text State 负责配置快照。 diff --git a/module/radio/BackEnd/IntermediateFrequencyWidget.cpp b/module/radio/BackEnd/IntermediateFrequencyWidget.cpp index 472b3aa..5550c43 100644 --- a/module/radio/BackEnd/IntermediateFrequencyWidget.cpp +++ b/module/radio/BackEnd/IntermediateFrequencyWidget.cpp @@ -26,7 +26,6 @@ QWidget* IntermediateFrequencyWidget::createSpectrogramPowerPlot() { void init() override { Plot::init(); mObjectName = "SpectrumPlot"; - bindRenderThread(mObjectName + "Thread"); xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) .set_tickLength(-10) .set_coordRange(frequent_range) @@ -92,7 +91,6 @@ QWidget* IntermediateFrequencyWidget::create_Afterglow_Plot(){ void init() override { Plot::init(); mObjectName = "AfterglowPlot"; - bindRenderThread(mObjectName + "Thread"); xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) .set_coordRange(frequent_range) .set_tickLength(-10) diff --git a/module/radio/BackEnd/RadioWidget.cpp b/module/radio/BackEnd/RadioWidget.cpp index 4393122..2a6b22b 100644 --- a/module/radio/BackEnd/RadioWidget.cpp +++ b/module/radio/BackEnd/RadioWidget.cpp @@ -24,7 +24,6 @@ QWidget* RadioWidget::createSpectrogramPowerPlot() { void init() override { Plot::init(); mObjectName = "SpectrumPlot"; - bindRenderThread(mObjectName + "Thread"); xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) @@ -89,7 +88,6 @@ QWidget* RadioWidget::createWaterFallPlot() { void init() override { Plot::init(); mObjectName = "WaterFallPlot"; - bindRenderThread(mObjectName + "Thread"); xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) .set_tickLength(-10) .set_subTickLength(-5) @@ -146,7 +144,6 @@ QWidget* RadioWidget::createAudioSpectrogramPlot() { void init() override { Plot::init(); mObjectName = "SpectrumPlot"; - bindRenderThread(mObjectName + "Thread"); xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) @@ -210,7 +207,6 @@ QWidget* RadioWidget::createTimePowerPlot() { void init() override { Plot::init(); mObjectName = "AudioPlot"; - bindRenderThread(mObjectName + "Thread"); xAxis = YSG::TimeAxis::Builder(this, Qt::Horizontal) .set_tickLength(-10) diff --git a/module/radio/BackEnd/SweepFrequencyWidget.cpp b/module/radio/BackEnd/SweepFrequencyWidget.cpp index 82ce6bc..605493d 100644 --- a/module/radio/BackEnd/SweepFrequencyWidget.cpp +++ b/module/radio/BackEnd/SweepFrequencyWidget.cpp @@ -22,7 +22,6 @@ QWidget* SweepFrequencyWidget::createSpectrogramPowerPlot() { void init() override { Plot::init(); mObjectName = "AxisTestPlot"; - bindRenderThread(mObjectName + "Thread"); xAxis = YSG::Axis::Builder(this, Qt::Horizontal) diff --git a/module/radio/main.cpp b/module/radio/main.cpp index 0bf3686..874b47e 100644 --- a/module/radio/main.cpp +++ b/module/radio/main.cpp @@ -25,7 +25,7 @@ int main(int argc, char *argv[]) { #endif MainWidget w; app.mMainWidget = &w; + YSG::startRenderScheduler(); w.show(); - YSG::startAllRenderThread(); return QApplication::exec(); }