Files
Radio/doc/整体架构思路.md
T
2026-07-23 12:46:33 +08:00

12 KiB
Raw Blame History

图表渲染整体架构设计

1. 总体模型

每个 RenderAble 拆成四类数据:

State 三缓冲
Input 三缓冲
Render 私有缓存
Color 三缓冲

职责边界:

State:
    小型、可复制、表示显示配置和稳定状态。

Input:
    外部输入到渲染线程的增量数据。

RenderCache:
    渲染侧长期缓存,不复制,不参与状态交换。

Color:
    后台生成的像素图,交给 Qt 主线程贴图。

核心原则:

配置走 State。
样本走 Input。
缓存留在 RenderData 私有成员。
像素走 Color。

不能把流式样本、环形缓冲、累计矩阵塞进 State。

State 的同步语义是复制。

Input 的同步语义是角色交换。

RenderCache 的语义是渲染侧所有权。

2. 整体流程

用户操作 / 属性修改
    ↓
修改 edit_state
    ↓
state edit -> ready 复制
    ↓
state ready -> render 交换
数据输入 / giveData
    ↓
追加到 edit_input
    ↓
input edit -> ready 交换
    ↓
input ready -> render 交换
    ↓
prepareData 消费 render_input
    ↓
更新 RenderCache
    ↓
clear render_input
draw
    ↓
读取 render_state
    ↓
读取 RenderCache
    ↓
写 render_color
    ↓
color render -> ready 交换
    ↓
Qt paintEvent 消费 ready_color 到 front_color

完整数据链路:

edit_state + edit_input
    ↓
prepareData
render_state + render_input
    ↓
RenderCache
    ↓
render_color
    ↓
ready_color
    ↓
front_color

3. 三缓冲控制器

State、Input、Color 的角色交换都使用同一个控制器:

Triple_Role_Buffer_Control

控制器只负责:

role -> physical index
busy 标记
lease
swap_role

它不理解实际数据类型。

实际数据可以是:

RenderState
InputData
ColorBuffer

控制器语义:

slot_state 管角色映射。
busy 管物理占用。
lease 管一次具体使用。
swap_role 只交换角色,不复制业务数据。

长时间使用某个角色时,必须释放 lease,不允许按 role 释放。

因为使用期间 role 可能已经被交换。

4. State 三缓冲

State 有三个角色:

State_Edit
State_Ready
State_Render

State 里只放:

坐标轴指针
范围
颜色
画笔
字体
hover 状态
marker 状态
模式开关
点数配置

State 里不放:

输入样本列表
环形缓冲区
QImage
累计矩阵
曲线当前数据缓存
WaterFall 行缓存
Afterglow 能量累计缓存

State 同步流程:

如果 edit_state.version 新于 ready_state.version
    mark ready_state
    copy edit_state -> ready_state
    unmark ready_state

如果 ready_state.version 新于 render_state.version
    swap ready_state, render_state

State 的核心意义:

把可复制配置从编辑侧稳定发布给渲染侧。
跳过中间状态。
保证 CPU 绘制时读取稳定 render_state。

5. Input 三缓冲

Input 有三个角色:

Input_Edit
Input_Ready
Input_Render

InputData 里放增量输入:

WaterFall 新行
AudioFrequent 新功率点
TimeAxis 新时间点
SweepFrequent 新块数据
Planisphere 新星座点
Spectrum 新曲线样本
Afterglow 新功率行

输入线程只追加到 edit_input。

追加成功后:

input_slot.version = ++mInputVersion
input_slot.pending = true
requestRender()

Input 同步流程:

如果 edit_input.pending 且 edit_input.version 新于 ready_input.version
    swap edit_input, ready_input

如果 ready_input.pending 且 ready_input.version 新于 render_input.version
    swap ready_input, render_input

prepareData 消费流程:

auto input = renderInputData()
for each delta in input:
    更新 RenderCache
clearRenderInput()

clearRenderInput 必须同时完成:

清空 render_input 数据
render_input.pending = false

Input 三缓冲的核心意义:

输入数据不复制到 State。
输入数据不触发 RenderState 大对象复制。
渲染侧按批消费增量。
edit_input 可以在下一轮继续接收新数据。

6. Render 私有缓存

