我的建议是：

```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
计算可以用定时器调度，刷新不要用定时器轮询；刷新应该由后台完成事件驱动，再由主线程事件循环合并执行。
```
