完美一版
This commit is contained in:
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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 完成前,当前版本不应继续进行性能调优,也不应声明三帧模型稳定。
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user