下面这个就是我建议你这个 **多图表后台渲染库**的完整模型。

核心目标是：

```text
元数据变化才计算
后台只算最新状态
主线程只贴图
多个图统一调度
不堆旧任务
不显示半成品
不因为 paintEvent 无意义重算
```

---

# 1. 总体管线

每个图都是这个流程：

```text
主线程修改元数据
    ↓
meta_version++
    ↓
调度器定时扫描
    ↓
判断这个图是否需要提交 CPU 任务
    ↓
生成元数据快照 Render_Input
    ↓
扔进 CPU 线程池
    ↓
CPU 线程生成 Pixel_Frame
    ↓
发布到三缓冲 ready
    ↓
主线程 update 合并
    ↓
paintEvent 消费 ready，贴 front
```

也就是：

```text
Meta -> Render_Input -> CPU Render -> Pixel_Frame -> paintEvent drawImage
```

每种具体图只需要实现：

```text
元数据 -> 像素图
```

线程控制、版本控制、三缓冲、刷新合并都放到通用框架里。

---

# 2. 每个图不要自己开线程

一堆图不能每个图一个线程、一个 timer。

应该是：

```text
一个 Render_Scheduler
一个 CPU Thread_Pool
多个 Chart_Node
```

结构类似：

```text
Render_Scheduler
    Chart_Node 1
    Chart_Node 2
    Chart_Node 3
    ...
Thread_Pool
```

调度器负责：

```text
扫描哪些图需要重算
限制每轮提交数量
限制单图提交频率
跳过不可见图
合并 update 区域
控制 CPU 线程池压力
```

具体图只负责：

```text
准备元数据
执行 CPU 绘制算法
提供 dirty rect / 绘制区域
```

---

# 3. 每个图的版本号

每个图建议维护这些版本：

```cpp
struct Chart_Versions {
    std::uint64_t meta = 0;
    std::uint64_t submitted = 0;
    std::uint64_t running = 0;
    std::uint64_t rendered = 0;
    std::uint64_t requested_paint = 0;
    std::uint64_t painted = 0;
};
```

语义：

```text
meta
    主线程最新元数据版本。

submitted
    已经提交给 CPU 线程池的版本。

running
    当前 CPU 正在计算的版本。

rendered
    后台已经生成像素图的版本。

requested_paint
    已经请求主线程刷新的版本。

painted
    paintEvent 已经贴出去的版本。
```

这里不要用 `OK`、`imageReady` 这种 bool。

bool 只能表示：

```text
有 / 没有
```

版本号能表示：

```text
是哪一版
新不新
有没有提交
有没有生成
有没有请求 update
有没有真正 paint
```

---

# 4. 元数据层：双缓冲或者快照

元数据是用户操作和数据输入修改的东西，比如：

```text
坐标范围
颜色表
缩放
图尺寸
数据源
采样参数
显示开关
曲线样式
```

主线程修改元数据：

```cpp
void Chart_Node::mark_meta_changed() {
    ++versions.meta;
}
```

调度器提交任务时生成快照：

```cpp
Render_Input Chart_Node::make_render_input() {
    Render_Input input;
    input.meta = edit_meta;
    input.version = versions.meta;
    return input;
}
```

如果元数据很小，直接拷贝快照。

如果元数据很大，用双缓冲或者共享不可变快照。

这一层一般不需要三缓冲。

原因是元数据中间版本可以丢，只要后台下一轮拿最新版本即可。

---

# 5. 任务层：single-flight

每个图同一时间最多一个后台任务。

不要这样：

```text
version 100 提交
version 101 提交
version 102 提交
version 103 提交
```

这样会造成旧任务堵新任务。

应该这样：

```text
一个图最多一个任务在飞
任务运行期间 meta 变了，只记录版本变化
任务完成后调度器下一轮再提交最新版本
```

任务状态：

```cpp
enum class Render_Job_State {
    Idle,
    Queued,
    Running
};
```

提交条件：

```cpp
bool Chart_Node::need_submit(std::uint64_t now_ms) const {
    if (!visible) {
        return false;
    }
    if (!valid_size) {
        return false;
    }
    if (job_state != Render_Job_State::Idle) {
        return false;
    }
    if (versions.meta == versions.submitted) {
        return false;
    }
    if (now_ms - last_submit_time < min_render_interval) {
        return false;
    }
    return true;
}
```

