diff --git a/Core/execution/Render_Executor.cpp b/Core/execution/Render_Executor.cpp index 99a87a0..0baff20 100644 --- a/Core/execution/Render_Executor.cpp +++ b/Core/execution/Render_Executor.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -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()), @@ -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(); - auto task_ptr = std::make_shared(std::move(task)); - auto completion = std::make_shared(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 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::max(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 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 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(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(std::move(task)); + auto completion = std::make_shared(std::move(task_ptr->completion)); + auto run_record = std::make_shared(); + auto topology = std::make_shared(); + 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(1, task.logical_task_count); + stat.peak_parallelism = static_cast(std::max(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(1, metrics->worker_count); + } + + std::size_t frame_queue_limit() const { + return std::max(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 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 lock(admission_mutex); - admitted_frame_plots.erase(task.plot_id); + std::optional next; + { + std::lock_guard 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 observer; std::mutex admission_mutex; std::unordered_set admitted_frame_plots; + std::unordered_set queued_frame_plots; + std::deque pending_frame_tasks; + std::size_t admitted_frame_count{}; std::atomic_bool shutting_down{false}; }; diff --git a/Core/execution/Render_Executor.h b/Core/execution/Render_Executor.h index 17bb9f0..93540bd 100644 --- a/Core/execution/Render_Executor.h +++ b/Core/execution/Render_Executor.h @@ -2,17 +2,23 @@ #include "../architecture/Render_Config.h" #include "../architecture/global.h" #include "../base/Task.h" +#include #include +#include namespace renderive { struct Render_Executor_Task { + using Taskflow_Builder = std::function; + 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_stat; + std::uint32_t logical_task_count{1}; bool frame_job{}; }; diff --git a/Core/plot/Plot_Core.cpp b/Core/plot/Plot_Core.cpp index 889b1c9..5fa432a 100644 --- a/Core/plot/Plot_Core.cpp +++ b/Core/plot/Plot_Core.cpp @@ -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& job) { - if (!job || !job->buffer || !job->lifecycle || !job->render_snapshot.context || job->render_snapshot.destroying()) + static bool frame_job_valid(const std::shared_ptr& job) { + return job && job->buffer && job->lifecycle && job->render_snapshot.context && !job->render_snapshot.destroying(); + } + + void prepare_frame_job(const std::shared_ptr& 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& 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& 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& 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& job) { if (!job || !job->buffer || !job->lifecycle) return; diff --git a/Renderive_最新验收_文本_Performance_Paint崩溃修复计划.md b/Renderive_最新验收_文本_Performance_Paint崩溃修复计划.md deleted file mode 100644 index 33e786c..0000000 --- a/Renderive_最新验收_文本_Performance_Paint崩溃修复计划.md +++ /dev/null @@ -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 metadata; -}; -``` - -语义: - -1. 开始 Rendering 时创建新的 metadata 对象。 -2. Rendering、Middle、Painting 只携带其 shared metadata。 -3. `Painting → Middle` 时,在角色仍受保护的交换回调内取得 `shared_ptr`。 -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 完成前,当前版本不应继续进行性能调优,也不应声明三帧模型稳定。 diff --git a/test/Renderive_Core_Tests.cpp b/test/Renderive_Core_Tests.cpp index 85cf430..a0b994c 100644 --- a/test/Renderive_Core_Tests.cpp +++ b/test/Renderive_Core_Tests.cpp @@ -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(); 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(); - 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(); + 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(); + 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(); + 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(); + + 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 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(), + 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(), + 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(), + 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(); }