diff --git a/doc/img/img.png b/doc/img/img.png new file mode 100644 index 0000000..c4b115a Binary files /dev/null and b/doc/img/img.png differ diff --git a/doc/修改指令.md b/doc/修改指令.md deleted file mode 100644 index 6fd04ce..0000000 --- a/doc/修改指令.md +++ /dev/null @@ -1,689 +0,0 @@ -# 多线程图表库后台渲染缓冲模型重构说明 - -## 基本要求 - -我要重构这个多线程图表库的后台渲染缓冲模型。 - -要求: - -* 不要动 git。 -* 代码保持 K&R 样式。 -* 不要加无意义空行。 -* 除非必要,不要破坏已有函数签名语义。 -* 如果替换实现,旧实现直接删除,不要做兼容实现。 -* 能不做兼容实现就不做兼容实现,除非明确说明设计如此。 - -## 核心目标 - -把每个图抽象成下面这个通用模型: - -```text -状态缓冲 -> 后台 CPU 渲染 -> 颜色缓冲 -> Qt 主线程贴图 -``` - -也就是: - -```text -State -> Color -> paintEvent drawImage -``` - -具体图只实现: - -```text -State -> Color 的 CPU 渲染逻辑 -``` - -线程调度、版本判断、缓冲交换、刷新合并逻辑放到通用框架里。 - -## 线程模型 - -Qt GUI 线程和 asio 调度线程是两个不同线程。 - -asio 调度线程是单线程,负责串行调度所有图的: - -```text -状态修改 -任务提交 -版本判断 -后台任务完成发布 -刷新请求合并 -``` - -Qt GUI 线程不直接修改渲染状态。 - -Qt 主线程收到用户操作、数据输入、缩放、尺寸变化、颜色表变化等事件后,只把修改请求投递给 asio 调度线程。 - -asio 调度线程串行修改 `edit_state`,并递增 `edit_state.version`。 - -## 后台计算模型 - -后台计算使用 asio / 线程池封装成 awaitable。 - -asio 调度线程只负责任务编排,不在调度线程里做重 CPU 计算。 - -`State -> Color` 的重计算放到后台 CPU 执行环境里完成。 - -后台计算完成后,把完成事件 post 回 asio 调度线程。 - -颜色缓冲发布由 asio 调度线程执行。 - -CPU 后台任务只做计算,不直接操作 QWidget,不直接修改 `edit_state`,不直接操作 `front_color`。 - -## 状态缓冲 - -每个图使用两个状态缓冲: - -```text -edit_state -render_state -``` - -### edit_state - -`edit_state` 是 asio 调度线程持有的最新状态。 - -Qt 主线程收到事件后只投递修改命令,不直接写 `edit_state`。 - -所有会影响最终图像的状态变化,都必须在 asio 调度线程中修改 `edit_state`,并递增 `edit_state.version`。 - -典型状态变化包括: - -```text -用户操作 -数据输入 -缩放 -平移 -尺寸变化 -颜色表变化 -显示开关变化 -采样参数变化 -坐标范围变化 -``` - -### render_state - -`render_state` 是后台渲染任务使用的稳定状态快照。 - -提交后台任务时,由 `edit_state` 复制、交换或快照生成 `render_state`。 - -提交时必须保证: - -```text -render_state.version = edit_state.version -``` - -后台 CPU 任务只读取 `render_state` 对应的稳定快照。 - -## 状态版本号 - -两个状态缓冲都必须带 `version`。 - -示例结构: - -```cpp -struct State_Buffer { - std::uint64_t version = 0; - Chart_State state; -}; -``` - -`edit_state.version` 是整个渲染管线的源版本。 - -`render_state.version` 表示当前提交给后台计算的状态版本。 - -状态版本流向: - -```text -edit_state.version - ↓ submit / snapshot -render_state.version -``` - -判断是否需要提交后台任务: - -```text -edit_state.version 新于 render_state.version -并且当前图没有后台任务在飞 -``` - -## 颜色缓冲 - -每个图使用三个颜色缓冲: - -```text -front_color -ready_color -render_color -``` - -### front_color - -`front_color` 是 Qt 主线程当前正在显示的颜色图。 - -Qt 主线程在 `paintEvent` 中绘制 `front_color`。 - -后台线程和 asio 调度线程不能写 `front_color` 的像素内容。 - -后台发布流程不能直接交换或修改 `front_color`。 - -### ready_color - -`ready_color` 是后台已经生成完成、等待 Qt 主线程消费的颜色图。 - -`ready_color` 是后台发布线程和 Qt 主线程之间的唯一交接槽。 - -如果旧的 `ready_color` 还没被 Qt 主线程消费,而新的 `render_color` 又完成了,直接用新的 `render_color` 覆盖旧的 -`ready_color`。 - -图表库追最新状态,不做帧队列。 - -### render_color - -`render_color` 是后台线程正在写入的颜色图。 - -Qt 主线程不能读 `render_color`。 - -Qt 主线程不能交换 `render_color`。 - -后台 CPU 任务只写 `render_color`。 - -## 颜色版本号 - -每个颜色缓冲对象都带 `version`。 - -示例结构: - -```cpp -struct Color_Buffer { - std::uint64_t version = 0; - QImage image; -}; -``` - -颜色缓冲的 `version` 表示: - -```text -这张颜色图对应哪个 state 版本 -``` - -后台 CPU 渲染完成后,必须设置: - -```text -render_color.version = render_state.version -``` - -颜色版本不是独立递增出来的,而是从状态版本传递过来的。 - -完整版本流向: - -```text -edit_state.version - ↓ submit / snapshot -render_state.version - ↓ CPU render -render_color.version - ↓ publish -ready_color.version - ↓ paintEvent consume -front_color.version -``` - -## 版本号原则 - -每个缓冲对象都带 `version`。 - -版本号跟随缓冲对象,不单独跟随槽位。 - -版本号属于缓冲对象,不属于 `front`、`ready`、`render` 这些槽位名称。 - -也就是说,交换的是缓冲对象指针,版本号跟着缓冲对象一起移动。 - -不要让版本号和图像对象分离。 - -## 回环版本比较 - -版本比较必须封装成支持回环的函数,不要到处直接写 `>`。 - -函数语义: - -```text -version_newer(a, b) -``` - -表示: - -```text -a 是否新于 b -``` - -可以使用 `uint64_t`,并通过有符号差值处理回环。 - -前提是两个待比较版本之间的距离不会超过半个取值空间。 - -示例语义: - -```cpp -static bool version_newer(std::uint64_t a, std::uint64_t b) { - return static_cast(a - b) > 0; -} -``` - -所有版本新旧判断都必须通过该函数完成。 - -## 颜色缓冲分配原则 - -颜色缓冲对象固定分配。 - -槽位只交换指针。 - -不要在发布流程里反复分配或释放颜色缓冲对象。 - -推荐模型: - -```text -front_color 指针 -ready_color 指针 -render_color 指针 -``` - -三个槽位分别指向三个固定分配的 `Color_Buffer` 对象。 - -交换槽位时只交换指针,不拷贝 QImage,不拷贝像素数组。 - -## 颜色缓冲锁原则 - -所有跨线程槽位交换必须加锁。 - -锁内只允许做: - -```text -版本判断 -指针交换 -状态标记 -读取 dirty rect -设置必要标志位 -``` - -锁内禁止做: - -```text -像素计算 -内存分配 -QImage 填充 -drawImage -复杂状态计算 -后台等待 -任务提交等待 -``` - -原则: - -```text -大计算锁外做。 -锁内只换身份。 -``` - -## 颜色缓冲唯一合法交换 - -整个系统只允许两个颜色缓冲交换操作。 - -后台发布: - -```text -ready_color <-> render_color -``` - -Qt 主线程消费: - -```text -front_color <-> ready_color -``` - -不允许: - -```text -front_color <-> render_color -后台线程操作 front_color -Qt 主线程操作 render_color -绕过 ready_color 直接发布 -``` - -## 后台发布流程 - -后台 CPU 任务只写 `render_color`。 - -`render_color` 填充完成后,完成事件回到 asio 调度线程。 - -asio 调度线程在 `color_lock` 下执行发布。 - -发布逻辑: - -```text -如果 render_color.version 新于 ready_color.version: - 交换 ready_color 和 render_color -``` - -交换后判断: - -```text -如果 ready_color.version 新于 front_color.version: - scheduler->request_update() -``` - -如果旧 `ready_color` 还没被 Qt 主线程消费,而新 `render_color` 又完成了: - -```text -直接用新的 render_color 覆盖旧 ready_color。 -旧 ready_color 变成新的 render_color,下一轮会被后台覆盖。 -``` - -图表库只保留最新待显示图,不做帧队列。 - -## Qt paintEvent 流程 - -`paintEvent` 中在 `color_lock` 下判断: - -```text -如果 ready_color.version 新于 front_color.version: - 交换 ready_color 和 front_color -``` - -释放锁后执行: - -```text -drawImage(front_color) -``` - -`paintEvent` 不允许做: - -```text -状态计算 -颜色映射 -后台等待 -任务提交 -像素生成 -大数组遍历 -``` - -`paintEvent` 只负责消费最新 `ready_color` 并绘制 `front_color`。 - -即使没有新 `ready_color`,`paintEvent` 也必须能够直接绘制旧的 `front_color`。 - -因为 Qt 可能因为窗口遮挡、恢复、缩放、系统重绘等原因触发 `paintEvent`。 - -## 刷新请求流程 - -后台发布 `ready_color` 后,不直接在后台线程操作 QWidget。 - -`scheduler->request_update()` 负责把刷新请求投递到 Qt GUI 线程。 - -刷新请求需要使用 `paint_request_pending` 合并重复 update 请求。 - -多个图完成时合并 dirty region。 - -不要每个图都直接 update 整个窗口。 - -Qt 主线程收到刷新任务后调用: - -```text -QWidget::update(region) -``` - -由 Qt 稍后触发 `paintEvent`。 - -## paint_request_pending 语义 - -`paint_request_pending` 表示: - -```text -已经有一个刷新请求投递到 Qt GUI 线程,暂时不要重复投递 -``` - -判断是否投递 update: - -```text -ready_color.version 新于 front_color.version -并且 paint_request_pending == false -``` - -`paint_request_pending` 只用于合并 Qt update 请求,不用于判断颜色图是否新。 - -颜色图是否新仍然通过: - -```text -version_newer(ready_color.version, front_color.version) -``` - -判断。 - -## 任务提交原则 - -每个图同一时间最多一个后台渲染任务。 - -如果任务正在 `Queued` 或 `Running`,不再提交同一图的新任务。 - -任务运行期间 `edit_state` 又变化,只记录新版本。 - -当前任务完成后,下一轮直接提交最新 `edit_state`。 - -不要排队计算中间版本。 - -图不可见、尺寸无效、版本没变时,不提交任务。 - -## 任务状态 - -每个图至少需要表达以下任务状态: - -```text -Idle -Queued -Running -``` - -状态语义: - -```text -Idle: - 当前图没有后台任务。 - -Queued: - 当前图的后台任务已经提交,但还没开始执行。 - -Running: - 当前图的后台任务正在执行。 -``` - -提交条件: - -```text -job_state == Idle -edit_state.version 新于 render_state.version -图可见 -尺寸有效 -没有超过提交频率限制 -``` - -## 状态缓冲提交流程 - -asio 调度线程判断: - -```text -edit_state.version 是否新于 render_state.version -``` - -如果需要提交,并且当前图没有任务在飞: - -```text -生成 render_state 快照 -render_state.version = edit_state.version -标记任务运行 -把 render_state 对应的计算任务提交到后台 CPU 执行环境 -``` - -CPU 任务只读取 `render_state` 快照并写 `render_color`。 - -CPU 任务不直接修改 `edit_state`。 - -CPU 任务不直接发布 `ready_color`。 - -CPU 任务完成后 post 回 asio 调度线程,由 asio 调度线程执行颜色缓冲发布。 - -## 过期任务处理原则 - -如果后台任务计算的是较旧版本,例如: - -```text -任务开始时 render_state.version = 100 -任务运行期间 edit_state.version 已经变成 110 -``` - -默认策略: - -```text -允许 version 100 的结果发布 -任务完成后下一轮直接提交最新 edit_state.version = 110 -不排队计算 101 到 109 -``` - -这样界面不会长时间完全没有更新,同时也不会浪费 CPU 计算中间版本。 - -如果某些图要求严格不显示过期结果,可以单独设计策略,但默认通用框架采用“允许当前任务结果发布,下一轮追最新”的方式。 - -## render_color 写入原则 - -后台 CPU 任务写 `render_color` 前,必须保证 `render_color` 当前不属于 `front_color` 或 `ready_color`。 - -这个由三缓冲槽位交换规则保证。 - -后台 CPU 任务只能写当前槽位名为 `render_color` 的缓冲对象。 - -不要在未持有正确所有权的情况下写任意颜色缓冲。 - -如果使用 `QImage`,要注意 Qt 隐式共享。 - -每个颜色缓冲应持有独立的 QImage 存储。 - -后台写像素前,需要保证 `render_color.image` 独占可写。 - -不要让多个颜色缓冲共享同一块 QImage data。 - -## 锁和绘制的关系 - -锁保护的是: - -```text -front_color / ready_color / render_color 三个槽位身份 -版本号和图像对象的一致性 -ready_color 这个共享交接区 -``` - -锁不保护: - -```text -后台像素写入过程 -主线程 drawImage 的耗时过程 -CPU 计算过程 -状态计算过程 -``` - -正确方式: - -```text -后台: - 锁外写 render_color.image - 锁内 ready_color <-> render_color - -Qt GUI: - 锁内 ready_color <-> front_color - 锁外 drawImage(front_color.image) -``` - -## 通用缓冲控制抽象 - -需要重构出一个通用缓冲控制抽象。 - -该抽象负责: - -```text -状态版本管理 -状态快照提交 -任务 single-flight 控制 -颜色三缓冲管理 -版本回环比较 -颜色发布 -Qt update 请求合并 -dirty region 合并 -``` - -具体图只实现: - -```text -State -> Color 的 CPU 渲染逻辑 -``` - -不要把线程控制、版本控制、颜色缓冲交换散落到每个具体图实现里。 - -## 最终模型总结 - -每个图拥有: - -```text -edit_state -render_state - -front_color -ready_color -render_color -``` - -版本流向: - -```text -edit_state.version - ↓ -render_state.version - ↓ -render_color.version - ↓ -ready_color.version - ↓ -front_color.version -``` - -线程职责: - -```text -Qt GUI 线程: - 接收用户事件 - 投递状态修改命令 - 执行 QWidget::update(region) - paintEvent 中消费 ready_color - drawImage(front_color) - -asio 调度线程: - 串行修改 edit_state - 递增 edit_state.version - 判断是否提交后台任务 - 生成 render_state 快照 - 接收后台任务完成事件 - 发布 ready_color - 合并刷新请求 - -后台 CPU 执行环境: - 读取 render_state - 写 render_color - 完成后 post 回 asio 调度线程 -``` - -核心原则: - -```text -state version 驱动计算。 -color version 追踪产物。 -single-flight 避免堆任务。 -三颜色缓冲保证图像安全交接。 -ready_color 只保存最新待显示图。 -paintEvent 只贴 front_color。 -锁内只交换指针。 -大计算全部锁外完成。 -``` diff --git a/doc/整体架构思路.md b/doc/整体架构思路.md index 6ebd446..0c1da6c 100644 --- a/doc/整体架构思路.md +++ b/doc/整体架构思路.md @@ -1,53 +1,739 @@ -每一个图有自己的元数据 +# 图表渲染整体架构设计 -单独看流程就是 元数据 -> cpu计算 然后出来一个装满像素的数组 然后主线程合适时机贴图 +## 1. 总体流程 -其实真正流程就是主线程用户的各种操作刷新元数据缓冲(无锁操作)。 协程定时器(也通过版本号来判断是否计算还是等待下一个周期)合适的时机触发(加锁交换缓冲)扔给cpu线程池去计算出图 -然后加锁交换缓冲 标记版本号 主线程定时器看到版本号决定是否paint event +每一个图都有自己的元数据、状态缓冲和颜色缓冲。 -如果这个流程的话 如果 - -![img.png](img/图.png) - -![img.png](img/占用.png) +单独看一个图,核心流程是: +```text +元数据 / 状态 + ↓ +CPU 后台计算 + ↓ +像素数组 / QImage + ↓ +Qt 主线程在合适时机贴图 ``` -使用asio做协程调度器,单线程调度线程 调度所有执行流程。状态缓冲计算到颜色缓冲,放到后台asio协程池里 -封装成一个awitable,也由单线程调度线程 调度。 -注意 Qt 主线程 和 asio 调度线程 是两个不同线程。 +完整流程是: -抽象缓冲控制 -版本号 (需要回环) 每个缓冲都带版本号 +```text +用户操作 / 数据输入 / 缩放 / 尺寸变化 / 颜色表变化 + ↓ +更新 edit_state + ↓ +edit_state 快照到 ready_state + ↓ +调度线程在合适周期提交 ready_state + ↓ +ready_state 切换成 render_state + ↓ +CPU 线程池读取 render_state 计算颜色图 + ↓ +写入 render_color + ↓ +render_color 发布成 ready_color + ↓ +Qt 主线程消费 ready_color + ↓ +front_color 贴图显示 +``` -两个状态缓冲: -edit_state 主线程正在接收用户操作、数据输入、缩放、颜色表、尺寸变化等修改的状态。 -render_state 后台线程正在使用的稳定状态快照。 +也就是: -三个颜色缓冲: -front_color 主线程当前正在显示的颜色图。 -ready_color 后台已经生成完,等待主线程消费的颜色图。 -render_color 后台线程正在写入的颜色图。 +```text +edit_state + ↓ snapshot +ready_state + ↓ swap ready/render +render_state + ↓ CPU render +render_color + ↓ swap render/ready +ready_color + ↓ swap ready/front +front_color + ↓ drawImage +``` -后台发布: - render_color -> ready_color - 如果 ready_color.version > front_color.version - scheduler->request_update() +--- -调度器: - 用 paint_request_pending 合并 update 请求 +## 2. 线程模型 -paintEvent: - 如果 ready_color.version > front_color.version - ready_color <-> front_color - drawImage(front_color) - - -缓冲对象固定分配,槽位只交换指针。 -版本号跟随缓冲对象,不单独跟随槽位。 -所有跨线程槽位交换必须加锁。 -锁内只允许做版本判断、指针交换、状态标记。 -锁内禁止做像素计算、内存分配、QImage 填充、drawImage。 -大活锁外做。 -锁内只换身份。 -``` \ No newline at end of file +使用 asio 做协程调度器。 + +有三个主要执行环境: + +```text +Qt 主线程 +asio 单线程调度线程 +CPU 后台线程池 +``` + +### Qt 主线程 + +负责: + +```text +接收用户输入 +接收鼠标、键盘、resize、paintEvent +把用户操作投递给 asio 调度线程 +执行 QWidget::update() +在 paintEvent 中 drawImage(front_color) +``` + +Qt 主线程不直接做重计算。 + +Qt 主线程也不直接修改后台正在使用的状态缓冲。 + +推荐语义是: + +```text +Qt 主线程只产生操作命令。 +asio 调度线程把操作命令应用到 edit_state。 +``` + +这样 edit_state 的“无锁修改”来自于单线程所有权,而不是多个线程裸写同一份状态。 + +### asio 单线程调度线程 + +负责: + +```text +串行处理用户操作命令 +修改 edit_state +递增 edit_state.version +执行 edit_state -> ready_state 快照 +通过协程定时器判断是否需要提交后台计算 +把 ready_state 切换成 render_state +把后台计算任务提交给 CPU 线程池 +接收 CPU 任务完成事件 +发布 render_color -> ready_color +合并 request_update +``` + +asio 调度线程是整个图表流水线的控制中心。 + +### CPU 后台线程池 + +负责: + +```text +读取 render_state +计算颜色图 +写入 render_color +设置 render_color.version +完成后 post 回 asio 调度线程 +``` + +CPU 线程池不操作 QWidget。 + +CPU 线程池不修改 edit_state。 + +CPU 线程池不直接触发 paintEvent。 + +--- + +## 3. 抽象缓冲控制器 + +状态缓冲和颜色缓冲都使用同一类抽象: + +```text +固定缓冲池 ++ +角色状态 slot_state ++ +物理占用标记 busy ++ +版本号 version +``` + +每个缓冲对象固定分配,不在运行过程中反复 new/delete。 + +角色只是映射关系。 + +物理缓冲才是真正被读写的对象。 + +### 核心概念 + +```text +slot_state: + 表示角色到物理 buffer 的映射。 + +busy: + 表示某个物理 buffer 当前正在被长时间使用。 + +version: + 表示这个物理 buffer 当前内容对应的版本。 + +lease: + 表示一次具体的物理 buffer 占用。 +``` + +### 核心操作 + +抽象接口只需要三个基础操作: + +```text +mark_use +unmark_use +swap_role +``` + +语义: + +```text +mark_use: + 标记某个物理 buffer 正在被使用。 + 成功后返回 lease。 + +unmark_use: + 释放 lease 对应的物理 buffer。 + +swap_role: + 交换两个角色对应的物理 buffer。 + 交换前检查参与交换的物理 buffer 是否被占用。 + 支持 Try 和 Wait 两种模式。 +``` + +### swap_role 结果 + +交换操作必须返回明确结果: + +```text +Success: + 交换成功。 + +Busy: + 参与交换的 buffer 正在被占用。 + +State_Changed: + 操作过程中 slot_state 已经变化,需要重试。 + +Condition_Failed: + 业务条件不满足,例如版本没有更新。 +``` + +### swap_role 模式 + +```text +Try: + 尝试一次。 + 如果 buffer 被占用,立即返回 Busy。 + 适合 Qt paintEvent 等不能阻塞的路径。 + +Wait: + 如果 buffer 被占用,就等待释放后重试。 + 适合 asio 调度线程或后台发布路径。 + 不允许在 Qt paintEvent 里使用。 +``` + +### 交换规则 + +交换不是简单改指针,而是: + +```text +读取 slot_state +找到两个角色对应的物理 buffer +尝试 mark_use 这两个物理 buffer +mark 成功后重新读取 slot_state +确认角色映射没有变化 +检查业务条件 +CAS 提交新的 slot_state +释放两个物理 buffer +``` + +核心原则: + +```text +slot_state 管角色。 +busy 管物理占用。 +version 管内容新旧。 +lease 管一次具体使用。 +``` + +不能混用。 + +不能用 version 判断 buffer 是否正在被使用。 + +不能只用 slot_state 判断 buffer 是否安全可写。 + +不能释放 role,必须释放 lease。 + +因为 role 在使用期间可能已经被交换,lease 才代表真正占用的物理 buffer。 + +--- + +## 4. 版本号规则 + +每个状态缓冲和颜色缓冲都带 version。 + +版本号使用 uint64_t,并支持回环比较。 + +```cpp +static bool version_newer(std::uint64_t left, std::uint64_t right) { + return static_cast(left - right) > 0; +} +``` + +语义: + +```text +version_newer(left, right) +表示 left 是否新于 right。 +``` + +版本号跟随物理缓冲对象,不跟随角色槽位。 + +角色切换时,不单独修改版本号。 + +版本号只在内容真正更新后修改。 + +--- + +## 5. 状态三缓冲 + +状态缓冲也使用三缓冲。 + +每个图有三个状态缓冲: + +```text +states[3] +``` + +三个角色: + +```text +edit_state +ready_state +render_state +``` + +### edit_state + +```text +asio 调度线程正在修改的最新状态。 +``` + +用户操作、数据输入、缩放、颜色表、尺寸变化等最终都作用到 edit_state。 + +每次影响最终图像的修改,都递增: + +```text +edit_state.version +``` + +### ready_state + +```text +已经从 edit_state 快照出来,等待提交给 CPU 的状态。 +``` + +ready_state 是交接缓冲。 + +它可以在 CPU 正在读取 render_state 时,被新的 edit_state 覆盖。 + +这样可以跳过中间状态,只保留最新待计算状态。 + +### render_state + +```text +CPU 后台任务正在读取的稳定状态。 +``` + +render_state 被 CPU 读取期间必须 mark_use。 + +render_state 被占用时,不能被交换到会被写入的位置。 + +### 状态快照流程 + +当 edit_state.version 新于 ready_state.version 时: + +```text +mark_use(ready_state) +copy edit_state -> ready_state +ready_state.version = edit_state.version +unmark_use(ready_state) +``` + +如果 edit_state 只由 asio 调度线程修改,edit_state 本身可以不显式 mark_use。 + +如果以后允许其他线程直接写 edit_state,则 edit_state 也必须 mark_use。 + +### 状态提交流程 + +协程定时器在合适时机检查: + +```text +ready_state.version 是否新于 render_state.version +CPU 任务是否空闲 +render_state 是否未被占用 +``` + +满足条件后: + +```text +swap_role(ready_state, render_state, Wait) +mark_use(render_state) +提交 CPU 后台计算任务 +``` + +CPU 任务完成后: + +```text +unmark_use(render_state) +``` + +状态三缓冲的核心意义是: + +```text +CPU 正在读 render_state。 +asio 继续改 edit_state。 +ready_state 保存下一次要提交的最新快照。 +``` + +--- + +## 6. 颜色三缓冲 + +每个图有三个颜色缓冲: + +```text +colors[3] +``` + +三个角色: + +```text +front_color +ready_color +render_color +``` + +### front_color + +```text +Qt 主线程当前正在显示的颜色图。 +``` + +paintEvent 只画 front_color。 + +### ready_color + +```text +后台已经生成完成,等待 Qt 主线程消费的颜色图。 +``` + +ready_color 是后台发布和 GUI 消费之间的交接缓冲。 + +### render_color + +```text +CPU 后台线程正在写入的颜色图。 +``` + +CPU 写 render_color 时必须 mark_use。 + +写完后设置: + +```text +render_color.version = render_state.version +``` + +--- + +## 7. CPU 计算流程 + +asio 调度线程提交任务前,先把 ready_state 切换成 render_state。 + +然后 CPU 任务执行: + +```text +mark_use(render_state) +mark_use(render_color) +读取 render_state +计算像素 +写入 render_color +render_color.version = render_state.version +unmark_use(render_color) +unmark_use(render_state) +post 完成事件到 asio 调度线程 +``` + +实际实现中,如果 mark_use (render_state) 已经在提交任务前完成,也可以把 lease 一起传给 CPU 任务,任务结束时释放 lease。 + +重点是: + +```text +CPU 读取的 render_state 在任务期间不能被复用。 +CPU 写入的 render_color 在任务期间不能被发布。 +``` + +--- + +## 8. 颜色发布流程 + +CPU 计算完成后,asio 调度线程发布颜色图。 + +发布逻辑: + +```text +如果 render_color.version 新于 ready_color.version: + swap_role(ready_color, render_color, Try 或 Wait) +``` + +发布成功后: + +```text +如果 ready_color.version 新于 front_color.version: + scheduler->request_update() +``` + +这里的 ready_color 是交换后的新 ready_color。 + +request_update 不直接绘制,只是向 Qt 主线程请求下一次 update。 + +--- + +## 9. Qt 更新合并 + +调度器使用 paint_request_pending 合并 update 请求。 + +语义: + +```text +paint_request_pending 表示已经有一个 update 请求在路上。 +``` + +它不表示是否有新图。 + +是否有新图仍然通过版本号判断: + +```text +ready_color.version 是否新于 front_color.version +``` + +request_update 流程: + +```text +如果 paint_request_pending == false: + paint_request_pending = true + 投递 QWidget::update() 到 Qt 主线程 +``` + +paintEvent 执行后: + +```text +paint_request_pending = false +``` + +--- + +## 10. paintEvent 流程 + +paintEvent 不能等待。 + +paintEvent 只能使用 Try 模式。 + +流程: + +```text +尝试 swap_role(front_color, ready_color, Try) +如果成功: + 新 ready_color 被消费成 front_color +如果失败: + 继续使用旧 front_color +mark_use(front_color) +drawImage(front_color) +unmark_use(front_color) +``` + +paintEvent 的核心规则: + +```text +不等待后台。 +不做 CPU 计算。 +不分配大内存。 +不填充 QImage。 +只消费 ready_color。 +只绘制 front_color。 +``` + +如果 Qt paintEvent 保证单线程不重入,并且后台永远不写 front_color,front_color 的 mark_use 可以作为优化项省略。 + +但从统一抽象上看,front_color 在 drawImage 期间属于被长时间读取的缓冲,最好仍然按 lease 模型处理。 + +--- + +## 11. 协程定时器调度 + +每个图可以由 asio 协程定时器驱动调度。 + +定时器周期到来时,不一定立刻计算。 + +它先判断版本: + +```text +edit_state.version 是否新于 ready_state.version +ready_state.version 是否新于 render_state.version +当前 CPU 任务是否空闲 +当前图是否可见 +当前尺寸是否有效 +是否超过刷新频率限制 +``` + +典型流程: + +```text +如果 edit_state 新于 ready_state: + 快照 edit_state -> ready_state + +如果 ready_state 新于 render_state 且 CPU 任务空闲: + ready_state <-> render_state + 提交 CPU 计算任务 + +否则: + 等待下一个周期 +``` + +这个机制天然跳过中间状态。 + +例如: + +```text +CPU 正在计算 version 100 +用户连续操作到 version 110 +ready_state 最终只保留 version 110 +CPU 完成 100 后,下一轮直接计算 110 +``` + +图表库追求最新状态,不排旧帧队列。 + +--- + +## 12. single-flight + +每个图同一时间只允许一个 CPU 计算任务。 + +任务状态可以是: + +```text +Idle +Queued +Running +``` + +规则: + +```text +只有 Idle 才能提交新的 CPU 任务。 +Running 期间继续更新 edit_state 和 ready_state。 +Running 完成后,如果 ready_state 新于 render_state,再提交下一轮。 +``` + +single-flight 的作用是: + +```text +防止多个 CPU 任务同时读取/写入同一组缓冲。 +防止旧版本任务并发污染新版本结果。 +避免无意义的中间帧堆积。 +``` + +--- + +## 13. 状态和颜色的完整版本链路 + +版本链路是: + +```text +edit_state.version + ↓ snapshot +ready_state.version + ↓ swap ready/render +render_state.version + ↓ CPU render +render_color.version + ↓ swap render/ready +ready_color.version + ↓ swap ready/front +front_color.version +``` + +颜色版本不是独立生成的。 + +颜色版本来自 render_state.version。 + +也就是: + +```text +render_color.version = render_state.version +``` + +这表示这张颜色图是由哪个状态版本计算出来的。 + +--- + +## 14. 锁和无锁边界 + +旧设计里的“加锁交换缓冲”可以替换成: + +```text +缓冲控制器交换角色 +``` + +缓冲控制器内部可以使用: + +```text +atomic busy +atomic slot_state +CAS +atomic_wait +``` + +Try 路径可以不使用 mutex。 + +Wait 路径可以不使用 mutex,但会等待 atomic 状态变化。 + +所以更准确的说法是: + +```text +不是所有交换都必须用 mutex。 +交换必须通过缓冲控制器完成。 +控制器保证占用检查、角色确认和 CAS 交换的原子提交语义。 +``` + +锁内原则可以改成控制器原则: + +```text +大活在 lease 持有期间做。 +角色切换只通过 swap_role 做。 +swap_role 内部只做占用检查、版本条件判断、slot_state CAS。 +swap_role 内禁止像素计算、内存分配、QImage 填充、drawImage。 +``` + +--- + +## 15. 最终核心规则 + +```text +缓冲对象固定分配。 +角色只通过 slot_state 映射。 +版本号跟随物理缓冲对象。 +每个缓冲可以被长时间占用。 +长时间使用前必须 mark_use。 +释放时必须 unmark_use lease。 +角色交换必须通过 swap_role。 +swap_role 必须返回明确成功或失败。 +Qt paintEvent 只使用 Try 模式。 +后台调度可以使用 Wait 模式。 +CPU 任务 single-flight。 +状态三缓冲负责输入侧交接。 +颜色三缓冲负责输出侧交接。 +``` + +最终架构一句话: + +```text +每个图使用两组三缓冲:状态三缓冲负责 edit/ready/render 的元数据交接,颜色三缓冲负责 render/ready/front 的像素交接;所有物理缓冲都有版本号和占用标记,所有角色切换都通过缓冲控制器以 CAS 方式提交,长时间读写通过 lease 保护。 +``` diff --git a/module/radio/BackEnd/IntermediateFrequencyWidget.cpp b/module/radio/BackEnd/IntermediateFrequencyWidget.cpp index 41020e2..472b3aa 100644 --- a/module/radio/BackEnd/IntermediateFrequencyWidget.cpp +++ b/module/radio/BackEnd/IntermediateFrequencyWidget.cpp @@ -76,7 +76,7 @@ QWidget* IntermediateFrequencyWidget::createSpectrogramPowerPlot() { mProgressBar1->mSetValue = [this]() { double middleFrequent = this->mSpectrogramLine->frequentRange().center(); bool ok; - this->mProgressBar1->mValue = mSpectrogramLine->getPower(middleFrequent, ok, YSG::SRC::Cache); + this->mProgressBar1->mValue = mSpectrogramLine->getPower(middleFrequent, ok, YSG::SRC::Edit); }; return w; } diff --git a/module/radio/BackEnd/RadioWidget.cpp b/module/radio/BackEnd/RadioWidget.cpp index 11be411..4393122 100644 --- a/module/radio/BackEnd/RadioWidget.cpp +++ b/module/radio/BackEnd/RadioWidget.cpp @@ -72,7 +72,7 @@ QWidget* RadioWidget::createSpectrogramPowerPlot() { mProgressBar1->mSetValue = [this]() { double middleFrequent = this->mSpectrogramLine->frequentRange().center(); bool ok; - this->mProgressBar1->mValue = mSpectrogramLine->getPower(middleFrequent, ok, YSG::SRC::Cache); + this->mProgressBar1->mValue = mSpectrogramLine->getPower(middleFrequent, ok, YSG::SRC::Edit); }; return w; }