核心原则：

```text
版本没变，不算。
图不可见，不算。
尺寸无效，不算。
已经有任务在跑，不算。
刚算过，间隔太短，不算。
```

---

# 6. 像素结果层：三缓冲

像素层建议三缓冲：

```cpp
struct Pixel_Frame {
    std::uint64_t version = 0;
    QImage image;
};
struct Pixel_Buffers {
    Pixel_Frame front;
    Pixel_Frame ready;
    Pixel_Frame working;
};
```

三张图语义：

```text
front
    主线程 paintEvent 当前稳定显示的图。

ready
    后台已经生成好，等待主线程消费的图。

working
    后台线程正在写入的图。
```

三缓冲解决的是：

```text
主线程读 front
后台写 working
ready 作为交接区
```

它不是为了替代版本号，而是为了控制图像内存所有权。

---

# 7. 后台任务完成后的发布规则

后台线程生成完 `working` 后，不直接碰 `front`。

只发布到 `ready`。

如果 `ready` 还没被主线程消费，新 `working` 又生成完了：

```text
旧 ready 丢掉
新 working 覆盖 ready
front 继续稳定显示
```

图表库不需要按帧播放旧图，应该追最新状态。

发布逻辑：

```cpp
void Chart_Node::publish_working_frame(Pixel_Frame frame) {
    bool need_update = false;
    {
        std::scoped_lock lock(pixel_lock);
        if (frame.version <= pixels.ready.version) {
            return;
        }
        pixels.working = std::move(frame);
        pixels.ready.swap(pixels.working);
        versions.rendered = pixels.ready.version;
        if (versions.rendered != versions.requested_paint) {
            versions.requested_paint = versions.rendered;
            need_update = true;
        }
        job_state = Render_Job_State::Idle;
    }
    if (need_update) {
        scheduler->request_update(this);
    }
}
```

状态变化示例：

```text
发布前：
front   = 100
ready   = 101
working = 102

发布后：
front   = 100
ready   = 102
working = 101
```

旧 `ready = 101` 被丢掉，变成新的空闲 working 槽，后面会被覆盖。

---

# 8. 已经开始生成的 working 要不要停

不要停。

规则是：

```text
已经开始生成的 working：继续生成完。
还没开始的新任务：由调度器根据版本、间隔、状态决定是否提交。
```

原因：

```text
CPU 任务中途取消复杂。
生成完的新版本可以覆盖旧 ready。
single-flight 已经保证不会无限堆任务。
```

所以正确策略是：

```text
working 已经开始：继续。
ready 没消费：不等。
新 working 完成：覆盖旧 ready。
下一轮是否继续算：由调度器判断。
```

---

# 9. 主线程 paintEvent 消费规则

主线程 `paintEvent()` 前先尝试消费 `ready`：

```cpp
void Chart_Node::prepare_paint() {
    std::scoped_lock lock(pixel_lock);
    if (pixels.ready.version <= pixels.front.version) {
        return;
    }
    pixels.front.swap(pixels.ready);
    versions.painted = pixels.front.version;
}
```

然后只贴 `front`：

```cpp
void Chart_Node::paint(QPainter& painter) {
    prepare_paint();
    painter.drawImage(rect, pixels.front.image);
}
```

`paintEvent()` 不做：

```text
元数据计算
颜色映射
衰减计算
坐标采样
大数组遍历
后台等待
```

它只做：

```text
ready -> front
drawImage(front)
```

如果没有新图，也照样画旧 `front`。

因为 Qt 可能因为窗口遮挡、缩放、系统刷新等原因主动触发 `paintEvent()`，不能假设每次 paint 都有新数据。

---

# 10. update 合并

后台发布新图以后，不要每个图都直接 `update()` 整个窗口。

应该由调度器合并刷新区域：

```text
图 A 有新图
图 B 有新图
图 C 有新图
    ↓
合并 dirty rect
    ↓
主线程 update(total_dirty_rect)
```

可以加一个 pending 标志，避免重复投递刷新请求：

```cpp
void Render_Scheduler::request_update(Chart_Node* chart) {
    dirty_region += chart->rect();
    if (paint_request_pending.exchange(true)) {
        return;
    }
    QMetaObject::invokeMethod(widget, [this] {
        paint_request_pending.store(false);
        QWidget* w = widget;
        QRegion region = std::move(dirty_region);
        dirty_region = QRegion();
        w->update(region);
    }, Qt::QueuedConnection);
}
```

