Files
Radio/doc/ai2.md
T
2026-07-22 18:08:10 +08:00

342 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
我的建议是:
```text
不要靠主线程固定定时器轮询 update。
后台生成完新图后,提交一个刷新任务到 Qt 主事件循环。
如果你要限帧,再用单次 QTimer 做合并/节流。
```
也就是主线程刷新应该是 **事件驱动**,不是一直定时扫。
---
# 1. 不推荐固定主线程 timer 轮询 update
比如这种方式:
```text
主线程每 16ms 扫描所有图
如果 rendered_version > painted_version
update()
```
它能工作,但问题是:
```text
没有新图时也一直醒
图很多时每次都要扫一遍
后台刚生成完图,最多要等一个 timer 周期
主线程刷新逻辑和后台完成事件脱节
```
如果你有很多图,这种轮询会变成一种隐性开销。
尤其是图不可见、数据静止时,timer 仍然在跑,这不优雅。
---
# 2. 更推荐:后台发布 ready 后投递主线程刷新任务
后台线程生成完像素图:
```text
CPU 线程生成 working
发布到 ready
发现 rendered_version > requested_paint_version
投递一个 Qt 主线程任务
主线程里调用 update(region)
```
注意:后台线程不要直接调用 QWidget 的 `update()`。Qt GUI 对象应该在 GUI 线程使用。
应该用:
```cpp
QMetaObject::invokeMethod(widget, [this] {
widget->update(dirty_region);
}, Qt::QueuedConnection);
```
或者用 signal/slot,连接方式是 `Qt::QueuedConnection`
---
# 3. 但是要加 pending,避免疯狂投递
后台很多图同时完成时,不能每个图都投递一个主线程任务。
应该有一个全局 pending 标志:
```cpp
std::atomic_bool update_pending = false;
```
请求刷新:
```cpp
void Render_Scheduler::request_update(Chart_Node* chart) {
{
std::scoped_lock lock(dirty_lock);
dirty_region += chart->rect();
}
if (update_pending.exchange(true, std::memory_order_acq_rel)) {
return;
}
QMetaObject::invokeMethod(widget, [this] {
flush_update();
}, Qt::QueuedConnection);
}
```
主线程执行:
```cpp
void Render_Scheduler::flush_update() {
QRegion region;
{
std::scoped_lock lock(dirty_lock);
region = dirty_region;
dirty_region = QRegion();
}
update_pending.store(false, std::memory_order_release);
if (!region.isEmpty()) {
widget->update(region);
}
}
```
这样效果是:
```text
后台完成 100 个图
只投递 1 个主线程刷新任务
主线程合并 dirty region
调用一次 update(region)
```
这个比固定 timer 更合适。
---
# 4. Qt 的 update 本身也会合并
`update()` 不是马上 paint,它只是请求 Qt 稍后重绘。
所以:
```cpp
widget->update();
widget->update();
widget->update();
```
通常不会立刻触发三次 `paintEvent()`Qt 会合并。
但是你自己这层仍然要合并,因为:
```text
Qt 会合并 paint event
但你仍然可能投递了大量 QueuedConnection 任务
```
所以你自己的 `update_pending` 是为了减少主线程事件队列压力。
---
# 5. timer 应该用在哪里
timer 不是完全不用,而是用途要分清。
## 计算调度 timer:可以有
比如每 16ms / 33ms 扫描哪些图需要提交 CPU 任务:
```text
Render_Scheduler::tick()
扫描 meta_version
提交必要 CPU 任务
```
这个 timer 管的是:
```text
要不要开始后台计算
```
不是管:
```text
要不要 update
```
也就是说:
```text
计算调度可以定时
UI 刷新最好事件驱动
```
## 刷新节流 timer:可以用 single-shot
如果你想限制 UI 最多 60 FPS,可以不用立刻 `invokeMethod -> update()`,而是主线程启动一个单次 timer:
```text
后台 ready
投递主线程任务
主线程发现 flush_timer 没启动
启动 singleShot(16ms)
timer 到了再 update(region)
```
这样是 **节流 timer**,不是一直轮询 timer。
---
# 6. 最推荐的刷新模型
我建议你最终用这个:
```text
后台完成图像
publish ready
scheduler->request_update(chart)
合并 dirty region
如果没有 pending flush
invokeMethod 投递主线程 flush
主线程 flush_update()
widget->update(region)
Qt 稍后调用 paintEvent
paintEvent 中 ready -> front
drawImage(front)
```
这个模型里:
```text
没有新图,不投递刷新。
多个图完成,合并刷新。
主线程不轮询。
paintEvent 不计算。
```
---
# 7. 如果要限帧,用这个变体
```cpp
void Render_Scheduler::request_update(Chart_Node* chart) {
{
std::scoped_lock lock(dirty_lock);
dirty_region += chart->rect();
}
QMetaObject::invokeMethod(widget, [this] {
if (flush_timer.isActive()) {
return;
}
flush_timer.start(16);
}, Qt::QueuedConnection);
}
```
timer timeout
```cpp
void Render_Scheduler::flush_update() {
QRegion region;
{
std::scoped_lock lock(dirty_lock);
region = dirty_region;
dirty_region = QRegion();
}
if (!region.isEmpty()) {
widget->update(region);
}
}
```
这个语义是:
```text
后台可以随时完成图
但 UI 最多约 60Hz 请求 update
```
如果你想 30 FPS
```text
flush_timer.start(33)
```
如果你想尽快刷新:
```text
invokeMethod 之后直接 update(region)
```
---
# 8. 最终结论
你的主线程不需要启动一个固定 timer 去不停判断:
```text
rendered_version 是否变化
```
更合理的是:
```text
后台生成完新 ready 后,投递一个刷新任务到 Qt 主事件循环。
```
然后根据需求加节流:
```text
低延迟:QueuedConnection -> update(region)
限帧:QueuedConnection -> singleShot timer -> update(region)
```
所以最终分工是:
```text
后台线程:
生成图
发布 ready
请求调度器刷新
调度器:
合并 dirty region
合并 queued update 请求
可选限帧
主线程:
执行 update(region)
在 paintEvent 中消费 ready 并贴 front
```
一句话:
```text
计算可以用定时器调度,刷新不要用定时器轮询;刷新应该由后台完成事件驱动,再由主线程事件循环合并执行。
```