diff --git a/doc/ai.md b/doc/ai.md new file mode 100644 index 0000000..0318a0d --- /dev/null +++ b/doc/ai.md @@ -0,0 +1,687 @@ +下面这个就是我建议你这个 **多图表后台渲染库**的完整模型。 + +核心目标是: + +```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。 +``` + +这个模型适合你的场景: **一堆图、多线程后台渲染、高频数据变化、主线程只做轻量显示**。 diff --git a/doc/ai2.md b/doc/ai2.md new file mode 100644 index 0000000..eb4e678 --- /dev/null +++ b/doc/ai2.md @@ -0,0 +1,341 @@ +我的建议是: + +```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 +计算可以用定时器调度,刷新不要用定时器轮询;刷新应该由后台完成事件驱动,再由主线程事件循环合并执行。 +``` diff --git a/doc/img/占用.png b/doc/img/占用.png new file mode 100644 index 0000000..927457d Binary files /dev/null and b/doc/img/占用.png differ diff --git a/doc/img/图.png b/doc/img/图.png new file mode 100644 index 0000000..baa389f Binary files /dev/null and b/doc/img/图.png differ diff --git a/doc/修改指令.md b/doc/修改指令.md new file mode 100644 index 0000000..bc871c8 --- /dev/null +++ b/doc/修改指令.md @@ -0,0 +1,48 @@ +我要重构这个多线程图表库的后台渲染缓冲模型。不要动 git。代码保持 K&R 样式,不要加无意义空行。除非必要,不要破坏已有函数签名语义;如果替换实现,旧实现直接删除,不要做兼容实现。 + +核心目标: 把每个图抽象成“状态缓冲 -> 后台 CPU 渲染 -> 颜色缓冲 -> Qt 主线程贴图”的通用模型。Qt GUI 线程和 asio +调度线程是两个不同线程。asio 调度线程是单线程,负责串行调度所有图的状态修改、任务提交、版本判断、任务完成发布、刷新请求合并。Qt +主线程不直接修改渲染状态,只把用户操作、数据输入、缩放、尺寸变化、颜色表变化等修改请求投递给 asio 调度线程。asio 调度线程串行修改 +edit_state,并递增 edit_state.version。 + +后台计算使用 asio/线程池封装成 awaitable。asio 调度线程只负责任务编排,不在调度线程里做重 CPU 计算。state -> color 的重计算放到后台 +CPU 执行环境里完成。后台计算完成后,把完成事件 post 回 asio 调度线程,由 asio 调度线程发布颜色缓冲。 + +每个图使用两个状态缓冲: edit_state: asio 调度线程持有的最新状态。Qt 主线程收到事件后只投递修改命令,不直接写 edit_state。 +render_state: 后台渲染任务使用的稳定状态快照。提交任务时由 edit_state 复制、交换或快照生成。 + +每个图使用三个颜色缓冲: front_color: Qt 主线程当前正在显示的颜色图。 ready_color: 后台已经生成完,等待 Qt 主线程消费的颜色图。 +render_color: 后台线程正在写入的颜色图。 + +每个缓冲对象都带 version。版本号跟随缓冲对象,不单独跟随槽位。版本比较需要封装成支持回环的函数,不要到处直接写 >。 + +版本比较函数语义: version_newer (a, b) 表示 a 是否新于 b。 可以用 uint64_t,并通过有符号差值处理回环,前提是版本差距不会超过半个取值空间。 + +颜色缓冲原则: 缓冲对象固定分配,槽位只交换指针。 所有跨线程槽位交换必须加锁。 锁内只允许做版本判断、指针交换、状态标记。 +锁内禁止做像素计算、内存分配、QImage 填充、drawImage。 大计算锁外做,锁内只换身份。 + +颜色缓冲的唯一合法交换: 后台发布: ready_color <-> render_color Qt 主线程消费: front_color <-> ready_color + +不允许: front_color <-> render_color 后台线程操作 front_color Qt 主线程操作 render_color 绕过 ready_color 直接发布 + +后台发布流程: 后台只写 render_color。 render_color 填充完成后,完成事件回到 asio 调度线程。 asio 调度线程在 color_lock 下判断: +如果 render_color.version 新于 ready_color.version,则交换 ready_color 和 render_color。 交换后,如果 ready_color.version +新于 front_color.version,则调用 scheduler->request_update ()。 如果旧 ready_color 还没被 Qt 主线程消费,新 render_color +又完成了,直接用新 render_color 覆盖旧 ready_color。图表库追最新状态,不做帧队列。 + +Qt paintEvent 流程: paintEvent 中在 color_lock 下判断: 如果 ready_color.version 新于 front_color.version,则交换 +ready_color 和 front_color。 释放锁后 drawImage (front_color)。 paintEvent 不做状态计算、不做颜色映射、不做后台等待、不提交后台任务。 + +刷新请求流程: 后台发布 ready_color 后,不直接在后台线程操作 QWidget。 scheduler->request_update () 负责把刷新请求投递到 Qt +GUI 线程。 使用 paint_request_pending 合并重复 update 请求。 多个图完成时合并 dirty region,不要每个图都直接 update 整个窗口。 +Qt 主线程收到刷新任务后调用 QWidget::update (region),由 Qt 稍后触发 paintEvent。 + +任务提交原则: 每个图同一时间最多一个后台渲染任务。 如果任务正在 Queued 或 Running,不再提交同一图的新任务。 任务运行期间 +edit_state 又变化,只记录新版本;当前任务完成后,下一轮直接提交最新 edit_state,不排队计算中间版本。 图不可见、尺寸无效、版本没变时,不提交任务。 + +状态缓冲提交流程: asio 调度线程判断 edit_state.version 是否新于 render_state.version。 如果需要提交,并且当前图没有任务在飞: +生成 render_state 快照。 render_state.version = edit_state.version。 标记任务运行。 把 render_state 对应的计算任务提交到后台 +CPU 执行环境。 CPU 任务只读取 render_state 快照并写 render_color,不直接修改 edit_state。 + +需要重构出一个通用的缓冲控制抽象,具体图只实现: State -> Color 的 CPU 渲染逻辑。 线程调度、版本判断、缓冲交换、update +合并逻辑放在通用框架里。 diff --git a/doc/整体架构思路.md b/doc/整体架构思路.md new file mode 100644 index 0000000..6ebd446 --- /dev/null +++ b/doc/整体架构思路.md @@ -0,0 +1,53 @@ +每一个图有自己的元数据 + +单独看流程就是 元数据 -> cpu计算 然后出来一个装满像素的数组 然后主线程合适时机贴图 + +其实真正流程就是主线程用户的各种操作刷新元数据缓冲(无锁操作)。 协程定时器(也通过版本号来判断是否计算还是等待下一个周期)合适的时机触发(加锁交换缓冲)扔给cpu线程池去计算出图 +然后加锁交换缓冲 标记版本号 主线程定时器看到版本号决定是否paint event + +如果这个流程的话 如果 + +![img.png](img/图.png) + +![img.png](img/占用.png) + +``` +使用asio做协程调度器,单线程调度线程 调度所有执行流程。状态缓冲计算到颜色缓冲,放到后台asio协程池里 +封装成一个awitable,也由单线程调度线程 调度。 +注意 Qt 主线程 和 asio 调度线程 是两个不同线程。 + + +抽象缓冲控制 +版本号 (需要回环) 每个缓冲都带版本号 + +两个状态缓冲: +edit_state 主线程正在接收用户操作、数据输入、缩放、颜色表、尺寸变化等修改的状态。 +render_state 后台线程正在使用的稳定状态快照。 + +三个颜色缓冲: +front_color 主线程当前正在显示的颜色图。 +ready_color 后台已经生成完,等待主线程消费的颜色图。 +render_color 后台线程正在写入的颜色图。 + +后台发布: + render_color -> ready_color + 如果 ready_color.version > front_color.version + scheduler->request_update() + +调度器: + 用 paint_request_pending 合并 update 请求 + +paintEvent: + 如果 ready_color.version > front_color.version + ready_color <-> front_color + drawImage(front_color) + + +缓冲对象固定分配,槽位只交换指针。 +版本号跟随缓冲对象,不单独跟随槽位。 +所有跨线程槽位交换必须加锁。 +锁内只允许做版本判断、指针交换、状态标记。 +锁内禁止做像素计算、内存分配、QImage 填充、drawImage。 +大活锁外做。 +锁内只换身份。 +``` \ No newline at end of file