This commit is contained in:
2026-07-23 13:41:30 +08:00
parent fac7c86065
commit 3247a08684
6 changed files with 214 additions and 19 deletions
+156
View File
@@ -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)` 才能变成明确的帧率策略参数。
+57 -11
View File
@@ -275,7 +275,8 @@ RenderCache 是 RenderData 的私有成员,不参与三缓冲复制。
```text ```text
QImage image QImage image
WaterFallRingBuffer ringBuffer WaterFallRingBuffer ringBuffer
TimeAxis::mRingBuffer TimeAxis::Psc::StreamRingBuffer_ST buffer
AudioFrequent::Psc::StreamRingBuffer_ST buffer
SweepFrequent::mFrequents SweepFrequent::mFrequents
Planisphere::dataList Planisphere::dataList
Afterglow cachedPowerData / oldCachePowerData / mergedPowerData Afterglow cachedPowerData / oldCachePowerData / mergedPowerData
@@ -443,7 +444,50 @@ Input dirty 不递增 edit_state.version。
它保证流式输入不会导致 RenderState 全量复制。 它保证流式输入不会导致 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 Plot 仍保留 asio steady_timer
@@ -454,6 +498,8 @@ Plot::startRender(int refreshTimesPreSecond)
Plot::scheduleRenderTimer() Plot::scheduleRenderTimer()
``` ```
`mRenderTimer` 绑定到全局 RenderScheduler 的 asio::io_context。
当前 timer 的语义: 当前 timer 的语义:
```text ```text
@@ -481,7 +527,7 @@ timer 周期触发为辅
如果后续需要严格限帧,应该增加独立的 render trigger 策略,而不是把 startRender(int) 继续解释成硬性 FPS cap。 如果后续需要严格限帧,应该增加独立的 render trigger 策略,而不是把 startRender(int) 继续解释成硬性 FPS cap。
## 11. prepareData 边界 ## 12. prepareData 边界
prepareData 的标准流程: prepareData 的标准流程:
@@ -514,7 +560,7 @@ prepareData 不应该触发 QWidget 绘制。
prepareData 不应该把 RenderCache 复制回 State。 prepareData 不应该把 RenderCache 复制回 State。
## 12. draw 边界 ## 13. draw 边界
draw 的标准流程: draw 的标准流程:
@@ -532,7 +578,7 @@ draw 不修改 ready_state。
draw 不做输入队列交换。 draw 不做输入队列交换。
## 13. 数据分类规则 ## 14. 数据分类规则
判断一个字段应该放哪里: 判断一个字段应该放哪里:
@@ -560,7 +606,7 @@ draw 不做输入队列交换。
把配置项放 InputData。 把配置项放 InputData。
``` ```
## 14. 锁使用边界 ## 15. 锁使用边界
radio 渲染链路不允许使用 RenderData 对象级大锁。 radio 渲染链路不允许使用 RenderData 对象级大锁。
@@ -575,10 +621,10 @@ RenderPipeline:
使用 color buffer version / paintRequestPending / editStateVersion。 使用 color buffer version / paintRequestPending / editStateVersion。
只保护帧发布和 Qt update 请求合并。 只保护帧发布和 Qt update 请求合并。
Plot / TimerThread: Plot / RenderScheduler:
mRenderEnabled / mDestroying / mActiveRenderTasks / pending render size / paint buffer acquire mode 使用 atomic。 mRenderEnabled / mDestroying / mActiveRenderTasks / pending render size / paint buffer acquire mode 使用 atomic。
mRenderTaskMutex + mRenderTaskDone 只用于 Plot 析构等待已投递任务结束。 mRenderTaskMutex + mRenderTaskDone 只用于 Plot 析构等待已投递任务结束。
TimerThread::postAndWait 的局部 mutex + condition_variable 只用于跨线程同步 shutdown/removePlot。 RenderScheduler::postAndWait 的局部 mutex + condition_variable 只用于跨线程同步 shutdown/removePlot。
PerformanceShower: PerformanceShower:
mMutex 只保护统计 map 和显示列表。 mMutex 只保护统计 map 和显示列表。
@@ -606,7 +652,7 @@ prepareData 和 draw 的并发边界由调度流保证:
submitRender 进入 asio scheduler。 submitRender 进入 asio scheduler。
scheduler 串行推进 jobState。 scheduler 串行推进 jobState。
prepareData 在 scheduler 阶段消费 State/Input。 prepareData 在 scheduler 阶段消费 State/Input。
renderColor 在 CPU 阶段只读取 State_Render 和 RenderCache。 renderColor 在 asio thread_pool 阶段只读取 State_Render 和 RenderCache。
finishRender 回到 scheduler 发布 Color。 finishRender 回到 scheduler 发布 Color。
``` ```
@@ -628,7 +674,7 @@ spdlog / asio / googlepinyin:
这些同步不能向 RenderData、prepareData、draw 路径扩散。 这些同步不能向 RenderData、prepareData、draw 路径扩散。
## 15. 性能统计 ## 16. 性能统计
性能测试框不再维护自己的耗时平滑算法。 性能测试框不再维护自己的耗时平滑算法。
@@ -673,7 +719,7 @@ SRTT
RTTVAR RTTVAR
``` ```
## 16. 最终规则 ## 17. 最终规则
```text ```text
State 负责配置快照。 State 负责配置快照。
@@ -26,7 +26,6 @@ QWidget* IntermediateFrequencyWidget::createSpectrogramPowerPlot() {
void init() override { void init() override {
Plot::init(); Plot::init();
mObjectName = "SpectrumPlot"; mObjectName = "SpectrumPlot";
bindRenderThread(mObjectName + "Thread");
xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal)
.set_tickLength(-10) .set_tickLength(-10)
.set_coordRange(frequent_range) .set_coordRange(frequent_range)
@@ -92,7 +91,6 @@ QWidget* IntermediateFrequencyWidget::create_Afterglow_Plot(){
void init() override { void init() override {
Plot::init(); Plot::init();
mObjectName = "AfterglowPlot"; mObjectName = "AfterglowPlot";
bindRenderThread(mObjectName + "Thread");
xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal)
.set_coordRange(frequent_range) .set_coordRange(frequent_range)
.set_tickLength(-10) .set_tickLength(-10)
-4
View File
@@ -24,7 +24,6 @@ QWidget* RadioWidget::createSpectrogramPowerPlot() {
void init() override { void init() override {
Plot::init(); Plot::init();
mObjectName = "SpectrumPlot"; mObjectName = "SpectrumPlot";
bindRenderThread(mObjectName + "Thread");
xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal)
@@ -89,7 +88,6 @@ QWidget* RadioWidget::createWaterFallPlot() {
void init() override { void init() override {
Plot::init(); Plot::init();
mObjectName = "WaterFallPlot"; mObjectName = "WaterFallPlot";
bindRenderThread(mObjectName + "Thread");
xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal)
.set_tickLength(-10) .set_tickLength(-10)
.set_subTickLength(-5) .set_subTickLength(-5)
@@ -146,7 +144,6 @@ QWidget* RadioWidget::createAudioSpectrogramPlot() {
void init() override { void init() override {
Plot::init(); Plot::init();
mObjectName = "SpectrumPlot"; mObjectName = "SpectrumPlot";
bindRenderThread(mObjectName + "Thread");
xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal) xAxis = YSG::FrequentAxis::Builder(this, Qt::Horizontal)
@@ -210,7 +207,6 @@ QWidget* RadioWidget::createTimePowerPlot() {
void init() override { void init() override {
Plot::init(); Plot::init();
mObjectName = "AudioPlot"; mObjectName = "AudioPlot";
bindRenderThread(mObjectName + "Thread");
xAxis = YSG::TimeAxis::Builder(this, Qt::Horizontal) xAxis = YSG::TimeAxis::Builder(this, Qt::Horizontal)
.set_tickLength(-10) .set_tickLength(-10)
@@ -22,7 +22,6 @@ QWidget* SweepFrequencyWidget::createSpectrogramPowerPlot() {
void init() override { void init() override {
Plot::init(); Plot::init();
mObjectName = "AxisTestPlot"; mObjectName = "AxisTestPlot";
bindRenderThread(mObjectName + "Thread");
xAxis = YSG::Axis::Builder(this, Qt::Horizontal) xAxis = YSG::Axis::Builder(this, Qt::Horizontal)
+1 -1
View File
@@ -25,7 +25,7 @@ int main(int argc, char *argv[]) {
#endif #endif
MainWidget w; MainWidget w;
app.mMainWidget = &w; app.mMainWidget = &w;
YSG::startRenderScheduler();
w.show(); w.show();
YSG::startAllRenderThread();
return QApplication::exec(); return QApplication::exec();
} }