完美一版

This commit is contained in:
2026-08-02 00:26:59 +08:00
parent 85aecd0a06
commit 883df14d5e
5 changed files with 378 additions and 835 deletions
+171 -61
View File
@@ -3,8 +3,10 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <deque>
#include <memory>
#include <mutex>
#include <optional>
#include <unordered_set>
#include <taskflow/taskflow.hpp>
#include <thread>
@@ -106,6 +108,16 @@ private:
};
class Render_Executor_Private {
struct Task_Run_Record {
std::uint64_t begin_ns{};
std::uint64_t queue_wait_ns{};
};
struct Pending_Frame_Task {
Render_Executor_Task task;
std::uint64_t enqueue_ns{};
};
public:
explicit Render_Executor_Private(std::size_t worker_count)
: metrics(std::make_shared<Render_Executor_Metrics>()),
@@ -119,62 +131,63 @@ public:
}
bool try_submit(Render_Executor_Task task) {
if (shutting_down.load(std::memory_order_acquire) || !task.work) {
if (shutting_down.load(std::memory_order_acquire) || (!task.work && !task.build_taskflow)) {
reject(task);
return false;
}
if (!admit(task)) {
reject(task);
return false;
}
std::uint64_t enqueue_ns = steady_now_ns();
metrics->submitted_tasks.fetch_add(1, std::memory_order_relaxed);
add_kind_count(*metrics, task.kind);
if (task.frame_job)
metrics->queued_frame_jobs.fetch_add(1, std::memory_order_relaxed);
if (task.frame_stat)
task.frame_stat->executor_at_submit = snapshot();
auto topology = std::make_shared<tf::Taskflow>();
auto task_ptr = std::make_shared<Render_Executor_Task>(std::move(task));
auto completion = std::make_shared<Task>(std::move(task_ptr->completion));
topology->emplace([this, enqueue_ns, task_ptr]() mutable {
Render_Executor_Task& task = *task_ptr;
std::uint64_t begin_ns = steady_now_ns();
std::uint64_t queue_wait_ns = begin_ns > enqueue_ns ? begin_ns - enqueue_ns : 0;
metrics->queue_wait_total_ns.fetch_add(queue_wait_ns, std::memory_order_relaxed);
update_peak(metrics->queue_wait_max_ns, queue_wait_ns);
if (task.frame_job) {
metrics->queued_frame_jobs.fetch_sub(1, std::memory_order_relaxed);
metrics->active_frame_jobs.fetch_add(1, std::memory_order_relaxed);
std::uint64_t enqueue_ns = steady_now_ns();
if (!task.frame_job) {
record_submission(task);
submit_to_executor(std::move(task), enqueue_ns);
return true;
}
bool submit_now = false;
{
std::lock_guard<std::mutex> lock(admission_mutex);
if (plot_already_pending_or_admitted_locked(task.plot_id)) {
reject(task);
return false;
}
task.work();
std::uint64_t end_ns = steady_now_ns();
std::uint64_t run_ns = end_ns > begin_ns ? end_ns - begin_ns : 0;
if (task.frame_stat) {
Frame_Worker_Stat& stat = *task.frame_stat;
stat.task_count++;
stat.peak_parallelism = static_cast<std::uint32_t>(std::max<std::uint64_t>(stat.peak_parallelism, metrics->peak_concurrency.load(std::memory_order_acquire)));
stat.queue_wait_total_ns += queue_wait_ns;
stat.queue_wait_max_ns = std::max(stat.queue_wait_max_ns, queue_wait_ns);
stat.worker_run_total_ns += run_ns;
stat.parallel_stage_wall_ns += run_ns;
stat.executor_at_finish = snapshot();
std::size_t limit = frame_admission_limit();
if (admitted_frame_count < limit) {
admit_frame_locked(task.plot_id);
submit_now = true;
}
if (task.frame_job)
metrics->active_frame_jobs.fetch_sub(1, std::memory_order_relaxed);
});
executor.run(*topology, [this, topology, completion, task_ptr]() mutable {
release(*task_ptr);
if (completion && *completion)
(*completion)();
});
return true;
else {
std::size_t queue_limit = frame_queue_limit();
if (pending_frame_tasks.size() >= queue_limit) {
reject(task);
return false;
}
if (task.plot_id)
queued_frame_plots.insert(task.plot_id);
record_submission(task);
pending_frame_tasks.push_back(Pending_Frame_Task{std::move(task), enqueue_ns});
return true;
}
}
if (submit_now) {
record_submission(task);
submit_to_executor(std::move(task), enqueue_ns);
return true;
}
reject(task);
return false;
}
void shutdown() {
if (shutting_down.exchange(true, std::memory_order_acq_rel))
return;
{
std::lock_guard<std::mutex> lock(admission_mutex);
pending_frame_tasks.clear();
queued_frame_plots.clear();
}
executor.wait_for_all();
}
@@ -215,25 +228,119 @@ public:
}
private:
bool admit(const Render_Executor_Task& task) {
if (!task.frame_job)
return true;
std::lock_guard<std::mutex> lock(admission_mutex);
std::size_t active = metrics->active_frame_jobs.load(std::memory_order_acquire);
std::size_t queued = metrics->queued_frame_jobs.load(std::memory_order_acquire);
std::size_t limit = std::max<std::size_t>(1, metrics->worker_count);
if (active + queued >= limit)
return false;
if (task.plot_id && !admitted_frame_plots.insert(task.plot_id).second)
return false;
return true;
void record_submission(Render_Executor_Task& task) {
metrics->submitted_tasks.fetch_add(1, std::memory_order_relaxed);
add_kind_count(*metrics, task.kind);
if (task.frame_job)
metrics->queued_frame_jobs.fetch_add(1, std::memory_order_relaxed);
if (task.frame_stat)
task.frame_stat->executor_at_submit = snapshot();
}
void release(const Render_Executor_Task& task) {
if (!task.frame_job || !task.plot_id)
void submit_to_executor(Render_Executor_Task task, std::uint64_t enqueue_ns) {
auto task_ptr = std::make_shared<Render_Executor_Task>(std::move(task));
auto completion = std::make_shared<Task>(std::move(task_ptr->completion));
auto run_record = std::make_shared<Task_Run_Record>();
auto topology = std::make_shared<tf::Taskflow>();
auto entry = topology->emplace([this, enqueue_ns, task_ptr, run_record]() mutable {
Render_Executor_Task& task = *task_ptr;
std::uint64_t begin_ns = steady_now_ns();
run_record->begin_ns = begin_ns;
std::uint64_t queue_wait_ns = begin_ns > enqueue_ns ? begin_ns - enqueue_ns : 0;
run_record->queue_wait_ns = queue_wait_ns;
metrics->queue_wait_total_ns.fetch_add(queue_wait_ns, std::memory_order_relaxed);
update_peak(metrics->queue_wait_max_ns, queue_wait_ns);
if (task.frame_job) {
metrics->queued_frame_jobs.fetch_sub(1, std::memory_order_relaxed);
metrics->active_frame_jobs.fetch_add(1, std::memory_order_relaxed);
}
});
auto exit = topology->emplace([this, task_ptr, run_record]() mutable {
Render_Executor_Task& task = *task_ptr;
std::uint64_t end_ns = steady_now_ns();
std::uint64_t begin_ns = run_record->begin_ns;
std::uint64_t queue_wait_ns = run_record->queue_wait_ns;
std::uint64_t run_ns = end_ns > begin_ns ? end_ns - begin_ns : 0;
if (task.frame_stat) {
Frame_Worker_Stat& stat = *task.frame_stat;
stat.task_count += std::max<std::uint32_t>(1, task.logical_task_count);
stat.peak_parallelism = static_cast<std::uint32_t>(std::max<std::uint64_t>(stat.peak_parallelism, metrics->peak_concurrency.load(std::memory_order_acquire)));
stat.queue_wait_total_ns += queue_wait_ns;
stat.queue_wait_max_ns = std::max(stat.queue_wait_max_ns, queue_wait_ns);
stat.worker_run_total_ns += run_ns;
stat.parallel_stage_wall_ns += run_ns;
stat.executor_at_finish = snapshot();
}
if (task.frame_job)
metrics->active_frame_jobs.fetch_sub(1, std::memory_order_relaxed);
});
if (task_ptr->build_taskflow) {
task_ptr->build_taskflow(*topology, entry, exit);
}
else {
auto work = topology->emplace([task_ptr]() mutable {
if (task_ptr->work)
task_ptr->work();
});
entry.precede(work);
work.precede(exit);
}
executor.run(*topology, [this, topology, completion, task_ptr]() mutable {
release_and_drain_next(*task_ptr);
if (completion && *completion)
(*completion)();
});
}
std::size_t frame_admission_limit() const {
return std::max<std::size_t>(1, metrics->worker_count);
}
std::size_t frame_queue_limit() const {
return std::max<std::size_t>(1, frame_admission_limit() * 2);
}
bool plot_already_pending_or_admitted_locked(Plot_Execution_Id plot_id) const {
if (!plot_id)
return false;
return admitted_frame_plots.find(plot_id) != admitted_frame_plots.end()
|| queued_frame_plots.find(plot_id) != queued_frame_plots.end();
}
void admit_frame_locked(Plot_Execution_Id plot_id) {
++admitted_frame_count;
if (plot_id)
admitted_frame_plots.insert(plot_id);
}
std::optional<Pending_Frame_Task> take_next_frame_task_locked() {
if (shutting_down.load(std::memory_order_acquire) || pending_frame_tasks.empty())
return std::nullopt;
if (admitted_frame_count >= frame_admission_limit())
return std::nullopt;
Pending_Frame_Task next = std::move(pending_frame_tasks.front());
pending_frame_tasks.pop_front();
if (next.task.plot_id)
queued_frame_plots.erase(next.task.plot_id);
admit_frame_locked(next.task.plot_id);
return next;
}
void release_and_drain_next(const Render_Executor_Task& task) {
if (!task.frame_job)
return;
std::lock_guard<std::mutex> lock(admission_mutex);
admitted_frame_plots.erase(task.plot_id);
std::optional<Pending_Frame_Task> next;
{
std::lock_guard<std::mutex> lock(admission_mutex);
if (admitted_frame_count)
--admitted_frame_count;
if (task.plot_id)
admitted_frame_plots.erase(task.plot_id);
next = take_next_frame_task_locked();
}
if (next)
submit_to_executor(std::move(next->task), next->enqueue_ns);
}
void reject(const Render_Executor_Task& task) {
@@ -250,6 +357,9 @@ private:
std::shared_ptr<tf::ObserverInterface> observer;
std::mutex admission_mutex;
std::unordered_set<Plot_Execution_Id> admitted_frame_plots;
std::unordered_set<Plot_Execution_Id> queued_frame_plots;
std::deque<Pending_Frame_Task> pending_frame_tasks;
std::size_t admitted_frame_count{};
std::atomic_bool shutting_down{false};
};
+6
View File
@@ -2,17 +2,23 @@
#include "../architecture/Render_Config.h"
#include "../architecture/global.h"
#include "../base/Task.h"
#include <functional>
#include <memory>
#include <taskflow/taskflow.hpp>
namespace renderive {
struct Render_Executor_Task {
using Taskflow_Builder = std::function<void(tf::Taskflow&, tf::Task, tf::Task)>;
Plot_Execution_Id plot_id{};
std::uint64_t frame_id{};
Render_Task_Kind kind = Render_Task_Kind::Primitive;
Task work;
Taskflow_Builder build_taskflow;
Task completion;
std::shared_ptr<Frame_Worker_Stat> frame_stat;
std::uint32_t logical_task_count{1};
bool frame_job{};
};
+44 -7
View File
@@ -113,15 +113,29 @@ public:
plot_execution_id,
frame.frame_id,
Render_Task_Kind::Frame,
[job]() {
job->owner->execute_frame_job(job);
Task{},
[job](tf::Taskflow& taskflow, tf::Task entry, tf::Task exit) {
auto prepare = taskflow.emplace([job]() {
job->owner->prepare_frame_job(job);
});
auto draw = taskflow.emplace([job]() {
job->owner->draw_frame_job(job);
});
auto finalize = taskflow.emplace([job]() {
job->owner->finalize_frame_job(job);
});
entry.precede(prepare);
prepare.precede(draw);
draw.precede(finalize);
finalize.precede(exit);
},
[job]() {
Task([job]() {
Global::instance()->render_scheduler().post([job]() {
job->owner->complete_frame_job(job);
});
},
}),
job->worker_stat,
3,
true
});
if (submitted)
@@ -137,8 +151,12 @@ public:
return false;
}
void execute_frame_job(const std::shared_ptr<Frame_Render_Job>& job) {
if (!job || !job->buffer || !job->lifecycle || !job->render_snapshot.context || job->render_snapshot.destroying())
static bool frame_job_valid(const std::shared_ptr<Frame_Render_Job>& job) {
return job && job->buffer && job->lifecycle && job->render_snapshot.context && !job->render_snapshot.destroying();
}
void prepare_frame_job(const std::shared_ptr<Frame_Render_Job>& job) {
if (!frame_job_valid(job))
return;
Frame_Lifecycle_Record& frame = *job->lifecycle;
frame.prepare_begin_ns = steady_now_ns();
@@ -150,8 +168,13 @@ public:
++frame.prepared_output_count;
}
frame.prepare_end_ns = steady_now_ns();
context->first_prepare_data.store(false, std::memory_order_release);
job->render_snapshot.context->first_prepare_data.store(false, std::memory_order_release);
}
void draw_frame_job(const std::shared_ptr<Frame_Render_Job>& job) {
if (!frame_job_valid(job) || !job->lifecycle->prepare_end_ns)
return;
Frame_Lifecycle_Record& frame = *job->lifecycle;
job->buffer->image.resize(job->render_snapshot.size.width, job->render_snapshot.size.height);
job->buffer->image.fill(job->render_snapshot.background_color);
Canvas canvas(job->buffer->image);
@@ -161,6 +184,12 @@ public:
job->renderables.front().renderable->render_tree_snapshot(canvas, job->render_snapshot, job->renderables, 0);
frame.draw_end_ns = steady_now_ns();
frame.render_end_ns = steady_now_ns();
}
void finalize_frame_job(const std::shared_ptr<Frame_Render_Job>& job) {
if (!frame_job_valid(job) || !job->lifecycle->render_end_ns)
return;
Frame_Lifecycle_Record& frame = *job->lifecycle;
job->buffer->version.store(frame.frame_id, std::memory_order_release);
for (const auto& update_state : job->buffer->presented_update_states) {
if (update_state)
@@ -168,6 +197,14 @@ public:
}
}
void execute_frame_job(const std::shared_ptr<Frame_Render_Job>& job) {
if (!job || !job->buffer || !job->lifecycle || !job->render_snapshot.context || job->render_snapshot.destroying())
return;
prepare_frame_job(job);
draw_frame_job(job);
finalize_frame_job(job);
}
void complete_frame_job(const std::shared_ptr<Frame_Render_Job>& job) {
if (!job || !job->buffer || !job->lifecycle)
return;
@@ -1,763 +0,0 @@
# Renderive 最新代码验收:文本不可见、Performance 不完整与 Paint 崩溃
## 1. 验收结论
当前版本未达到目标,不能通过验收。
已经完成:
- Taskflow v4.1.0 已接入。
- Plot 的完整 Frame Job 已从 Asio scheduler 线程移到 Taskflow worker。
- 不同 Plot 的完整帧可以在不同 worker 上并发执行。
- 外层 `Rendering / Middle / Painting` 三帧角色和 `Render / Paint / User` 负反馈骨架已经存在。
- Radio Mock 定时器此前的裸指针 UAF 已改为 `QPointer`,并保存、停止和销毁 `QTimer`
仍未完成:
- 轴刻度文本和单位文本不可见。
- Performance_Shower 在 Radio 中没有真正启用。
- Performance 快照即使启用,也会因为字体、颜色、宽度和滚动路径问题显示为空、过窄或被裁剪。
- `try_begin_paint()` 存在确定的数据竞争,能够解释当前崩溃堆栈。
- Curve 和 Waterfall 没有使用 Taskflow 做帧内并行。
- Taskflow 当前只有单节点 topology,没有 Plot admission、公平准入和有界背压。
- Performance 任务与主渲染任务共用同一 Executor,会继续干扰主图。
- 所谓 `Bounded_MPSC_Queue` 实际是 `mutex + deque`,不是无锁,也不是严格 try-and-drop。
- 当前测试没有覆盖文本渲染、Performance resize、角色交换竞态和多 Plot 压力场景。
---
## 2. 轴刻度和文本完全不可见的根因
### 2.1 `Canvas::draw_text()` 要求有效字体,但项目没有初始化默认字体
`Core/render/Canvas.cpp`
```cpp
void Canvas::draw_text(PointF position, std::string_view text)
{
if (text.empty() || !impl_->current_font.valid())
return;
}
```
当前工程中:
- `Font_Registry` 已定义。
- 没有任何代码调用 `load_file()``load_memory()`
- 没有任何代码调用 `set_default_face()`
- `Abs_Axis` 没有刻度文本字体状态。
- `Performance_Shower_Render_State::font` 默认无 `Font_Face`
因此轴刻度和 Performance 文本都会在 `draw_text()` 入口直接返回。
这是截图中轴标签和 Performance 文本同时不可见的共同根因。
### 2.2 文本使用 brush 绘制,但调用者普遍只设置 pen
`Canvas::draw_text()` 调用:
```cpp
impl_->apply_brush();
impl_->ctx.fill_utf8_text(...);
```
但轴绘制只调用:
```cpp
canvas.set_pen(Pen{.color = s->color});
canvas.draw_text(...);
```
没有设置 brush。
Performance 也只调用:
```cpp
canvas.set_pen(Pen{state.foreground});
canvas.draw_text(...);
```
而此前用于背景的 brush 仍然是白色。即使字体有效,Performance 文本也会成为白底白字。
同样问题还存在于:
- `Label`
- `Color_Bar`
- `Hover_Info`
- `Spectrum` 标签
- `Multi_Select_Rect`
这不是 Axis 单点 Bug,而是 `Canvas` 文本颜色语义与整个调用层的既有语义不一致。
### 2.3 `draw_text(RectF)` 存在重复 Y 偏移
矩形版本先按 ascent 计算 Y:
```cpp
y += metrics.ascent;
```
随后又调用点版本,而点版本继续执行:
```cpp
position.y + current_font.size
```
因此矩形文本会被再次向下移动。
这会导致:
- 文本垂直位置错误。
- 靠近底部的文本被裁剪。
- Performance 行距和可见行数与实际绘制不一致。
### 2.4 Axis 自己没有设置刻度字体
`Core/Axis/Abs_Axis_p.h` 的刻度绘制没有:
```cpp
canvas.set_font(...);
```
代码还存在无效逻辑:
```cpp
Text_Metrics tm = canvas.measure_text(!s->unit_text_font.valid() ? "0" : " ");
```
它既没有把 `unit_text_font` 设置给 Canvas,也没有真正测量刻度字体。
### 2.5 Radio 把轴放在视口边缘,没有给标签预留空间
Radio 中多个 Plot 的 resize 逻辑类似:
```cpp
x_axis->set_x(0);
x_axis->set_y(height() - 1);
x_axis->set_pixel_size(width());
y_axis->set_x(0);
y_axis->set_y(0);
y_axis->set_pixel_size(height());
```
这意味着:
- 横轴位于最后一个像素。
- 横轴标签容易越出底部。
- 首尾标签容易越出左右边缘。
- 纵轴标签占用数据绘图区。
- 单位文本没有专用 margin。
Renderive Demo 中已经有正确方向的参考:
```cpp
int left_margin = 20;
int right_margin = 20;
int top_margin = 20;
int bottom_margin = 20;
```
Radio 没有采用该布局。
### 2.6 文本修复要求
一次性修复文本系统,不要只在 Axis 打补丁:
1. 明确 `Canvas::draw_text(PointF)` 的语义为“左上角”。
2. 点版本使用 `metrics.ascent` 计算 Blend2D baseline,不再直接加 `font.size`
3. 矩形版本只计算左上角,然后调用点版本,禁止重复 baseline 偏移。
4. 为保持现有调用语义,文本颜色使用当前 pen,而不是当前 brush。
5. 增加 Renderive 唯一默认字体配置。
6. Canvas 在当前字体无效时使用 Render Runtime 默认字体。
7. Axis 使用默认字体或显式文本字体。
8. Performance 使用相同默认字体。
9. 新增 Canvas、Axis、Label、Performance 的像素测试。
10. Radio 为坐标轴保留实际 margins。
---
## 3. Performance_Shower 不显示或显示不全的根因
### 3.1 Radio 根本没有启用 Performance overlay
`radio_show_performance_overlay()` 已定义,但没有任何调用者。
全 Radio 工程中没有调用:
```cpp
set_performance_shower_enabled(...)
```
因此即使设置:
```text
RADIO_SHOW_PERFORMANCE_OVERLAY=1
```
也不会启用 Performance_Shower。
`use_Performance_shower` 也是未使用的死变量。
应在 Plot 初始化完成后执行:
```cpp
if (radio_show_performance_overlay())
renderive::set_performance_shower_enabled(*plot->core(), true);
```
并直接删除未使用的旧变量。
### 3.2 Performance 默认字体无效
```cpp
struct Performance_Shower_Render_State : Render_State {
Font font;
};
```
默认 `Font` 没有 face。
因此:
```cpp
metrics = measure_canvas.measure_text("M");
```
返回全零。
随后:
```cpp
line_height = 1;
panel_width = margin + scrollbar ;
```
最终 Performance Image 可能只有约 30 像素宽,而且文本完全不绘制。
这直接解释了“显示出来还是初始化/异常大小”以及“性能测试看不见”。
### 3.3 Performance 文本颜色设置错误
绘制背景后,当前 brush 是白色:
```cpp
canvas.set_brush(Brush{state.background, Brush_Style::Solid});
canvas.fill_rect(...);
```
随后只设置黑色 pen
```cpp
canvas.set_pen(Pen{state.foreground});
```
`draw_text()` 使用 brush,因此文本仍按白色 brush 绘制。
结果为白底白字。
### 3.4 宽度计算不正确
当前使用:
```cpp
metrics.width * line.size()
```
其中 `metrics.width` 是字符 `"M"` 的宽度。
问题:
- 比例字体每个字符宽度不同。
- 中文、数字和标点宽度不同。
- UTF-8 的 `string::size()` 是字节数,不是字符数。
- 估算宽度可能严重错误。
必须逐行执行:
```cpp
measure_text(line)
```
### 3.5 只有垂直滚动,没有换行或水平滚动
Panel 宽度被限制为 viewport 宽度:
```cpp
panel_width = min(content_width, viewport.width);
```
文本区域随后直接 clip。
长行没有:
- 自动换行。
- 水平滚动。
- 多列布局。
- 字段折叠。
因此即使字体和颜色修好,长行右侧仍然不可见。
建议在 Performance worker 中按当前 viewport 宽度进行确定性换行,再生成 Image。保留垂直滚动即可,不增加第二套水平滚动状态。
### 3.6 当前滚轮处理实际上不可达
Performance_Shower 使用:
```cpp
created_shower->init_root(&plot);
```
它不是被测 Plot root 的 child。
Plot 的事件命中只遍历被测 root tree,因此:
```cpp
Performance_Shower_Private::wheel_event()
```
不会被调用。
当前虽然画了 scrollbar,但用户无法滚动,内容自然无法完整查看。
正确方案:
- Performance_Shower 不再继承 Renderable。
- Qt overlay 层根据不可变 snapshot 的 `destination_rect` 判断 wheel。
- Qt 只提交 `scroll_delta` 命令给 Performance worker。
- worker 重新生成完整 Image Snapshot。
### 3.7 resize 后没有主动重建快照
Performance snapshot 的 viewport 信息取自“最近一个完整帧”。
Plot resize 只增加 `viewport_version`,没有立即通知 Performance worker。
Qt 绘制时又严格要求:
```cpp
snapshot.viewport_version == current_viewport_version
```
因此 resize 后到下一完整帧返回之前,旧 snapshot 会被直接跳过。
如果渲染因其他问题没有成功返回,Performance 会长期消失。
应在 viewport 改变时向 Performance worker提交最新 `Size + viewport_version` 快照,并请求重建,不依赖下一帧统计样本。
### 3.8 Performance 仍会干扰主渲染
Performance 聚合、Image 构建和每帧 CSV task 都提交到同一个 Render Executor。
尤其:
```cpp
write_frame_performance_log(frame)
```
无论日志是否开启,都会先向 Taskflow 提交一个任务;是否开启是在任务内部才判断。
结果:
- 每个完成帧额外创建一个 Taskflow topology。
- 污染线程池统计。
- 与 Curve、Waterfall 和普通 Plot Frame Job 竞争 worker。
- Performance 仍存在 CPU 侧观察者效应。
修复:
1. 日志未启用时,不提交任何任务。
2. 日志并入 Performance 专用 worker。
3. Performance 使用独立单 worker executor 或专用后台线程。
4. Performance 只观察 Render Executor snapshot,不占用 Render Executor worker。
---
## 4. `try_begin_paint()` 崩溃的确定根因
用户提供的堆栈落在:
```text
Plot_Frame_Pipeline::try_begin_paint()
Plot_Frame_Pipeline.cpp:156
```
第 156 行是:
```cpp
result.returned_frame = old_painting->metadata;
```
### 4.1 具体竞态
当前流程:
```text
读取 old_painting 指针
读取 new_painting 指针
Middle ↔ Painting 交换
try_swap_role 内部释放两个物理帧 busy 标记
UI 再写 old_painting metadata
UI 再复制 old_painting metadata
```
交换成功后:
- `old_painting` 已经成为 Middle。
- 它不再被 UI lease 保护。
- scheduler 可以立即执行 `Rendering ↔ Middle`
- 该物理帧可能立刻成为 Rendering。
- worker 随后会执行:
```cpp
buffer->metadata = {};
```
并重新写入:
```cpp
renderable_stats
renderable_cache_stats
presented_update_states
```
与此同时 UI 正在按值复制 `old_painting->metadata`
`Frame_Lifecycle_Record` 内含多个 `std::vector`。并发 reset、push 和 copy 会导致:
- 读取失效指针。
- vector 控制块损坏。
- 越界访问。
- 崩溃。
该竞态与堆栈完全吻合。
### 4.2 `try_publish_rendered()` 也有同类问题
角色交换成功后,代码继续通过裸 `Plot_Frame*`
- 写 rendered frame metadata。
- 写新 Rendering frame metadata。
- 返回 `superseded_middle` 裸指针。
- 返回 `dropped_frame` 裸指针。
这些物理帧在返回后可能已经被其他角色重新使用。
因此问题不是第 156 行单点,而是“角色交换后继续使用未持 lease 的物理帧指针”的整体设计错误。
### 4.3 `paint_request_pending` 也可能丢事件
`try_begin_paint()` 一开始执行:
```cpp
paint_request_pending.exchange(false)
```
如果后续角色交换或 Painting acquire 失败,pending 状态已经被清除。
结果:
- 当前更新请求被消费。
- 新帧没有成功进入 Painting。
- Qt 不一定再收到 update。
- 界面可能停在黑屏或旧帧。
### 4.4 正确修复方案
不要在角色交换后复制可变物理帧 metadata。
推荐直接替换:
```cpp
struct Plot_Frame {
Image image;
std::shared_ptr<Frame_Lifecycle_Record> metadata;
};
```
语义:
1. 开始 Rendering 时创建新的 metadata 对象。
2. Rendering、Middle、Painting 只携带其 shared metadata。
3. `Painting → Middle` 时,在角色仍受保护的交换回调内取得 `shared_ptr<const Frame_Lifecycle_Record>`
4. UI 返回的是 shared pointer,不复制大型 vector。
5. 物理帧被重新用作 Rendering 时,只替换自己的 metadata shared pointer。
6. Performance worker消费 immutable metadata snapshot。
7. `Frame_Publish_Result``Frame_Consume_Result` 禁止返回裸 `Plot_Frame*`
同时扩展 Triple Buffer API,使成功回调在两个物理 buffer 仍被标记 busy 时执行:
```cpp
try_swap_role(left, right, can_swap, on_success_locked)
```
所有以下操作必须在 locked callback 中完成:
- 读取旧角色 metadata shared pointer。
- 写 transition 时间。
- 更新 role-enter 时间。
- 捕获 superseded record。
- 捕获 returned record。
禁止交换完成、释放 busy 后再访问旧角色指针。
`paint_request_pending` 只有在成功获得 Painting lease 后才能清除;失败时必须保留或重新发布 update。
---
## 5. Taskflow 与多 Plot 目标的当前状态
### 5.1 已完成的部分
Taskflow 已真正接入。
每个 Plot 的完整 Frame Job 会提交给 Taskflow,因此多个 Plot 可以在不同 worker 上执行。
这已经解决了“所有 Plot 完整绘制都在单一 scheduler 线程串行执行”的旧问题。
### 5.2 仍未完成的部分
当前每次提交只创建:
```text
一个 Taskflow
└── 一个 task
└── execute_frame_job()
```
`execute_frame_job()` 内部仍然串行:
```text
遍历 prepare_data
→ Curve 计算
→ Waterfall 计算
→ 创建整张 Image
→ 遍历整棵 Renderable Tree
→ 合成
```
全项目没有实际提交:
```text
Render_Task_Kind::Curve
Render_Task_Kind::Waterfall
Render_Task_Kind::Compose
```
因此:
- Curve 没有帧内并行。
- Waterfall 没有 Tile 并行。
- 一个重型 Plot 仍会长期占住一个 worker。
- 当 Plot 数量大于 worker 数时,轻量 Plot 仍可能等待重型 Plot。
### 5.3 `try_submit()` 没有 try 背压
当前 `Render_Executor::try_submit()` 除非 shutdown,否则始终调用:
```cpp
executor.run(...)
```
没有:
- 有界 topology 数。
- active Frame Job 上限。
- queued Frame Job 上限。
- round-robin Plot admission。
- 拒绝或降级策略。
因此名字叫 `try_submit`,实际并不是有界 try-and-drop。
### 5.4 没有 Plot 级公平准入
当前每个 Plot 依靠自己的 `job_state` 限制一帧在途,这是正确的基础。
但全局没有:
- ready Plot queue。
- round-robin admission。
- oldest-ready-time。
- 每轮每 Plot 一次准入。
Taskflow 只负责 worker 级 work-stealing,不保证 Plot 业务公平。
### 5.5 线程池指标部分不准确
Performance 中名称为 p95 的字段实际使用的是 max:
```text
p95_queue_wait_ns = queue_wait_max_ns
p95_task_duration_ns = task_duration_max_ns
```
不能将最大值显示为 P95。
必须实现固定容量直方图、ring sample 或真正的在线分位数统计;否则改名为 max。
---
## 6. 当前所谓无锁队列没有达到目标
`Bounded_MPSC_Queue` 当前实现是:
```cpp
std::mutex
std::deque
std::lock_guard
```
问题:
- 不是 lock-free。
- `try_push()` 可能等待 mutex。
- `drain()` 会持锁并分配 vector。
- Performance 的 `3→2` 采样不再是无锁。
- Curve、Waterfall 多生产者输入会互相阻塞。
当前实现解决了旧版 sequence 发布顺序错误,但没有达到最终目标。
必须替换为正确的 bounded MPSC ring
```text
每 slot sequence
固定容量
预分配 storage
try_push 不等待
try_pop 不等待
满时返回 false
空时返回 false
```
---
## 7. 一次性执行计划
本次不要再分 Axis、Performance 和三帧分别打补丁,按以下顺序一次完成。
### 工作块一:修复 Canvas 文本契约和默认字体
1. 重写 `Canvas::draw_text` 的坐标与颜色语义。
2. 文本使用 pen color。
3. 修复矩形版本双重 Y 偏移。
4. 增加 Render Runtime 默认字体。
5. Radio 启动时加载可显示中文的默认字体。
6. Axis、Performance、Label、Color_Bar 统一使用该契约。
7. 添加像素级文本测试。
### 工作块二:修复 Axis 布局
1. Axis 使用有效字体和正确 text pen。
2. 根据 tick_length 正负计算标签在轴线两侧的位置。
3. 使用实际 text metrics 放置标签。
4. 首尾标签不得越界。
5. Radio 所有 Plot 使用统一 plot content rect 和 margins。
6. Waterfall、Spectrum、Audio、Afterglow 全部复用同一布局函数。
7. 禁止每个 Radio Plot 重复手写一套错误 resize。
### 工作块三:修复三帧 metadata 生命周期
1. `Plot_Frame::metadata` 改为 immutable shared record。
2. Triple Buffer swap 增加 locked success callback。
3. 删除交换后访问裸 Plot_Frame 指针的代码。
4. Publish/Consume result 不再返回裸帧指针。
5. pending update 只在成功 acquire 后清除。
6. 增加确定性交错测试,复现旧第 156 行竞态。
7. TSan 覆盖 publish、paint、next render 同时发生。
### 工作块四:完成 Taskflow 帧内任务图
1. 一个 Frame Job 构建一个真正的 Taskflow DAG。
2. Curve 按像素桶或样本范围分片。
3. Waterfall 使用二维 Tile。
4. 每个 worker 写独立输出。
5. 单一 compose task 稳定合并。
6. 禁止多个 worker 同时写一个 Blend2D Canvas。
7. 删除没有实际消费者的 task kind,或让所有 kind 真正被使用。
### 工作块五:增加 Plot admission 和背压
1. scheduler 管理 ready Plot queue。
2. 每 Plot 最多一帧在途。
3. 全局 active topology 有界。
4. round-robin admission。
5. 脏版本合并。
6. 队列满不等待,记录 dropped attempt。
7. Performance task 不进入主 Render Executor。
### 工作块六:重写 Performance overlay
1. Radio 真正调用 enable。
2. 删除未使用的 `use_Performance_shower`
3. Performance 不再继承 Renderable。
4. 使用独立后台 worker。
5. snapshot 包含 Image、viewport、destination rect 和 wrapped lines。
6. resize 主动触发 snapshot rebuild。
7. 按实际 `measure_text(line)` 计算宽度。
8. 按 viewport 宽度换行。
9. Qt overlay 单独路由 wheel。
10. 日志关闭时不提交任何任务。
11. 主 Frame/paint 统计在 overlay 前截止。
### 工作块七:批量验收
必须一次运行:
- Canvas text pixel test。
- Axis four-side layout test。
- Performance enable/resize/wrap/scroll test。
- `try_begin_paint` deterministic race test。
- 三帧一百万次转换。
- 多 Plot 60 秒压力测试。
- Curve 1/N worker 一致性。
- Waterfall 1/N worker一致性。
- bounded MPSC 确定性交错。
- Plot 销毁时在途 Frame Job。
- ASan。
- TSan。
- UBSan。
- Radio Qt 集成测试。
---
## 8. 最终验收标准
只有同时满足以下条件才算完成:
- 轴线、主刻度、子刻度、刻度文本和单位文本均可见。
- 中文文本正常显示。
- Radio 四个图表均预留正确 axis margins。
- Performance overlay 通过环境变量或业务配置真正启用。
- Performance 文本颜色正确。
- Performance resize 后不使用旧尺寸 snapshot。
- 长 Performance 行完整换行或可访问。
- Performance wheel 可以滚动全部内容。
- `try_begin_paint()` 不再复制正在被复用的物理帧 metadata。
- Publish/Consume 不返回裸物理帧指针。
- Taskflow topology 包含真实 Curve/Waterfall/Compose 子任务。
- 多 Plot 不永久饥饿。
- Executor 有界且有拒绝统计。
- Performance 不占用主 Render Executor。
- bounded MPSC 不使用 mutex/deque。
- ASan、TSan、UBSan 全部通过。
- 用户提供的崩溃堆栈无法再复现。
---
## 9. 当前最优先修复顺序
如果只按严重程度排序:
```text
P0 try_begin_paint metadata 数据竞争
P0 Canvas 默认字体和文本颜色契约
P0 Radio 未启用 Performance overlay
P1 Performance resize、换行和 wheel 路由
P1 Radio axis margins
P1 Performance 与主 Render Executor 分离
P2 Taskflow 帧内 Curve/Waterfall DAG
P2 Plot admission 和有界背压
P2 真正 bounded lock-free MPSC
```
在 P0 完成前,当前版本不应继续进行性能调优,也不应声明三帧模型稳定。
+157 -4
View File
@@ -261,7 +261,7 @@ TEST(Renderive_Render_Executor, FrameAdmissionIsBounded) {
Render_Executor executor(Render_Runtime_Config{1});
std::atomic_bool entered{false};
std::atomic_bool release{false};
std::atomic_bool completed{false};
std::atomic_int completed{0};
auto first_stat = std::make_shared<Frame_Worker_Stat>();
ASSERT_TRUE(executor.try_submit(Render_Executor_Task{
1,
@@ -272,29 +272,182 @@ TEST(Renderive_Render_Executor, FrameAdmissionIsBounded) {
while (!release.load(std::memory_order_acquire))
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}),
{},
Task([&]() {
completed.store(true, std::memory_order_release);
completed.fetch_add(1, std::memory_order_acq_rel);
}),
first_stat,
1,
true
}));
ASSERT_TRUE(wait_until([&]() {
return entered.load(std::memory_order_acquire);
}));
auto second_stat = std::make_shared<Frame_Worker_Stat>();
EXPECT_FALSE(executor.try_submit(Render_Executor_Task{
EXPECT_TRUE(executor.try_submit(Render_Executor_Task{
2,
1,
Render_Task_Kind::Frame,
Task([]() {}),
Task([]() {}),
{},
Task([&]() {
completed.fetch_add(1, std::memory_order_acq_rel);
}),
second_stat,
1,
true
}));
auto third_stat = std::make_shared<Frame_Worker_Stat>();
EXPECT_TRUE(executor.try_submit(Render_Executor_Task{
3,
1,
Render_Task_Kind::Frame,
Task([]() {}),
{},
Task([&]() {
completed.fetch_add(1, std::memory_order_acq_rel);
}),
third_stat,
1,
true
}));
auto duplicate_stat = std::make_shared<Frame_Worker_Stat>();
EXPECT_FALSE(executor.try_submit(Render_Executor_Task{
2,
2,
Render_Task_Kind::Frame,
Task([]() {}),
{},
Task([]() {}),
duplicate_stat,
1,
true
}));
auto overflow_stat = std::make_shared<Frame_Worker_Stat>();
EXPECT_FALSE(executor.try_submit(Render_Executor_Task{
4,
1,
Render_Task_Kind::Frame,
Task([]() {}),
{},
Task([]() {}),
overflow_stat,
1,
true
}));
release.store(true, std::memory_order_release);
ASSERT_TRUE(wait_until([&]() {
return completed.load(std::memory_order_acquire) == 3;
}));
executor.shutdown();
}
TEST(Renderive_Render_Executor, TaskflowTopologyRunsInFrameOrder) {
Render_Executor executor(Render_Runtime_Config{2});
std::atomic_int sequence{0};
std::atomic_bool completed{false};
auto stat = std::make_shared<Frame_Worker_Stat>();
ASSERT_TRUE(executor.try_submit(Render_Executor_Task{
1,
1,
Render_Task_Kind::Frame,
Task{},
[&sequence](tf::Taskflow& taskflow, tf::Task entry, tf::Task exit) {
auto prepare = taskflow.emplace([&sequence]() {
int expected = 0;
sequence.compare_exchange_strong(expected, 1, std::memory_order_acq_rel);
});
auto draw = taskflow.emplace([&sequence]() {
int expected = 1;
sequence.compare_exchange_strong(expected, 2, std::memory_order_acq_rel);
});
auto finalize = taskflow.emplace([&sequence]() {
int expected = 2;
sequence.compare_exchange_strong(expected, 3, std::memory_order_acq_rel);
});
entry.precede(prepare);
prepare.precede(draw);
draw.precede(finalize);
finalize.precede(exit);
},
Task([&completed]() {
completed.store(true, std::memory_order_release);
}),
stat,
3,
true
}));
ASSERT_TRUE(wait_until([&]() {
return completed.load(std::memory_order_acquire);
}));
EXPECT_EQ(sequence.load(std::memory_order_acquire), 3);
EXPECT_EQ(stat->task_count, 3u);
executor.shutdown();
}
TEST(Renderive_Render_Executor, QueuedFramesRunRoundRobinByPlot) {
Render_Executor executor(Render_Runtime_Config{1});
std::atomic_bool first_entered{false};
std::atomic_bool release_first{false};
std::atomic_int order_index{0};
std::array<int, 3> order{};
ASSERT_TRUE(executor.try_submit(Render_Executor_Task{
1,
1,
Render_Task_Kind::Frame,
Task([&]() {
first_entered.store(true, std::memory_order_release);
while (!release_first.load(std::memory_order_acquire))
std::this_thread::sleep_for(std::chrono::milliseconds(1));
order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 1;
}),
{},
Task([]() {}),
std::make_shared<Frame_Worker_Stat>(),
1,
true
}));
ASSERT_TRUE(wait_until([&]() {
return first_entered.load(std::memory_order_acquire);
}));
ASSERT_TRUE(executor.try_submit(Render_Executor_Task{
2,
1,
Render_Task_Kind::Frame,
Task([&]() {
order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 2;
}),
{},
Task([]() {}),
std::make_shared<Frame_Worker_Stat>(),
1,
true
}));
ASSERT_TRUE(executor.try_submit(Render_Executor_Task{
3,
1,
Render_Task_Kind::Frame,
Task([&]() {
order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 3;
}),
{},
Task([]() {}),
std::make_shared<Frame_Worker_Stat>(),
1,
true
}));
release_first.store(true, std::memory_order_release);
ASSERT_TRUE(wait_until([&]() {
return order_index.load(std::memory_order_acquire) == 3;
}));
EXPECT_EQ(order[0], 1);
EXPECT_EQ(order[1], 2);
EXPECT_EQ(order[2], 3);
executor.shutdown();
}