这样多个图连续完成渲染，也只触发一次合并后的主线程刷新。

---

# 11. 调度器 tick 流程

调度器定时扫描所有图：

```cpp
void Render_Scheduler::tick() {
    std::uint64_t now_ms = current_time_ms();
    int submitted_count = 0;
    for (Chart_Node* chart : charts) {
        if (submitted_count >= max_submit_per_tick) {
            break;
        }
        if (!chart->need_submit(now_ms)) {
            continue;
        }
        chart->submit_render(thread_pool, now_ms);
        ++submitted_count;
    }
}
```

提交任务：

```cpp
void Chart_Node::submit_render(Thread_Pool& pool, std::uint64_t now_ms) {
    Render_Input input;
    {
        std::scoped_lock lock(meta_lock);
        input = make_render_input();
        versions.submitted = input.version;
        versions.running = input.version;
        job_state = Render_Job_State::Queued;
        last_submit_time = now_ms;
    }
    pool.enqueue([this, input = std::move(input)]() mutable {
        run_render_task(std::move(input));
    });
}
```

CPU 任务：

```cpp
void Chart_Node::run_render_task(Render_Input input) {
    {
        std::scoped_lock lock(job_lock);
        job_state = Render_Job_State::Running;
    }
    Pixel_Frame frame;
    frame.version = input.version;
    frame.image = render_image(input);
    publish_working_frame(std::move(frame));
}
```

---

# 12. 过期任务结果怎么处理

假设：

```text
提交 version 100
CPU 正在算
用户已经改到 version 110
version 100 算完
```

推荐默认策略：

```text
允许发布 version 100
但任务完成后，如果 meta 还是新于 submitted，下一轮马上提交最新 version 110
```

这样用户不会长时间看不到任何结果。

但是不排队算：

```text
101、102、103、104...
```

只算当前正在跑的 100，然后下一轮追最新的 110。

也就是：

```text
旧结果可以短暂显示
旧任务不排队
新任务追最新
```

如果某些图对实时一致性要求特别高，也可以配置成：

```text
任务完成时发现 version < meta_version，直接丢弃结果
```

但默认不建议这么激进，否则用户拖动时可能一直看不到更新。

---

# 13. 最终状态机

每个图的任务状态：

```text
Idle
    ↓ submit
Queued
    ↓ thread starts
Running
    ↓ publish
Idle
```

版本流向：

```text
meta_version
    ↓ submit
submitted_version
    ↓ run
running_version
    ↓ publish
rendered_version
    ↓ request update
requested_paint_version
    ↓ paintEvent
painted_version
```

像素缓冲流向：

```text
CPU writes working
    ↓ publish
working <-> ready
    ↓ paintEvent
ready <-> front
    ↓ drawImage(front)
```

如果 ready 没消费，新 working 又来了：

```text
working <-> ready
旧 ready 丢掉
front 不动
```

如果 meta 没变：

```text
不提交任务
不生成图
不 request update
```

如果没有新 ready：

```text
paintEvent 只画旧 front
```

---

# 14. 最终设计原则

整个模型可以定成这几条：

```text
1. 每个图有自己的元数据版本。
2. 每个图同一时间最多一个 CPU 渲染任务。
3. 调度器统一扫描所有图，不让每个图自建线程和 timer。
4. 元数据层用快照或者双缓冲。
5. 像素结果层用三缓冲。
6. 三缓冲只管图像内存所有权，不替代版本号。
7. ready 只保存最新待显示图，不做帧队列。
8. 旧 ready 没被消费时，新 working 直接覆盖它。
9. 已经开始的 working 继续算完。
10. 是否继续提交新任务由版本号、single-flight、限速、可见性共同决定。
11. 主线程 paintEvent 只贴 front，不做重计算。
12. update 由调度器合并，不让每个图疯狂 update。
```

一句话总结：

```text
元数据版本号决定要不要算；
single-flight 决定不要堆任务；
CPU 线程池负责把 Meta 算成 Pixel；
三缓冲负责 Pixel 的安全交接；
调度器负责多图公平和 update 合并；
paintEvent 只负责贴最新 front。
```

这个模型适合你的场景： **一堆图、多线程后台渲染、高频数据变化、主线程只做轻量显示**。