RenderCache 是 RenderData 的私有成员,不参与三缓冲复制。

典型对象:

QImage image
WaterFallRingBuffer ringBuffer
TimeAxis::mRingBuffer
SweepFrequent::mFrequents
Planisphere::dataList
Afterglow cachedPowerData / oldCachePowerData / mergedPowerData
Spectrum curPowers / maxPowers / minPowers / frequents

RenderCache 的来源:

render_state 配置
render_input 增量

RenderCache 的更新位置:

prepareData()

RenderCache 的读取位置:

draw()

RenderCache 不应该由属性 setter 直接修改。

属性 setter 只改 edit_state。

数据输入函数只追加 edit_input。

7. Color 三缓冲

Color 有三个角色:

Color_Front
Color_Ready
Color_Render

Color_Render

CPU 后台线程写入。

Color_Ready

后台完成后等待 Qt 主线程消费。

Color_Front

Qt paintEvent 当前显示。

发布流程:

render_color.version 新于 ready_color.version
    swap ready_color, render_color

ready_color.version 新于 front_color.version
    request update

paintEvent 流程:

按 Plot_Buffer_Acquire_Mode 消费 ready_color
mark front_color
drawImage(front_color)
unmark front_color

Plot_Buffer_Acquire_Mode 有两种策略:

Try
    paintEvent 尝试交换 ready_color -> front_color。
    如果物理 buffer 正忙,立即放弃本次新帧消费。
    这是默认模式,GUI 线程不进入 atomic_wait。

Wait
    paintEvent 等待参与交换或 front_color lease 可用。
    只适合调用方明确接受 GUI 线程短暂等待的场景。

Color 发布侧仍然可以等待 Color_Render / Color_Ready,因为它运行在调度和后台渲染流程,不阻塞 Qt paintEvent。

8. 三缓冲获取策略

三缓冲获取策略不是单一全局开关,而是分层控制。

Plot 层:

Plot_Buffer_Acquire_Mode

只控制 Qt paintEvent 消费 Color_Ready / Color_Front 的行为。

默认:

Try

RenderAble 层:

Triple_Buffer_Mode

控制单个 RenderAble 在后台 draw 阶段获取 State_Render lease 的行为。

默认:

Wait

RenderAble 设置为 Try 后:

draw 阶段取不到 State_Render lease
    ↓
跳过该 RenderAble 本帧绘制

State/Input 的编辑发布路径默认仍使用 Wait。

原因:

setter / giveData 当前多数是 void 语义。
Try 失败后没有统一返回值表达本次修改是否被接受。
在没有重新设计 API 返回语义前,不允许静默丢弃编辑和输入。

9. 调度版本

渲染调度使用 RenderPipeline 的帧版本。

State dirty 和 Input dirty 都会推进帧版本。

区别:

State dirty
    edit_state.version++
    frame_version++

Input dirty
    input_version++
    frame_version++

Input dirty 不递增 edit_state.version。

这点非常重要。

它保证流式输入不会导致 RenderState 全量复制。

10. asio timer 和帧率

Plot 仍保留 asio steady_timer

PlotPrivate::mRenderTimer
PlotPrivate::mRefreshTimesPreSecond
Plot::startRender(int refreshTimesPreSecond)
Plot::scheduleRenderTimer()

当前 timer 的语义:

按 mRefreshTimesPreSecond 周期触发 submitRender()
作为渲染 heartbeat / 兜底轮询
保持旧 startRender(int) 调用语义

当前 timer 不是严格 FPS 上限。

原因:

State dirty 会立即 requestRender()
Input dirty 会立即 requestRender()
finishRender 发现还有新版本也会继续 submitRender()

因此实际渲染触发是:

事件驱动为主
timer 周期触发为辅

如果后续需要严格限帧,应该增加独立的 render trigger 策略,而不是把 startRender(int) 继续解释成硬性 FPS cap。

11. prepareData 边界

prepareData 的标准流程:

syncStatePipeline()
读取 render_state 配置
读取 render_input 增量
更新 RenderCache
clearRenderInput()

syncStatePipeline 内部同时处理:

State edit -> ready -> render
Input edit -> ready -> render

prepareData 不再持有 RenderData 对象级锁。

edit_state 写入必须通过 State_Edit lease。

edit_input 写入必须通过 Input_Edit lease。

RenderCache 只允许 render 流程内部更新。

prepareData 不应该写 Color。

prepareData 不应该触发 QWidget 绘制。

prepareData 不应该把 RenderCache 复制回 State。

12. draw 边界

draw 的标准流程:

读取 render_state
读取 RenderCache
写入当前 render_color 对应的 QPainter

draw 不消费 Input。

draw 不修改 edit_state。

draw 不修改 ready_state。

draw 不做输入队列交换。

13. 数据分类规则

判断一个字段应该放哪里:

需要被属性 getter/setter 表达,体积小,可复制:
    放 State。

外部不断输入,prepareData 批量消费:
    放 InputData。

由 render_state + render_input 计算出来,长期保留给 draw 使用:
    放 RenderData 私有缓存。

最终像素图:
    放 ColorBuffer。

错误分类:

把 QVector 大缓存放 State。
把 QImage 放 State。
把输入 list 放 State。
把 RenderCache 放 InputData。
把配置项放 InputData。

14. 锁使用边界

radio 渲染链路不允许使用 RenderData 对象级大锁。

允许存在的同步只有下面几类:

Triple_Role_Buffer_Control:
    使用 atomic busy / slot_state / wait / notify。
    只保护物理 buffer lease 和 role 交换。

RenderPipeline:
    使用 color buffer version / paintRequestPending / editStateVersion。
    只保护帧发布和 Qt update 请求合并。

Plot / TimerThread:
    mRenderEnabled / mDestroying / mActiveRenderTasks / pending render size / paint buffer acquire mode 使用 atomic。
    mRenderTaskMutex + mRenderTaskDone 只用于 Plot 析构等待已投递任务结束。
    TimerThread::postAndWait 的局部 mutex + condition_variable 只用于跨线程同步 shutdown/removePlot。

PerformanceShower:
    mMutex 只保护统计 map 和显示列表。
    计数使用 CPP_Core/Core/Statistics::Speed_Statistics。
    耗时使用 CPP_Core/Core/Statistics::Value_Statistics。
    Value_Statistics 提供 instant / smooth / variation。

不允许重新引入:

RenderData::mBufferLock
prepareData 全局锁
render size mutex
Graphic resize mutex
HoverInfo 独立 mutex
PrePareMutiDataMutex
SpinLock / SpinLockGuard
Singleton mutex + atomic 双重检查

prepareData 和 draw 的并发边界由调度流保证:

submitRender 进入 asio scheduler。
scheduler 串行推进 jobState。
prepareData 在 scheduler 阶段消费 State/Input。
renderColor 在 CPU 阶段只读取 State_Render 和 RenderCache。
finishRender 回到 scheduler 发布 Color。

CPP_Core 和第三方库内部同步不属于 radio 渲染架构边界:

RingBuffer_MT / StreamRingBuffer_MT:
    通用多线程容器版本。

SM_RingBuffer / Cross_Process_Mutex:
    跨进程共享内存同步。

Frequency_Limit:
    通用频率限制器。

spdlog / asio / googlepinyin:
    第三方或通用库内部同步。

这些同步不能向 RenderData、prepareData、draw 路径扩散。

15. 性能统计

性能测试框不再维护自己的耗时平滑算法。

计数类数据:

PerformanceShower::counterIncrease()
    ↓
Psc::Speed_Statistics::update(1)

耗时类数据:

Cacl 析构
    ↓
Psc::Value_Statistics::update(useTime)

Value_Statistics 的含义:

instant
    本次采样值。

average:
    保留旧字段语义的 EWMA 平均值。

smooth
    类似 SRTT 的平滑值。

variation
    类似 RTTVAR 的波动估计。
    它不是严格方差或标准差,而是 instant 与 smooth 偏差的 EWMA。

性能框显示耗时时使用:

instant
SRTT
RTTVAR

16. 最终规则

State 负责配置快照。
Input 负责增量输入交接。
RenderCache 负责渲染侧长期缓存。
Color 负责像素交接。

一句话:

每个 RenderAble 使用 State 三缓冲发布小状态,使用 Input 三缓冲发布增量输入,使用 RenderData 私有缓存保存重型渲染中间结果,使用 Color 三缓冲把后台像素图交给 Qt 主线程。