diff --git a/Core/architecture/Frame_Scheduler.h b/Core/architecture/Frame_Scheduler.h index 293e26f..955332a 100644 --- a/Core/architecture/Frame_Scheduler.h +++ b/Core/architecture/Frame_Scheduler.h @@ -80,6 +80,7 @@ struct Frame_Lifecycle_Record { std::uint64_t superseded_time_ns{}; Frame_Outcome outcome = Frame_Outcome::Render_Acquire_Dropped; Refresh_Control_Snapshot refresh; + Frame_Worker_Stat worker; std::uint64_t raw_input_count{}; std::uint64_t prepared_output_count{}; std::uint64_t renderable_cache_hit_count{}; @@ -91,6 +92,7 @@ struct Frame_Lifecycle_Record { std::uint64_t renderable_cache_image_area{}; std::uint64_t pixel_width{}; std::uint64_t pixel_height{}; + std::uint64_t viewport_version{}; std::vector renderable_stats; std::vector renderable_cache_stats; diff --git a/Core/architecture/Plot_Render_Context.h b/Core/architecture/Plot_Render_Context.h index 59338e9..f58811c 100644 --- a/Core/architecture/Plot_Render_Context.h +++ b/Core/architecture/Plot_Render_Context.h @@ -4,8 +4,10 @@ #include "Frame_Scheduler.h" #include #include +#include #include #include +#include #include namespace renderive { @@ -17,7 +19,7 @@ struct Update_State; struct Frame_Lifecycle_Observer { virtual ~Frame_Lifecycle_Observer() = default; virtual bool enabled() const = 0; - virtual void collect_frame(const Frame_Lifecycle_Record& frame) = 0; + virtual void collect_frame(std::shared_ptr frame) = 0; }; struct Timeline_Stream_Snapshot; @@ -38,8 +40,11 @@ struct Plot_Render_Context { std::atomic_bool first_prepare_data{true}; std::atomic_int render_width{0}; std::atomic_int render_height{0}; + std::atomic viewport_version{0}; std::atomic background_rgba{0xff000000u}; std::shared_ptr frame_lifecycle_observer; + std::mutex performance_update_callback_mutex; + std::function performance_update_callback; std::atomic owner{}; }; diff --git a/Core/architecture/Render_Config.h b/Core/architecture/Render_Config.h index 4dfb27f..985fb10 100644 --- a/Core/architecture/Render_Config.h +++ b/Core/architecture/Render_Config.h @@ -1,6 +1,57 @@ #pragma once +#include #include namespace renderive { +using Plot_Execution_Id = std::uint64_t; + +enum class Render_Task_Kind : std::uint8_t { + Frame, + Curve, + Waterfall, + Primitive, + Compose, + Performance +}; + +struct Render_Runtime_Config { + std::size_t worker_count{}; +}; + +struct Render_Executor_Snapshot { + std::size_t worker_count{}; + std::size_t active_workers{}; + std::size_t idle_workers{}; + std::size_t active_frame_jobs{}; + std::size_t queued_frame_jobs{}; + std::uint64_t submitted_tasks{}; + std::uint64_t started_tasks{}; + std::uint64_t completed_tasks{}; + std::uint64_t rejected_frame_jobs{}; + std::uint64_t dropped_frame_attempts{}; + std::uint64_t peak_concurrency{}; + std::uint64_t average_queue_wait_ns{}; + std::uint64_t p95_queue_wait_ns{}; + std::uint64_t average_task_duration_ns{}; + std::uint64_t p95_task_duration_ns{}; + std::uint64_t curve_tasks{}; + std::uint64_t waterfall_tasks{}; + std::uint64_t primitive_tasks{}; + std::uint64_t compose_tasks{}; + std::uint64_t performance_tasks{}; +}; + +struct Frame_Worker_Stat { + Render_Executor_Snapshot executor_at_submit; + Render_Executor_Snapshot executor_at_finish; + std::uint32_t task_count{}; + std::uint32_t peak_parallelism{}; + std::uint64_t queue_wait_total_ns{}; + std::uint64_t queue_wait_max_ns{}; + std::uint64_t worker_run_total_ns{}; + std::uint64_t parallel_stage_wall_ns{}; + std::uint64_t rejected_task_count{}; +}; + enum class Refresh_Limit_Source : std::uint8_t { Unknown, Render, diff --git a/Core/architecture/Render_Data.h b/Core/architecture/Render_Data.h index f044ad8..e135942 100644 --- a/Core/architecture/Render_Data.h +++ b/Core/architecture/Render_Data.h @@ -159,8 +159,11 @@ struct Render_Data { Render_State* state_by_index(std::uint8_t index) const { return state_buffers[index]; } - Triple_Buffer_Lease mark_state_role(std::uint8_t role) { - return state_control.mark_use_role(role); + Triple_Buffer_Lease try_mark_state_role(std::uint8_t role) { + auto record = state_control.try_acquire_role(role); + if (record.result != Triple_Buffer_Result::Success) + return {}; + return record.lease; } void unmark_state(Triple_Buffer_Lease lease) { state_control.unmark_use(lease); diff --git a/Core/architecture/Render_Lease.h b/Core/architecture/Render_Lease.h index 2763f3e..08c8bf6 100644 --- a/Core/architecture/Render_Lease.h +++ b/Core/architecture/Render_Lease.h @@ -55,14 +55,25 @@ struct Typed_Render_Data_Base : Base { template requires std::derived_from void set_state_value(Field Source_State::* field, Field value) { - auto lease = this->state_control.mark_use_role(State_Edit); + auto record = this->state_control.try_acquire_role(State_Edit); + if (record.result != Triple_Buffer_Result::Success) + return; + auto lease = record.lease; auto state = state_data_by_index(lease.index); Render_Edit_Guard guard(this, lease); state->*field = std::move(value); } void sync_state_pipeline() override { - Triple_Buffer_Lease edit_lease = this->state_control.mark_use_role(State_Edit); - Triple_Buffer_Lease ready_lease = this->state_control.mark_use_role(State_Ready); + auto edit_record = this->state_control.try_acquire_role(State_Edit); + if (edit_record.result != Triple_Buffer_Result::Success) + return; + auto ready_record = this->state_control.try_acquire_role(State_Ready); + if (ready_record.result != Triple_Buffer_Result::Success) { + this->state_control.unmark_use(edit_record.lease); + return; + } + Triple_Buffer_Lease edit_lease = edit_record.lease; + Triple_Buffer_Lease ready_lease = ready_record.lease; auto ready = reinterpret_cast(this->state_by_index(ready_lease.index)); auto edit = reinterpret_cast(this->state_by_index(edit_lease.index)); if (buffer_version_newer(edit->version, ready->version)) @@ -127,9 +138,20 @@ struct Renderable_Binding { } template struct Render_State_Lease : Immovable { - explicit Render_State_Lease(Data* data) : data(data), lease(data->state_control.mark_use_role(State_Edit)), state(reinterpret_cast(data->state_by_index(lease.index))) {} + explicit Render_State_Lease(Data* data) : data(data) { + auto record = data->state_control.try_acquire_role(State_Edit); + if (record.result == Triple_Buffer_Result::Success) { + lease = record.lease; + state = reinterpret_cast(data->state_by_index(lease.index)); + } + else { + fallback_owner = make_render_object(); + state = fallback_owner.get(); + } + } ~Render_State_Lease() { - data->unmark_state(lease); + if (lease) + data->unmark_state(lease); } State* operator->() { return state; @@ -140,11 +162,24 @@ struct Render_State_Lease : Immovable { Data* data{}; Triple_Buffer_Lease lease{}; State* state{}; + Render_Object_Owner fallback_owner; }; template struct Render_Edit_Lease : Immovable { - explicit Render_Edit_Lease(Data* data) : data(data), lease(data->state_control.mark_use_role(State_Edit)), state(reinterpret_cast(data->state_by_index(lease.index))) {} + explicit Render_Edit_Lease(Data* data) : data(data) { + auto record = data->state_control.try_acquire_role(State_Edit); + if (record.result == Triple_Buffer_Result::Success) { + lease = record.lease; + state = reinterpret_cast(data->state_by_index(lease.index)); + } + else { + fallback_owner = make_render_object(); + state = fallback_owner.get(); + } + } ~Render_Edit_Lease() { + if (!lease) + return; ++data->state_by_index(lease.index)->version; auto q = data->q_ptr; data->unmark_state(lease); @@ -160,11 +195,24 @@ struct Render_Edit_Lease : Immovable { Data* data{}; Triple_Buffer_Lease lease{}; State* state{}; + Render_Object_Owner fallback_owner; }; template struct Render_Input_Lease : Immovable { - explicit Render_Input_Lease(Data* data, bool notify_render = true) : data(data), notify_render(notify_render), lease(data->input_control.mark_use_role(Input_Edit)), input(reinterpret_cast(data->input_data_by_index(lease.index))) {} + explicit Render_Input_Lease(Data* data, bool notify_render = true) : data(data), notify_render(notify_render) { + auto record = data->input_control.try_acquire_role(Input_Edit); + if (record.result == Triple_Buffer_Result::Success) { + lease = record.lease; + input = reinterpret_cast(data->input_data_by_index(lease.index)); + } + else { + fallback_owner = make_render_object(); + input = fallback_owner.get(); + } + } ~Render_Input_Lease() { + if (!lease) + return; data->mark_input_dirty(lease, false); data->input_control.unmark_use(lease); if (notify_render && data->q_ptr) @@ -180,5 +228,6 @@ struct Render_Input_Lease : Immovable { bool notify_render{}; Triple_Buffer_Lease lease{}; Input* input{}; + Render_Object_Owner fallback_owner; }; } // namespace renderive diff --git a/Core/architecture/Renderable.cpp b/Core/architecture/Renderable.cpp index af1a175..4cc03b7 100644 --- a/Core/architecture/Renderable.cpp +++ b/Core/architecture/Renderable.cpp @@ -363,7 +363,7 @@ void Renderable::render_self(Canvas& canvas, const Plot_Render_Snapshot& snapsho void Renderable::render_self(Canvas& canvas, const Plot_Render_Snapshot& snapshot, const Renderable_Tree_Node* node) { if (!d_ptr) return; - auto lease = d_ptr->mark_state_role(State_Render); + auto lease = d_ptr->try_mark_state_role(State_Render); if (!lease) return; Renderable_Cache_Mode mode = get_cache_mode(); diff --git a/Core/architecture/Timeline_Stream.cpp b/Core/architecture/Timeline_Stream.cpp index d8c9559..0fe227f 100644 --- a/Core/architecture/Timeline_Stream.cpp +++ b/Core/architecture/Timeline_Stream.cpp @@ -34,45 +34,44 @@ int Timeline_Stream::push_time(Time_Of_Day time) { next_tick_value.compare_exchange_strong(expected, 1, std::memory_order_acq_rel, std::memory_order_acquire); tick = 0; } - std::uint64_t sequence = write_sequence.fetch_add(1, std::memory_order_acq_rel); - auto sample = std::make_shared(Sample{sequence, tick, time}); - time_slots[sequence % Max_Buffered_Time_Count].store(std::move(sample), std::memory_order_release); + { + std::lock_guard lock(samples_mutex); + if (samples.size() >= Max_Buffered_Time_Count) + samples.pop_front(); + samples.push_back(Sample{tick, time}); + version_value.fetch_add(1, std::memory_order_release); + } return tick; } Timeline_Stream_Snapshot Timeline_Stream::capture(int time_point_size, int tick_space, bool start_coord_to_end_coord) const { time_point_size = std::max(1, time_point_size); tick_space = std::max(1, tick_space); Timeline_Stream_Snapshot snapshot; - std::uint64_t write = write_sequence.load(std::memory_order_acquire); - snapshot.version = write + 1; + std::pmr::vector captured; + { + std::lock_guard lock(samples_mutex); + std::uint64_t version = version_value.load(std::memory_order_acquire); + snapshot.version = version + 1; + std::size_t count = std::min(static_cast(time_point_size), samples.size()); + captured.reserve(count); + auto first = samples.end() - static_cast(count); + for (auto it = first; it != samples.end(); ++it) + captured.push_back(*it); + } snapshot.start_coord_to_end_coord = start_coord_to_end_coord; snapshot.time_point_size = time_point_size; - if (write == 0) { + if (captured.empty()) { snapshot.lower = 0; return snapshot; } - std::uint64_t available = std::min(write, Max_Buffered_Time_Count); - std::uint64_t count = std::min(static_cast(time_point_size), available); - std::uint64_t first_sequence = write - count; - std::pmr::vector samples; - samples.reserve(static_cast(count)); - for (std::uint64_t sequence = first_sequence; sequence < write; ++sequence) { - auto sample = time_slots[sequence % Max_Buffered_Time_Count].load(std::memory_order_acquire); - if (sample && sample->sequence == sequence) - samples.push_back(*sample); - } - if (samples.empty()) { - snapshot.lower = 0; - return snapshot; - } - snapshot.lower_stream_tick = samples.front().tick; - snapshot.upper_stream_tick = samples.back().tick; + snapshot.lower_stream_tick = captured.front().tick; + snapshot.upper_stream_tick = captured.back().tick; snapshot.lower = snapshot.upper_stream_tick - time_point_size + 1; - snapshot.times.reserve(samples.size()); - for (const auto& sample : samples) + snapshot.times.reserve(captured.size()); + for (const auto& sample : captured) snapshot.times.push_back(sample.time); - snapshot.ticks.reserve(samples.size() / static_cast(tick_space) + 1); - for (const auto& sample : samples) { + snapshot.ticks.reserve(captured.size() / static_cast(tick_space) + 1); + for (const auto& sample : captured) { int coord{}; if ((tick_space == 1 || sample.tick % tick_space == 0) && snapshot.coord_by_stream_tick(sample.tick, coord)) snapshot.ticks.push_back({sample.time, coord}); diff --git a/Core/architecture/Timeline_Stream.h b/Core/architecture/Timeline_Stream.h index ee83068..9e2f84d 100644 --- a/Core/architecture/Timeline_Stream.h +++ b/Core/architecture/Timeline_Stream.h @@ -1,8 +1,9 @@ #pragma once -#include #include #include +#include #include +#include #include #include "../base/Time_Of_Day.h" #include "global.h" @@ -31,13 +32,13 @@ public: [[nodiscard]] Timeline_Stream_Snapshot capture(int time_point_size, int tick_space, bool start_coord_to_end_coord) const; private: struct Sample { - std::uint64_t sequence{}; int tick{}; Time_Of_Day time; }; static constexpr std::size_t Max_Buffered_Time_Count = 65536; - std::array>, Max_Buffered_Time_Count> time_slots; - std::atomic write_sequence{0}; + mutable std::mutex samples_mutex; + std::deque samples; + std::atomic version_value{0}; std::atomic next_tick_value{0}; }; } diff --git a/Core/architecture/Triple_Buffer.h b/Core/architecture/Triple_Buffer.h index 954a248..999a303 100644 --- a/Core/architecture/Triple_Buffer.h +++ b/Core/architecture/Triple_Buffer.h @@ -60,7 +60,7 @@ public: return decode_order(slot_state.load(std::memory_order_acquire)); } /// 尝试占用指定角色当前对应的物理缓冲。 - Triple_Buffer_Mark_Record try_mark_use_role(std::uint8_t role) { + Triple_Buffer_Mark_Record try_acquire_role(std::uint8_t role) { auto order = slot_state.load(std::memory_order_acquire); auto view = decode_order(order); auto index = view[role]; @@ -74,14 +74,6 @@ public: } return {Triple_Buffer_Result::Success, {index}, triple_buffer_invalid_index}; } - Triple_Buffer_Lease mark_use_role(std::uint8_t role) { - for (;;) { - auto record = try_mark_use_role(role); - if (record.result == Triple_Buffer_Result::Success) { - return record.lease; - } - } - } /// 释放租约持有的物理缓冲。 void unmark_use(Triple_Buffer_Lease lease) { unmark_index(lease.index); @@ -90,7 +82,12 @@ public: template Triple_Buffer_Swap_Record try_swap_role(std::uint8_t left_role, std::uint8_t right_role, Can_Swap can_swap) { std::uint8_t busy_index = triple_buffer_invalid_index; - return try_swap_role_once(left_role, right_role, can_swap, busy_index); + return try_swap_role_once(left_role, right_role, can_swap, [](const Triple_Buffer_View&, const Triple_Buffer_View&) {}, busy_index); + } + template + Triple_Buffer_Swap_Record try_swap_role(std::uint8_t left_role, std::uint8_t right_role, Can_Swap can_swap, On_Success on_success) { + std::uint8_t busy_index = triple_buffer_invalid_index; + return try_swap_role_once(left_role, right_role, can_swap, on_success, busy_index); } bool role_busy(std::uint8_t role) const { auto view = read_view(); @@ -169,8 +166,8 @@ private: unmark_index(left_index); unmark_index(right_index); } - template - Triple_Buffer_Swap_Record try_swap_role_once(std::uint8_t left_role, std::uint8_t right_role, Can_Swap can_swap, std::uint8_t& busy_index) { + template + Triple_Buffer_Swap_Record try_swap_role_once(std::uint8_t left_role, std::uint8_t right_role, Can_Swap can_swap, On_Success on_success, std::uint8_t& busy_index) { auto old_order = slot_state.load(std::memory_order_acquire); auto old_view = decode_order(old_order); auto left_index = old_view[left_role]; @@ -193,6 +190,7 @@ private: unmark_two_indices(left_index, right_index); return {Triple_Buffer_Result::State_Changed, old_view, triple_buffer_invalid_index}; } + on_success(old_view, decode_order(new_order)); unmark_two_indices(left_index, right_index); slot_state.notify_all(); return {Triple_Buffer_Result::Success, old_view, triple_buffer_invalid_index}; diff --git a/Core/base/global.cpp b/Core/base/global.cpp index c499a4c..c4c2927 100644 --- a/Core/base/global.cpp +++ b/Core/base/global.cpp @@ -1,7 +1,9 @@ #include "global.h" #include +#include "Assert.h" #include "Memory.h" #include "../plot/Plot_Core.h" +#include "../render/Canvas.h" #include "asio.hpp" #include @@ -10,12 +12,12 @@ namespace renderive { struct Render_Scheduler_Private { asio::io_context io_context; std::unique_ptr> work_guard; - std::unique_ptr cpu_pool; + std::unique_ptr render_executor; std::thread scheduler_thread; std::thread::id scheduler_thread_id; std::vector plots; std::atomic_bool running{false}; - std::atomic_bool cpu_joined{true}; + std::size_t configured_worker_count{}; }; Render_Scheduler::Render_Scheduler() : d(std::make_unique()) {} @@ -45,15 +47,18 @@ Render_Scheduler::~Render_Scheduler() { shutdown(); } -void Render_Scheduler::start() { +void Render_Scheduler::start(Render_Runtime_Config config) { memory_resource(); - if (d->running.exchange(true, std::memory_order_acq_rel)) + Canvas::diagnose_default_font_once(); + std::size_t resolved_worker_count = resolve_render_worker_count(config.worker_count); + if (d->running.exchange(true, std::memory_order_acq_rel)) { + ASSERT(d->configured_worker_count == resolved_worker_count, "Render scheduler already started with a different Taskflow worker count"); return; + } d->io_context.restart(); d->work_guard = std::make_unique>(d->io_context.get_executor()); - unsigned cpu_count = std::max(1u, std::thread::hardware_concurrency() > 1 ? std::thread::hardware_concurrency() - 1 : 1u); - d->cpu_pool = std::make_unique(cpu_count); - d->cpu_joined.store(false, std::memory_order_release); + d->configured_worker_count = resolved_worker_count; + d->render_executor = std::make_unique(Render_Runtime_Config{resolved_worker_count}); d->scheduler_thread = std::thread([this]() { d->scheduler_thread_id = std::this_thread::get_id(); for (Plot_Core* plot : d->plots) @@ -65,29 +70,32 @@ void Render_Scheduler::start() { void Render_Scheduler::shutdown() { if (!d->running.load(std::memory_order_acquire)) { - join_cpu_pool(); if (d->scheduler_thread.joinable() && d->scheduler_thread.get_id() != std::this_thread::get_id()) d->scheduler_thread.join(); - d->cpu_pool.reset(); + if (d->render_executor) + d->render_executor->shutdown(); + d->render_executor.reset(); d->work_guard.reset(); return; } if (is_scheduler_thread()) { shutdown_on_scheduler(); d->work_guard.reset(); - join_cpu_pool(); + if (d->render_executor) + d->render_executor->shutdown(); return; } post_and_wait([this]() { shutdown_on_scheduler(); }); - join_cpu_pool(); post([this]() { d->work_guard.reset(); }); if (d->scheduler_thread.joinable()) d->scheduler_thread.join(); - d->cpu_pool.reset(); + if (d->render_executor) + d->render_executor->shutdown(); + d->render_executor.reset(); } void Render_Scheduler::post(Task task) { @@ -105,11 +113,20 @@ void Render_Scheduler::post_at(std::uint64_t steady_time_ns, Task task) { }); } -void Render_Scheduler::post_cpu(Task task) { - if (d->cpu_pool) - asio::post(*d->cpu_pool, std::move(task)); - else - post(std::move(task)); +bool Render_Scheduler::try_submit_render_task(Render_Executor_Task task) { + if (!d->render_executor) + return false; + return d->render_executor->try_submit(std::move(task)); +} + +Render_Executor_Snapshot Render_Scheduler::render_executor_snapshot() const { + if (!d->render_executor) + return {}; + return d->render_executor->snapshot(); +} + +std::size_t Render_Scheduler::render_worker_count() const { + return d->configured_worker_count; } void Render_Scheduler::register_plot(Plot_Core* plot) { @@ -144,11 +161,6 @@ asio::io_context& Render_Scheduler::io_context() { return d->io_context; } -void Render_Scheduler::join_cpu_pool() { - if (d->cpu_pool && !d->cpu_joined.exchange(true, std::memory_order_acq_rel)) - d->cpu_pool->join(); -} - void Render_Scheduler::post_and_wait(Task task) { if (is_scheduler_thread() || !d->running.load(std::memory_order_acquire)) { task(); @@ -199,8 +211,8 @@ Render_Scheduler& Global::render_scheduler() { return render_scheduler_value; } -void Global::start_render_scheduler() { - render_scheduler_value.start(); +void Global::start_render_scheduler(Render_Runtime_Config config) { + render_scheduler_value.start(config); } } // namespace renderive diff --git a/Core/base/global.h b/Core/base/global.h index b82bf3b..e6440f0 100644 --- a/Core/base/global.h +++ b/Core/base/global.h @@ -1,5 +1,6 @@ #pragma once #include "../architecture/global.h" +#include "../execution/Render_Executor.h" #include "Task.h" #include namespace asio { @@ -12,18 +13,19 @@ class LIB_DECL Render_Scheduler { public: Render_Scheduler(); ~Render_Scheduler(); - void start(); + void start(Render_Runtime_Config config = {}); void shutdown(); void post(Task task); void post_at(std::uint64_t steady_time_ns, Task task); - void post_cpu(Task task); + bool try_submit_render_task(Render_Executor_Task task); + [[nodiscard]] Render_Executor_Snapshot render_executor_snapshot() const; + [[nodiscard]] std::size_t render_worker_count() const; void register_plot(Plot_Core* plot); void remove_plot(Plot_Core* plot); [[nodiscard]] bool is_scheduler_thread() const; [[nodiscard]] bool is_running() const; asio::io_context& io_context(); private: - void join_cpu_pool(); void post_and_wait(Task task); void register_plot_on_scheduler(Plot_Core* plot); void remove_plot_on_scheduler(Plot_Core* plot); @@ -39,7 +41,7 @@ public: Global(const Global&) = delete; Global& operator=(const Global&) = delete; Render_Scheduler& render_scheduler(); - void start_render_scheduler(); + void start_render_scheduler(Render_Runtime_Config config = {}); private: Render_Scheduler render_scheduler_value; }; diff --git a/Core/execution/Render_Executor.cpp b/Core/execution/Render_Executor.cpp new file mode 100644 index 0000000..99a87a0 --- /dev/null +++ b/Core/execution/Render_Executor.cpp @@ -0,0 +1,284 @@ +#include "Render_Executor.h" +#include "../architecture/Render_Time.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive { +namespace { + +struct Render_Executor_Metrics { + std::size_t worker_count{}; + std::atomic active_workers{0}; + std::atomic active_frame_jobs{0}; + std::atomic queued_frame_jobs{0}; + std::atomic submitted_tasks{0}; + std::atomic started_tasks{0}; + std::atomic completed_tasks{0}; + std::atomic rejected_frame_jobs{0}; + std::atomic dropped_frame_attempts{0}; + std::atomic peak_concurrency{0}; + std::atomic queue_wait_total_ns{0}; + std::atomic queue_wait_max_ns{0}; + std::atomic task_duration_total_ns{0}; + std::atomic task_duration_max_ns{0}; + std::atomic curve_tasks{0}; + std::atomic waterfall_tasks{0}; + std::atomic primitive_tasks{0}; + std::atomic compose_tasks{0}; + std::atomic performance_tasks{0}; +}; + +void update_peak(std::atomic& target, std::uint64_t value) { + std::uint64_t current = target.load(std::memory_order_acquire); + while (current < value && !target.compare_exchange_weak(current, value, std::memory_order_acq_rel, std::memory_order_acquire)) {} +} + +void add_kind_count(Render_Executor_Metrics& metrics, Render_Task_Kind kind) { + switch (kind) { + case Render_Task_Kind::Frame: + case Render_Task_Kind::Primitive: + metrics.primitive_tasks.fetch_add(1, std::memory_order_relaxed); + break; + case Render_Task_Kind::Curve: + metrics.curve_tasks.fetch_add(1, std::memory_order_relaxed); + break; + case Render_Task_Kind::Waterfall: + metrics.waterfall_tasks.fetch_add(1, std::memory_order_relaxed); + break; + case Render_Task_Kind::Compose: + metrics.compose_tasks.fetch_add(1, std::memory_order_relaxed); + break; + case Render_Task_Kind::Performance: + metrics.performance_tasks.fetch_add(1, std::memory_order_relaxed); + break; + } +} + +} // namespace + +class Render_Executor_Observer final : public tf::ObserverInterface { +public: + explicit Render_Executor_Observer(std::shared_ptr metrics) + : metrics(std::move(metrics)) {} + + void set_up(std::size_t worker_count) override { + if (metrics) + metrics->worker_count = worker_count; + worker_entry_ns.clear(); + worker_entry_ns.resize(worker_count); + } + + void on_entry(tf::WorkerView worker, tf::TaskView) override { + if (!metrics) + return; + std::size_t worker_id = worker.id(); + if (worker_id < worker_entry_ns.size()) + worker_entry_ns[worker_id] = steady_now_ns(); + std::size_t active = metrics->active_workers.fetch_add(1, std::memory_order_acq_rel) + 1; + metrics->started_tasks.fetch_add(1, std::memory_order_relaxed); + update_peak(metrics->peak_concurrency, static_cast(active)); + } + + void on_exit(tf::WorkerView worker, tf::TaskView) override { + if (!metrics) + return; + std::size_t worker_id = worker.id(); + if (worker_id < worker_entry_ns.size()) { + std::uint64_t begin_ns = worker_entry_ns[worker_id]; + std::uint64_t end_ns = steady_now_ns(); + std::uint64_t duration_ns = end_ns > begin_ns ? end_ns - begin_ns : 0; + metrics->task_duration_total_ns.fetch_add(duration_ns, std::memory_order_relaxed); + update_peak(metrics->task_duration_max_ns, duration_ns); + } + metrics->completed_tasks.fetch_add(1, std::memory_order_relaxed); + metrics->active_workers.fetch_sub(1, std::memory_order_acq_rel); + } + +private: + std::shared_ptr metrics; + std::vector worker_entry_ns; +}; + +class Render_Executor_Private { +public: + explicit Render_Executor_Private(std::size_t worker_count) + : metrics(std::make_shared()), + executor(worker_count) { + metrics->worker_count = worker_count; + observer = executor.make_observer(metrics); + } + + ~Render_Executor_Private() { + shutdown(); + } + + bool try_submit(Render_Executor_Task task) { + if (shutting_down.load(std::memory_order_acquire) || !task.work) { + 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); + } + 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(); + } + 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; + } + + void shutdown() { + if (shutting_down.exchange(true, std::memory_order_acq_rel)) + return; + executor.wait_for_all(); + } + + Render_Executor_Snapshot snapshot() const { + std::uint64_t submitted = metrics->submitted_tasks.load(std::memory_order_acquire); + std::uint64_t completed = metrics->completed_tasks.load(std::memory_order_acquire); + std::uint64_t started = metrics->started_tasks.load(std::memory_order_acquire); + std::uint64_t queue_total = metrics->queue_wait_total_ns.load(std::memory_order_acquire); + std::uint64_t duration_total = metrics->task_duration_total_ns.load(std::memory_order_acquire); + std::size_t active = metrics->active_workers.load(std::memory_order_acquire); + std::size_t workers = metrics->worker_count; + return { + workers, + active, + workers > active ? workers - active : 0, + metrics->active_frame_jobs.load(std::memory_order_acquire), + metrics->queued_frame_jobs.load(std::memory_order_acquire), + submitted, + started, + completed, + metrics->rejected_frame_jobs.load(std::memory_order_acquire), + metrics->dropped_frame_attempts.load(std::memory_order_acquire), + metrics->peak_concurrency.load(std::memory_order_acquire), + submitted ? queue_total / submitted : 0, + metrics->queue_wait_max_ns.load(std::memory_order_acquire), + completed ? duration_total / completed : 0, + metrics->task_duration_max_ns.load(std::memory_order_acquire), + metrics->curve_tasks.load(std::memory_order_acquire), + metrics->waterfall_tasks.load(std::memory_order_acquire), + metrics->primitive_tasks.load(std::memory_order_acquire), + metrics->compose_tasks.load(std::memory_order_acquire), + metrics->performance_tasks.load(std::memory_order_acquire) + }; + } + + std::size_t worker_count() const { + return metrics->worker_count; + } + +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 release(const Render_Executor_Task& task) { + if (!task.frame_job || !task.plot_id) + return; + std::lock_guard lock(admission_mutex); + admitted_frame_plots.erase(task.plot_id); + } + + void reject(const Render_Executor_Task& task) { + if (task.frame_job) + metrics->rejected_frame_jobs.fetch_add(1, std::memory_order_relaxed); + else + metrics->dropped_frame_attempts.fetch_add(1, std::memory_order_relaxed); + if (task.frame_stat) + task.frame_stat->rejected_task_count++; + } + + std::shared_ptr metrics; + tf::Executor executor; + std::shared_ptr observer; + std::mutex admission_mutex; + std::unordered_set admitted_frame_plots; + std::atomic_bool shutting_down{false}; +}; + +std::size_t resolve_render_worker_count(std::size_t configured_count) { + if (configured_count) + return configured_count; + auto cpu_count = std::thread::hardware_concurrency(); + return cpu_count > 1 ? cpu_count - 1 : 1; +} + +Render_Executor::Render_Executor(Render_Runtime_Config config) + : d(std::make_unique(resolve_render_worker_count(config.worker_count))) {} + +Render_Executor::~Render_Executor() = default; + +bool Render_Executor::try_submit(Render_Executor_Task task) { + return d->try_submit(std::move(task)); +} + +void Render_Executor::shutdown() { + d->shutdown(); +} + +Render_Executor_Snapshot Render_Executor::snapshot() const { + return d->snapshot(); +} + +std::size_t Render_Executor::worker_count() const { + return d->worker_count(); +} + +} // namespace renderive diff --git a/Core/execution/Render_Executor.h b/Core/execution/Render_Executor.h new file mode 100644 index 0000000..17bb9f0 --- /dev/null +++ b/Core/execution/Render_Executor.h @@ -0,0 +1,39 @@ +#pragma once +#include "../architecture/Render_Config.h" +#include "../architecture/global.h" +#include "../base/Task.h" +#include + +namespace renderive { + +struct Render_Executor_Task { + Plot_Execution_Id plot_id{}; + std::uint64_t frame_id{}; + Render_Task_Kind kind = Render_Task_Kind::Primitive; + Task work; + Task completion; + std::shared_ptr frame_stat; + bool frame_job{}; +}; + +class Render_Executor_Private; + +class LIB_DECL Render_Executor { +public: + explicit Render_Executor(Render_Runtime_Config config = {}); + Render_Executor(const Render_Executor&) = delete; + Render_Executor& operator=(const Render_Executor&) = delete; + ~Render_Executor(); + + bool try_submit(Render_Executor_Task task); + void shutdown(); + [[nodiscard]] Render_Executor_Snapshot snapshot() const; + [[nodiscard]] std::size_t worker_count() const; + +private: + std::unique_ptr d; +}; + +std::size_t resolve_render_worker_count(std::size_t configured_count); + +} // namespace renderive diff --git a/Core/plot/Plot_Core.cpp b/Core/plot/Plot_Core.cpp index 65f1e9f..889b1c9 100644 --- a/Core/plot/Plot_Core.cpp +++ b/Core/plot/Plot_Core.cpp @@ -11,7 +11,20 @@ namespace renderive { -class Plot_Core_Private final { +class Plot_Core_Private; + +struct Frame_Render_Job { + std::shared_ptr owner; + std::uint64_t generation{}; + Plot_Frame_Lease frame_lease; + Plot_Frame* buffer{}; + std::shared_ptr lifecycle; + Plot_Render_Snapshot render_snapshot; + Renderable_Tree_Snapshot renderables; + std::shared_ptr worker_stat; +}; + +class Plot_Core_Private final : public std::enable_shared_from_this { public: explicit Plot_Core_Private(std::unique_ptr s) : strategy(std::move(s)) {} @@ -57,9 +70,10 @@ public: Plot_Frame* buffer = frame_lease.frame; cancel_update_states(buffer->presented_update_states); - std::uint64_t render_role_enter_ns = buffer->metadata.render_role_enter_ns; - buffer->metadata = {}; - Frame_Lifecycle_Record& frame = buffer->metadata; + std::uint64_t render_role_enter_ns = buffer->metadata ? buffer->metadata->render_role_enter_ns : 0; + buffer->metadata = std::make_shared(); + auto frame_record = buffer->metadata; + Frame_Lifecycle_Record& frame = *frame_record; std::uint64_t submit_time = steady_now_ns(); frame.frame_id = ++next_frame_id; frame.input_version = pipeline.edit_state_version.load(std::memory_order_acquire); @@ -69,68 +83,121 @@ public: frame.raw_input_count = pending_input_count.exchange(0, std::memory_order_acq_rel); frame.pixel_width = static_cast(size.width); frame.pixel_height = static_cast(size.height); + frame.viewport_version = context->viewport_version.load(std::memory_order_acquire); frame.wait_21_ns = submit_time > frame.render_role_enter_ns ? submit_time - frame.render_role_enter_ns : 0; frame.render_begin_ns = submit_time; + std::uint64_t previous_render_state_version = pipeline.render_state_version; pipeline.job_state = Plot_Render_Job_State::Running; pipeline.render_state_version = frame.input_version; - Plot_Render_Snapshot render_snapshot; - render_snapshot.context = context; - render_snapshot.size = size; - render_snapshot.background_color = background_color(); - render_snapshot.previous_frame_time_ns = previous_frame_time_ns; - render_snapshot.frame_time_ns = submit_time; - render_snapshot.frame_metadata = &frame; - render_snapshot.frame_update_states = &buffer->presented_update_states; + auto job = std::make_shared(); + job->owner = shared_from_this(); + job->generation = generation.load(std::memory_order_acquire); + job->frame_lease = std::move(frame_lease); + job->buffer = buffer; + job->lifecycle = frame_record; + job->worker_stat = std::make_shared(); + job->render_snapshot.context = context; + job->render_snapshot.size = size; + job->render_snapshot.background_color = background_color(); + job->render_snapshot.previous_frame_time_ns = previous_frame_time_ns; + job->render_snapshot.frame_time_ns = submit_time; + job->render_snapshot.frame_metadata = frame_record.get(); + job->render_snapshot.frame_update_states = &buffer->presented_update_states; previous_frame_time_ns = submit_time; - auto renderables = renderable_tree_snapshot(); + job->renderables = renderable_tree_snapshot(); + auto& scheduler = Global::instance()->render_scheduler(); + bool submitted = scheduler.try_submit_render_task(Render_Executor_Task{ + plot_execution_id, + frame.frame_id, + Render_Task_Kind::Frame, + [job]() { + job->owner->execute_frame_job(job); + }, + [job]() { + Global::instance()->render_scheduler().post([job]() { + job->owner->complete_frame_job(job); + }); + }, + job->worker_stat, + true + }); + if (submitted) + return true; + + frame.outcome = Frame_Outcome::Worker_Budget_Dropped; + frame.worker.rejected_task_count++; + job->frame_lease.reset(); + pipeline.render_state_version = previous_render_state_version; + pipeline.job_state = Plot_Render_Job_State::Idle; + collect_frame(frame_record); + trigger_refresh_strategy(Refresh_Trigger::Render_Finished, &frame); + 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()) + return; + Frame_Lifecycle_Record& frame = *job->lifecycle; frame.prepare_begin_ns = steady_now_ns(); - for (const auto& node : renderables) { + for (const auto& node : job->renderables) { const auto& renderable = node.renderable; if (!renderable || !renderable->should_prepare()) continue; - renderable->prepare_data(render_snapshot); + renderable->prepare_data(job->render_snapshot); ++frame.prepared_output_count; } frame.prepare_end_ns = steady_now_ns(); context->first_prepare_data.store(false, std::memory_order_release); - buffer->image.resize(size.width, size.height); - buffer->image.fill(render_snapshot.background_color); - Canvas canvas(buffer->image); + 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); frame.draw_begin_ns = steady_now_ns(); - if (!renderables.empty() && renderables.front().renderable) - renderables.front().renderable->render_tree_snapshot(canvas, render_snapshot, renderables, 0); + if (!job->renderables.empty() && job->renderables.front().renderable) + 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(); - buffer->version.store(frame.frame_id, std::memory_order_release); - for (const auto& update_state : buffer->presented_update_states) { + job->buffer->version.store(frame.frame_id, std::memory_order_release); + for (const auto& update_state : job->buffer->presented_update_states) { if (update_state) update_state->complete_until(Update_Stage::Rendered); } + } + + void complete_frame_job(const std::shared_ptr& job) { + if (!job || !job->buffer || !job->lifecycle) + return; + Frame_Lifecycle_Record& frame = *job->lifecycle; + if (job->worker_stat) + frame.worker = *job->worker_stat; + bool valid_generation = job->generation == generation.load(std::memory_order_acquire); + bool valid_context = !destroying_flag.load(std::memory_order_acquire) && context && !context->destroying.load(std::memory_order_acquire); + job->frame_lease.reset(); + if (!valid_generation || !valid_context || !frame.render_end_ns) { + frame.outcome = Frame_Outcome::Cancelled; + pipeline.job_state = Plot_Render_Job_State::Idle; + supersede_update_states(job->buffer->presented_update_states); + trigger_refresh_strategy(Refresh_Trigger::Render_Finished, &frame); + return; + } - frame_lease.reset(); auto publish_result = pipeline.try_publish_rendered(steady_now_ns()); if (publish_result.superseded_middle) { - Frame_Lifecycle_Record& superseded_frame = publish_result.superseded_middle->metadata; - trigger_refresh_strategy(Refresh_Trigger::Frame_Returned, &superseded_frame); - collect_frame(superseded_frame); - supersede_update_states(publish_result.superseded_middle->presented_update_states); + trigger_refresh_strategy(Refresh_Trigger::Frame_Returned, publish_result.superseded_middle.get()); + collect_frame(publish_result.superseded_middle); } if (publish_result.dropped_frame) { - Frame_Lifecycle_Record& dropped_frame = publish_result.dropped_frame->metadata; - collect_frame(dropped_frame); - supersede_update_states(publish_result.dropped_frame->presented_update_states); + collect_frame(publish_result.dropped_frame); } pipeline.job_state = Plot_Render_Job_State::Idle; if (publish_result.request_present) request_present(publish_result.dirty_rect); trigger_refresh_strategy(Refresh_Trigger::Render_Finished, &frame); - return true; } void post_scheduler_action(Task task) { @@ -185,8 +252,10 @@ public: trigger_refresh_strategy(Refresh_Trigger::Paused); } - void notify_frame_returned(Frame_Lifecycle_Record frame) { - trigger_refresh_strategy(Refresh_Trigger::Frame_Returned, &frame); + void notify_frame_returned(std::shared_ptr frame) { + if (!frame) + return; + trigger_refresh_strategy(Refresh_Trigger::Frame_Returned, frame.get()); collect_frame(frame); } @@ -220,7 +289,8 @@ public: scheduled_refresh_deadline_ns = deadline_ns; std::uint64_t generation = ++refresh_deadline_generation; auto guard = context; - Global::instance()->render_scheduler().post_at(deadline_ns, [guard, self = this, generation, deadline_ns]() { + auto self = shared_from_this(); + Global::instance()->render_scheduler().post_at(deadline_ns, [guard, self, generation, deadline_ns]() { if (!guard || guard->destroying.load(std::memory_order_acquire)) return; if (self->refresh_deadline_generation != generation || self->scheduled_refresh_deadline_ns != deadline_ns) @@ -339,7 +409,9 @@ public: target->request_present(dirty_rect); } - void collect_frame(const Frame_Lifecycle_Record& frame) { + void collect_frame(std::shared_ptr frame) { + if (!frame) + return; auto observer = std::atomic_load_explicit(&context->frame_lifecycle_observer, std::memory_order_acquire); if (observer && observer->enabled()) observer->collect_frame(frame); @@ -365,10 +437,20 @@ public: std::uint64_t scheduled_refresh_deadline_ns{}; std::uint64_t next_frame_id{}; std::uint64_t previous_frame_time_ns{}; + Plot_Execution_Id plot_execution_id{}; + std::atomic generation{1}; }; +namespace { +Plot_Execution_Id next_plot_execution_id() { + static std::atomic next_id{1}; + return next_id.fetch_add(1, std::memory_order_acq_rel); +} +} // namespace + Plot_Core::Plot_Core(std::unique_ptr strategy) - : d(std::make_unique(std::move(strategy))) { + : d(std::make_shared(std::move(strategy))) { + d->plot_execution_id = next_plot_execution_id(); d->context->owner.store(this, std::memory_order_release); Global::instance()->render_scheduler().register_plot(this); } @@ -376,6 +458,7 @@ Plot_Core::Plot_Core(std::unique_ptr strategy) Plot_Core::~Plot_Core() { d->destroying_flag.store(true, std::memory_order_release); d->context->destroying.store(true, std::memory_order_release); + d->generation.fetch_add(1, std::memory_order_acq_rel); Global::instance()->render_scheduler().remove_plot(this); d->pipeline.cancel_all_update_states(); d->context->owner.store(nullptr, std::memory_order_release); @@ -428,8 +511,14 @@ Color Plot_Core::background_color() const { } void Plot_Core::set_viewport_size(Size size) { + Size previous{ + d->context->render_width.load(std::memory_order_acquire), + d->context->render_height.load(std::memory_order_acquire) + }; d->context->render_width.store(size.width, std::memory_order_release); d->context->render_height.store(size.height, std::memory_order_release); + if (previous != size) + d->context->viewport_version.fetch_add(1, std::memory_order_acq_rel); if (!size.empty()) d->context->first_resize.store(false, std::memory_order_release); } @@ -452,7 +541,7 @@ void Plot_Core::mark_render_state_dirty() { d->pipeline.mark_edit_state(); d->latest_input_time_ns.store(now_ns, std::memory_order_release); d->pending_input_count.fetch_add(1, std::memory_order_relaxed); - d->post_scheduler_action([data = d.get(), now_ns]() { + d->post_scheduler_action([data = d, now_ns]() { data->notify_input_changed(now_ns); }); } @@ -460,7 +549,7 @@ void Plot_Core::mark_render_state_dirty() { void Plot_Core::request_manual_refresh() { if (d->destroying_flag.load(std::memory_order_acquire)) return; - d->post_scheduler_action([data = d.get()]() { + d->post_scheduler_action([data = d]() { data->trigger_refresh_strategy(Refresh_Trigger::Manual_Request); }); } @@ -469,14 +558,14 @@ void Plot_Core::start_render() { if (d->destroying_flag.load(std::memory_order_acquire)) return; d->rendering.store(true, std::memory_order_release); - d->post_scheduler_action([data = d.get()]() { + d->post_scheduler_action([data = d]() { data->notify_started(); }); } void Plot_Core::pause_render() { d->rendering.store(false, std::memory_order_release); - d->post_scheduler_action([data = d.get()]() { + d->post_scheduler_action([data = d]() { data->notify_paused(); }); } @@ -494,8 +583,8 @@ Present_Frame Plot_Core::begin_present() { if (!d->root) return result; auto consumed = d->pipeline.try_begin_paint(steady_now_ns()); - if (consumed.has_returned_frame) { - d->post_scheduler_action([data = d.get(), frame = consumed.returned_frame]() mutable { + if (consumed.returned_frame) { + d->post_scheduler_action([data = d, frame = consumed.returned_frame]() mutable { data->notify_frame_returned(frame); }); } @@ -504,7 +593,7 @@ Present_Frame Plot_Core::begin_present() { if (consumed.lease) result.lease_ = std::make_unique(std::move(consumed.lease)); if (!result.new_frame() && result.request_was_pending()) { - d->post_scheduler_action([data = d.get()]() { + d->post_scheduler_action([data = d]() { data->trigger_refresh_strategy(Refresh_Trigger::Paint_Finished); }); } @@ -514,13 +603,15 @@ Present_Frame Plot_Core::begin_present() { void Plot_Core::end_present(Present_Frame&& frame, std::uint64_t paint_begin_ns, std::uint64_t paint_end_ns) { if (frame.lease_ && frame.lease_->frame) { auto* rendered_frame = frame.lease_->frame; - rendered_frame->metadata.paint_begin_ns = paint_begin_ns; - rendered_frame->metadata.paint_end_ns = paint_end_ns; - rendered_frame->metadata.wait_23_ns = paint_begin_ns > rendered_frame->metadata.transition_12_ns ? paint_begin_ns - rendered_frame->metadata.transition_12_ns : 0; + if (rendered_frame->metadata) { + rendered_frame->metadata->paint_begin_ns = paint_begin_ns; + rendered_frame->metadata->paint_end_ns = paint_end_ns; + rendered_frame->metadata->wait_23_ns = paint_begin_ns > rendered_frame->metadata->transition_12_ns ? paint_begin_ns - rendered_frame->metadata->transition_12_ns : 0; + } complete_update_states(rendered_frame->presented_update_states, Update_Stage::Presented); } frame.lease_.reset(); - d->post_scheduler_action([data = d.get()]() mutable { + d->post_scheduler_action([data = d]() mutable { data->trigger_refresh_strategy(Refresh_Trigger::Paint_Finished); }); } diff --git a/Core/plot/Plot_Core.h b/Core/plot/Plot_Core.h index 6256376..b0195e7 100644 --- a/Core/plot/Plot_Core.h +++ b/Core/plot/Plot_Core.h @@ -48,7 +48,7 @@ public: void end_present(Present_Frame&& frame, std::uint64_t paint_begin_ns, std::uint64_t paint_end_ns); private: - std::unique_ptr d; + std::shared_ptr d; }; } // namespace renderive diff --git a/Core/plot/Plot_Frame_Pipeline.cpp b/Core/plot/Plot_Frame_Pipeline.cpp index e63bbe5..78dfab6 100644 --- a/Core/plot/Plot_Frame_Pipeline.cpp +++ b/Core/plot/Plot_Frame_Pipeline.cpp @@ -4,7 +4,9 @@ namespace renderive { -Plot_Frame::Plot_Frame() : presented_update_states(memory_resource(Memory_Domain::Update_Completion)) {} +Plot_Frame::Plot_Frame() + : presented_update_states(memory_resource(Memory_Domain::Update_Completion)), + metadata(std::make_shared()) {} Plot_Frame_Lease::Plot_Frame_Lease(Plot_Frame* buffer, Triple_Role_Buffer_Control* control, Triple_Buffer_Lease lease) : frame(buffer), control(control), lease(lease) {} @@ -70,7 +72,7 @@ const Plot_Frame* Plot_Frame_Pipeline::frame_by_role(std::uint8_t role) const { } Plot_Frame_Lease Plot_Frame_Pipeline::try_begin_render() { - auto record = frame_control.try_mark_use_role(Frame_Rendering); + auto record = frame_control.try_acquire_role(Frame_Rendering); if (record.result != Triple_Buffer_Result::Success) { ++attempts.render_role_busy_count; return {}; @@ -81,32 +83,39 @@ Plot_Frame_Lease Plot_Frame_Pipeline::try_begin_render() { Plot_Frame_Pipeline::Frame_Publish_Result Plot_Frame_Pipeline::try_publish_rendered(std::uint64_t now_ns) { Frame_Publish_Result result; Plot_Frame* rendered = frame_by_role(Frame_Rendering); - Plot_Frame* old_middle = frame_by_role(Frame_Middle); - const Plot_Frame* painting = frame_by_role(Frame_Painting); - bool supersede_middle = buffer_version_newer(old_middle->version.load(std::memory_order_acquire), painting->version.load(std::memory_order_acquire)); auto swap = frame_control.try_swap_role(Frame_Rendering, Frame_Middle, [this](const Triple_Buffer_View& view) { const Plot_Frame* rendering = frame_by_index(view[Frame_Rendering]); const Plot_Frame* middle = frame_by_index(view[Frame_Middle]); return buffer_version_newer(rendering->version.load(std::memory_order_acquire), middle->version.load(std::memory_order_acquire)); + }, [this, &result, now_ns](const Triple_Buffer_View& old_view, const Triple_Buffer_View& new_view) { + Plot_Frame* rendered = frame_by_index(old_view[Frame_Rendering]); + Plot_Frame* old_middle = frame_by_index(old_view[Frame_Middle]); + const Plot_Frame* painting = frame_by_index(old_view[Frame_Painting]); + bool supersede_middle = buffer_version_newer(old_middle->version.load(std::memory_order_acquire), painting->version.load(std::memory_order_acquire)); + + rendered->metadata->transition_12_ns = now_ns; + rendered->metadata->wait_12_ns = now_ns > rendered->metadata->render_end_ns ? now_ns - rendered->metadata->render_end_ns : 0; + + if (supersede_middle && old_middle->metadata && old_middle->metadata->frame_id) { + old_middle->metadata->superseded_time_ns = now_ns; + old_middle->metadata->outcome = Frame_Outcome::Superseded; + result.superseded_middle = old_middle->metadata; + supersede_update_states(old_middle->presented_update_states); + } + + Plot_Frame* next_rendering = frame_by_index(new_view[Frame_Rendering]); + next_rendering->metadata = std::make_shared(); + next_rendering->metadata->render_role_enter_ns = now_ns; }); if (swap.result != Triple_Buffer_Result::Success) { - rendered->metadata.outcome = Frame_Outcome::Publish_Dropped; - result.dropped_frame = rendered; + rendered->metadata->outcome = Frame_Outcome::Publish_Dropped; + result.dropped_frame = rendered->metadata; + supersede_update_states(rendered->presented_update_states); ++attempts.publish_dropped_count; return result; } - rendered->metadata.transition_12_ns = now_ns; - rendered->metadata.wait_12_ns = now_ns > rendered->metadata.render_end_ns ? now_ns - rendered->metadata.render_end_ns : 0; - frame_by_role(Frame_Rendering)->metadata.render_role_enter_ns = now_ns; - - if (supersede_middle && old_middle->metadata.frame_id) { - old_middle->metadata.superseded_time_ns = now_ns; - old_middle->metadata.outcome = Frame_Outcome::Superseded; - result.superseded_middle = old_middle; - } - Frame_Publish_Result update_result = request_paint_update(now_ns); update_result.superseded_middle = result.superseded_middle; update_result.dropped_frame = result.dropped_frame; @@ -119,14 +128,14 @@ Plot_Frame_Pipeline::Frame_Publish_Result Plot_Frame_Pipeline::request_paint_upd return result; Plot_Frame* middle = frame_by_role(Frame_Middle); if (paint_request_pending.load(std::memory_order_acquire)) { - middle->metadata.update_request_id = pending_update_request_id.load(std::memory_order_acquire); - middle->metadata.update_request_time_ns = pending_update_request_time.load(std::memory_order_acquire); + middle->metadata->update_request_id = pending_update_request_id.load(std::memory_order_acquire); + middle->metadata->update_request_time_ns = pending_update_request_time.load(std::memory_order_acquire); return result; } result.update_request_id = ++next_update_request_id; result.update_request_time = now_ns; - middle->metadata.update_request_id = result.update_request_id; - middle->metadata.update_request_time_ns = result.update_request_time; + middle->metadata->update_request_id = result.update_request_id; + middle->metadata->update_request_time_ns = result.update_request_time; pending_update_request_id.store(result.update_request_id, std::memory_order_release); pending_update_request_time.store(result.update_request_time, std::memory_order_release); paint_request_pending.store(true, std::memory_order_release); @@ -138,36 +147,36 @@ Plot_Frame_Pipeline::Frame_Publish_Result Plot_Frame_Pipeline::request_paint_upd Plot_Frame_Pipeline::Frame_Consume_Result Plot_Frame_Pipeline::try_begin_paint(std::uint64_t now_ns) { Frame_Consume_Result result; - result.update_was_pending = paint_request_pending.exchange(false, std::memory_order_acq_rel); + bool pending = paint_request_pending.load(std::memory_order_acquire); if (middle_has_new_frame()) { - Plot_Frame* old_painting = frame_by_role(Frame_Painting); - Plot_Frame* new_painting = frame_by_role(Frame_Middle); auto swap = frame_control.try_swap_role(Frame_Middle, Frame_Painting, [this](const Triple_Buffer_View& view) { const Plot_Frame* middle = frame_by_index(view[Frame_Middle]); const Plot_Frame* painting = frame_by_index(view[Frame_Painting]); return buffer_version_newer(middle->version.load(std::memory_order_acquire), painting->version.load(std::memory_order_acquire)); - }); - if (swap.result == Triple_Buffer_Result::Success) { + }, [this, &result, now_ns](const Triple_Buffer_View& old_view, const Triple_Buffer_View&) { + Plot_Frame* old_painting = frame_by_index(old_view[Frame_Painting]); + Plot_Frame* new_painting = frame_by_index(old_view[Frame_Middle]); result.new_frame = true; - if (old_painting->metadata.frame_id && old_painting->metadata.paint_end_ns) { - old_painting->metadata.transition_32_ns = now_ns; - old_painting->metadata.wait_32_ns = now_ns > old_painting->metadata.paint_end_ns ? now_ns - old_painting->metadata.paint_end_ns : 0; + if (old_painting->metadata && old_painting->metadata->frame_id && old_painting->metadata->paint_end_ns) { + old_painting->metadata->transition_32_ns = now_ns; + old_painting->metadata->wait_32_ns = now_ns > old_painting->metadata->paint_end_ns ? now_ns - old_painting->metadata->paint_end_ns : 0; result.returned_frame = old_painting->metadata; - result.has_returned_frame = true; } - new_painting->metadata.transition_23_ns = now_ns; - new_painting->metadata.wait_23_ns = now_ns > new_painting->metadata.transition_12_ns ? now_ns - new_painting->metadata.transition_12_ns : 0; - } - else { + new_painting->metadata->transition_23_ns = now_ns; + new_painting->metadata->wait_23_ns = now_ns > new_painting->metadata->transition_12_ns ? now_ns - new_painting->metadata->transition_12_ns : 0; + }); + if (swap.result != Triple_Buffer_Result::Success) { result.paint_acquire_dropped = true; ++attempts.paint_role_busy_count; } } - auto record = frame_control.try_mark_use_role(Frame_Painting); - if (record.result == Triple_Buffer_Result::Success) + auto record = frame_control.try_acquire_role(Frame_Painting); + if (record.result == Triple_Buffer_Result::Success) { result.lease = {frame_by_index(record.lease.index), &frame_control, record.lease}; + result.update_was_pending = pending && paint_request_pending.exchange(false, std::memory_order_acq_rel); + } else { result.paint_acquire_dropped = true; ++attempts.paint_role_busy_count; diff --git a/Core/plot/Plot_Frame_Pipeline.h b/Core/plot/Plot_Frame_Pipeline.h index b130aec..24706ed 100644 --- a/Core/plot/Plot_Frame_Pipeline.h +++ b/Core/plot/Plot_Frame_Pipeline.h @@ -25,7 +25,7 @@ struct Plot_Frame { std::atomic version{0}; Image image; std::pmr::vector> presented_update_states; - Frame_Lifecycle_Record metadata; + std::shared_ptr metadata; }; struct Plot_Frame_Lease : Move_Only { @@ -47,8 +47,8 @@ class Plot_Frame_Pipeline { public: struct Frame_Publish_Result { Rect dirty_rect; - Plot_Frame* superseded_middle{}; - Plot_Frame* dropped_frame{}; + std::shared_ptr superseded_middle; + std::shared_ptr dropped_frame; std::uint64_t update_request_id{}; std::uint64_t update_request_time{}; bool request_present{}; @@ -56,8 +56,7 @@ public: struct Frame_Consume_Result { Plot_Frame_Lease lease; - Frame_Lifecycle_Record returned_frame; - bool has_returned_frame{}; + std::shared_ptr returned_frame; bool new_frame{}; bool update_was_pending{}; bool paint_acquire_dropped{}; diff --git a/Core/plottable/Hover_Info.h b/Core/plottable/Hover_Info.h index 319dc03..a94fe44 100644 --- a/Core/plottable/Hover_Info.h +++ b/Core/plottable/Hover_Info.h @@ -148,7 +148,10 @@ private: } Hover_Edit begin_hover_edit_impl() override { auto t = that(); - auto lease = t->d_ptr->state_control.mark_use_role(State_Edit); + auto record = t->d_ptr->state_control.try_acquire_role(State_Edit); + if (record.result != Triple_Buffer_Result::Success) + return {}; + auto lease = record.lease; auto* data = t->d_ptr; auto* state = dynamic_cast(data->state_by_index(lease.index)); return Hover_Edit(this, state, [data, lease]() mutable { diff --git a/Core/plottable/Performance_Shower.cpp b/Core/plottable/Performance_Shower.cpp index b075189..56d627f 100644 --- a/Core/plottable/Performance_Shower.cpp +++ b/Core/plottable/Performance_Shower.cpp @@ -3,10 +3,45 @@ #include #include #include -#include "../base/global.h" +#include +#include "../base/Task.h" #include "../plot/Plot_Core.h" namespace renderive { namespace { +class Performance_Background_Service { +public: + ~Performance_Background_Service() { + shutdown(); + } + + bool try_submit(Task task) { + if (!task || shutting_down.load(std::memory_order_acquire)) + return false; + auto topology = std::make_shared(); + auto task_ptr = std::make_shared(std::move(task)); + topology->emplace([task_ptr]() { + (*task_ptr)(); + }); + executor.run(*topology, [topology]() {}); + return true; + } + + void shutdown() { + if (shutting_down.exchange(true, std::memory_order_acq_rel)) + return; + executor.wait_for_all(); + } + +private: + tf::Executor executor{1}; + std::atomic_bool shutting_down{false}; +}; + +Performance_Background_Service& performance_background_service() { + static Performance_Background_Service service; + return service; +} + double duration_ms(std::uint64_t begin_ns, std::uint64_t end_ns) { return end_ns > begin_ns ? static_cast(end_ns - begin_ns) / 1000000.0 : 0.0; } @@ -104,6 +139,10 @@ std::string renderable_stats_text(const Frame_Lifecycle_Record& frame) { } class Performance_Frame_Log { public: + bool is_enabled() const { + return enabled(); + } + void write(const Frame_Lifecycle_Record& frame) { if (!enabled()) return; @@ -187,8 +226,37 @@ static std::shared_ptr performance_shower_owner(const std::s auto observer = std::atomic_load_explicit(&ctx->frame_lifecycle_observer, std::memory_order_acquire); return std::dynamic_pointer_cast(observer); } + +static std::function performance_update_callback(const std::shared_ptr& ctx) { + if (!ctx) + return {}; + std::lock_guard lock(ctx->performance_update_callback_mutex); + return ctx->performance_update_callback; } -RENDERIVE_DEFINE_RENDERABLE_BINDING(Performance_Shower, Performance_Shower_Private, Performance_Shower_Render_State, Performance_Shower_Input_Data, "Performance_Shower") +} + +void schedule_performance_metrics_worker(const std::shared_ptr& state) { + if (!state || state->cancelled.load(std::memory_order_acquire)) + return; + state->work_requested.store(true, std::memory_order_release); + if (state->worker_active.exchange(true, std::memory_order_acq_rel)) + return; + if (!performance_background_service().try_submit(Task([state]() { + Performance_Shower_Private::run_metrics_worker(state); + }))) { + state->worker_active.store(false, std::memory_order_release); + } +} + +Performance_Shower::Performance_Shower() + : private_data(std::make_unique()) {} + +Performance_Shower::~Performance_Shower() = default; + +Performance_Shower_Private* Performance_Shower::d() { + return private_data.get(); +} + std::shared_ptr attach_performance_shower(Plot_Core& plot) { auto ctx = plot.render_context(); if (!ctx) @@ -197,7 +265,7 @@ std::shared_ptr attach_performance_shower(Plot_Core& plot) { if (shower) return shower; auto created_shower = renderive::make_shared(); - created_shower->init(plot.root_renderable()); + created_shower->set_update_callback(performance_update_callback(ctx)); std::atomic_store_explicit(&ctx->frame_lifecycle_observer, std::static_pointer_cast(created_shower), std::memory_order_release); return created_shower; } @@ -207,10 +275,8 @@ void detach_performance_shower(Plot_Core& plot) { return; auto shower = performance_shower_owner(ctx); std::atomic_store_explicit(&ctx->frame_lifecycle_observer, std::shared_ptr{}, std::memory_order_release); - if (shower) { + if (shower) shower->setEnabled(false); - plot.remove_renderable(shower); - } } bool performance_shower_enabled(const Plot_Core& plot) { auto shower = performance_shower_owner(plot.render_context()); @@ -222,28 +288,139 @@ void set_performance_shower_enabled(Plot_Core& plot, bool enabled) { return; } auto shower = attach_performance_shower(plot); - if (shower) + if (shower) { shower->setEnabled(true); + shower->set_viewport(plot.viewport_size(), plot.render_context() ? plot.render_context()->viewport_version.load(std::memory_order_acquire) : 0); + } +} +std::shared_ptr performance_shower_snapshot(const Plot_Core& plot) { + auto shower = performance_shower_owner(plot.render_context()); + if (!shower || !shower->enabled()) + return {}; + return shower->display_snapshot(); +} +void set_performance_shower_update_callback(Plot_Core& plot, std::function callback) { + auto ctx = plot.render_context(); + if (!ctx) + return; + { + std::lock_guard lock(ctx->performance_update_callback_mutex); + ctx->performance_update_callback = std::move(callback); + } + auto shower = performance_shower_owner(ctx); + if (shower) + shower->set_update_callback(performance_update_callback(ctx)); +} +void notify_performance_shower_viewport_changed(Plot_Core& plot) { + auto shower = performance_shower_owner(plot.render_context()); + if (!shower || !shower->enabled()) + return; + auto ctx = plot.render_context(); + shower->set_viewport(plot.viewport_size(), ctx ? ctx->viewport_version.load(std::memory_order_acquire) : 0); +} +bool performance_shower_handle_wheel(Plot_Core& plot, PointF position, double angle_delta_y, double pixel_delta_y) { + auto shower = performance_shower_owner(plot.render_context()); + return shower && shower->enabled() && shower->handle_wheel(position, angle_delta_y, pixel_delta_y); +} +bool performance_shower_handle_pointer_press(Plot_Core& plot, PointF position) { + auto shower = performance_shower_owner(plot.render_context()); + return shower && shower->enabled() && shower->handle_pointer_press(position); +} +bool performance_shower_handle_pointer_move(Plot_Core& plot, PointF position) { + auto shower = performance_shower_owner(plot.render_context()); + return shower && shower->enabled() && shower->handle_pointer_move(position); +} +bool performance_shower_handle_pointer_release(Plot_Core& plot, PointF position) { + auto shower = performance_shower_owner(plot.render_context()); + return shower && shower->enabled() && shower->handle_pointer_release(position); } void Performance_Shower::setEnabled(bool enabled) { d()->enabled_value.store(enabled, std::memory_order_release); if (enabled) { d()->metrics_worker->reset_requested.store(true, std::memory_order_release); - d()->schedule_metrics_worker(); + d()->metrics_worker->snapshot_rebuild_requested.store(true, std::memory_order_release); + schedule_performance_metrics_worker(d()->metrics_worker); } } bool Performance_Shower::enabled() const { return const_cast(this)->d()->enabled_value.load(std::memory_order_acquire); } -void Performance_Shower::collect_frame(const Frame_Lifecycle_Record& frame) { +std::shared_ptr Performance_Shower::display_snapshot() const { + return const_cast(this)->d()->metrics_worker->display_snapshot_slot.load(std::memory_order_acquire); +} +void Performance_Shower::collect_frame(std::shared_ptr frame) { if (!enabled()) return; d()->enqueue_frame_metric(frame); write_frame_performance_log(frame); } -void write_frame_performance_log(const Frame_Lifecycle_Record& frame) { - Global::instance()->render_scheduler().post_cpu([frame]() { - frame_log().write(frame); - }); +void Performance_Shower::set_viewport(Size viewport, std::uint64_t version) { + d()->metrics_worker->viewport_width.store(viewport.width, std::memory_order_release); + d()->metrics_worker->viewport_height.store(viewport.height, std::memory_order_release); + d()->metrics_worker->viewport_version.store(version, std::memory_order_release); + d()->metrics_worker->snapshot_rebuild_requested.store(true, std::memory_order_release); + schedule_performance_metrics_worker(d()->metrics_worker); +} +bool Performance_Shower::handle_wheel(PointF position, double angle_delta_y, double pixel_delta_y) { + auto snapshot = display_snapshot(); + if (!snapshot || !snapshot->destination_rect.contains(position)) + return false; + double line_height = snapshot->line_height > 0.0 ? snapshot->line_height : d()->metrics_worker->last_line_height; + double delta = 0.0; + if (pixel_delta_y != 0.0) + delta = -pixel_delta_y; + else if (angle_delta_y != 0.0) + delta = -(angle_delta_y / 120.0) * 3.0 * std::max(1.0, line_height); + if (delta != 0.0) { + performance_atomic_add(d()->metrics_worker->pending_wheel_delta_px, delta); + schedule_performance_metrics_worker(d()->metrics_worker); + } + return true; +} +bool Performance_Shower::handle_pointer_press(PointF position) { + auto snapshot = display_snapshot(); + if (!snapshot || snapshot->max_scroll_offset <= 0.0) + return false; + if (snapshot->scroll_thumb_rect.contains(position)) { + d()->metrics_worker->drag_anchor_y.store(position.y - snapshot->scroll_thumb_rect.y, std::memory_order_release); + d()->metrics_worker->drag_position_y.store(position.y, std::memory_order_release); + d()->metrics_worker->drag_end_requested.store(false, std::memory_order_release); + d()->metrics_worker->drag_active.store(true, std::memory_order_release); + schedule_performance_metrics_worker(d()->metrics_worker); + return true; + } + if (snapshot->scroll_track_rect.contains(position)) { + int page = position.y < snapshot->scroll_thumb_rect.y ? -1 : 1; + d()->metrics_worker->pending_page_delta.fetch_add(page, std::memory_order_acq_rel); + schedule_performance_metrics_worker(d()->metrics_worker); + return true; + } + return false; +} +bool Performance_Shower::handle_pointer_move(PointF position) { + if (!d()->metrics_worker->drag_active.load(std::memory_order_acquire)) + return false; + d()->metrics_worker->drag_position_y.store(position.y, std::memory_order_release); + schedule_performance_metrics_worker(d()->metrics_worker); + return true; +} +bool Performance_Shower::handle_pointer_release(PointF position) { + if (!d()->metrics_worker->drag_active.load(std::memory_order_acquire)) + return false; + d()->metrics_worker->drag_position_y.store(position.y, std::memory_order_release); + d()->metrics_worker->drag_end_requested.store(true, std::memory_order_release); + schedule_performance_metrics_worker(d()->metrics_worker); + return true; +} +void Performance_Shower::set_update_callback(std::function callback) { + std::lock_guard lock(d()->metrics_worker->update_callback_mutex); + d()->metrics_worker->update_callback = std::move(callback); +} +void write_frame_performance_log(std::shared_ptr frame) { + if (!frame || !frame_log().is_enabled()) + return; + performance_background_service().try_submit(Task([frame = std::move(frame)]() { + frame_log().write(*frame); + })); } } // namespace renderive diff --git a/Core/plottable/Performance_Shower_p.h b/Core/plottable/Performance_Shower_p.h index ca23b3a..0fcad44 100644 --- a/Core/plottable/Performance_Shower_p.h +++ b/Core/plottable/Performance_Shower_p.h @@ -2,25 +2,32 @@ #include #include #include +#include +#include +#include +#include #include +#include #include +#include #include #include #include +#include #include "../architecture/Frame_Scheduler.h" -#include "../architecture/Renderable.h" -#include "../architecture/Render_Lease.h" +#include "../architecture/Plot_Render_Context.h" +#include "../architecture/Render_Data.h" #include "../base/Color.h" #include "../base/Geometry.h" +#include "../base/Memory.h" #include "../base/Style.h" #include "../base/String_Format.h" #include "../base/Text.h" -#include "../base/global.h" -#include "../event/Wheel_Event.h" #include "../render/Canvas.h" +#include "Performance_Snapshot.h" #include "Psc_Cpp_Core/Statistics/Statistics.h" namespace renderive { -void write_frame_performance_log(const Frame_Lifecycle_Record& frame); +void write_frame_performance_log(std::shared_ptr frame); struct Performance_Shower_Render_State : Render_State { Font font; int top_margin = 4; @@ -232,262 +239,545 @@ private: statistics.update(static_cast(end_ns - begin_ns) / 1000000.0); } }; + +enum class Performance_Field_Id : std::uint16_t { + Frame_Id, + Input_Version, + Render_Period, + Paint_Period, + User_Period, + Target_Period, + Wait_21, + Wait_12, + Wait_23, + Wait_32, + Worker_Count, + Active_Workers, + Queued_Frame_Jobs, + Active_Frame_Jobs, + Submitted_Tasks, + Started_Tasks, + Completed_Tasks, + Rejected_Frame_Jobs, + Queue_Wait_Avg, + Queue_Wait_P95, + Task_Run_Avg, + Task_Run_P95, + Frame_Task_Count, + Frame_Peak_Parallelism, + Frame_Wait_Max, + Frame_Run_Total, + Cache_Hit, + Cache_Miss, + Cache_Ratio, + Cache_Rebuild_Count, + Cache_Rebuild_Time, + Cache_Compose_Time, + Cache_Image_Area, + Cache_Image_Bytes, + Cache_Edit_Version, + Cache_Ready_Version, + Current_Ms, + Smooth_Ms, + Variation_Ms, + P95_Ms, + Current_Percent, + Smooth_Percent, + Variation_Percent, + P95_Percent, + Raw_Input_Count, + Prepared_Output_Count, + Observed_Frame_Count, + Consumed_Frame_Count, + Superseded_Frame_Count, + Cancelled_Frame_Count, + Dropped_Input_Count, + Generic_Count, + Generic_Number, + Count +}; + +inline constexpr std::size_t performance_field_count = static_cast(Performance_Field_Id::Count); + +struct Performance_Field_Layout_State { + double max_value_width{}; +}; + +struct Performance_Text_Run { + bool value{}; + Performance_Field_Id field_id = Performance_Field_Id::Generic_Number; + std::string text; + double measured_width{}; + double reserved_width{}; +}; + +struct Performance_Render_Line { + std::vector runs; + double width{}; +}; + struct Performance_Shower_Private; -class Performance_Shower : public Renderable, public Frame_Lifecycle_Observer { +class Performance_Shower : public Frame_Lifecycle_Observer { public: Performance_Shower(); + ~Performance_Shower(); void setEnabled(bool enabled); bool enabled() const override; - void collect_frame(const Frame_Lifecycle_Record& frame) override; + void collect_frame(std::shared_ptr frame) override; + [[nodiscard]] std::shared_ptr display_snapshot() const; + void set_viewport(Size viewport, std::uint64_t version); + bool handle_wheel(PointF position, double angle_delta_y, double pixel_delta_y); + bool handle_pointer_press(PointF position); + bool handle_pointer_move(PointF position); + bool handle_pointer_release(PointF position); + void set_update_callback(std::function callback); private: Performance_Shower_Private* d(); - Render_Object_Owner create_render_data() override; - Render_Object_Owner create_render_state() override; - Render_Object_Owner create_input_data() override; + std::unique_ptr private_data; }; -struct Performance_Display_Snapshot { - std::vector lines; -}; - -struct Performance_Frame_Metrics_Sample { - std::uint64_t sequence{}; - Frame_Lifecycle_Record frame; -}; +using Performance_Frame_Record = std::shared_ptr; struct Performance_Metrics_Worker_State { static constexpr std::size_t Capacity = 256; - std::array>, Capacity> metric_slots; - std::atomic metric_write_sequence{0}; - std::atomic metric_consumed_sequence{0}; + Performance_Metrics_Worker_State() + : metric_queue(Capacity) {} + rigtorp::MPMCQueue metric_queue; std::atomic metric_capacity_dropped{0}; std::atomic_bool worker_active{false}; + std::atomic_bool work_requested{false}; std::atomic_bool cancelled{false}; std::atomic_bool reset_requested{false}; - std::uint64_t metric_read_sequence{}; + std::atomic_bool snapshot_rebuild_requested{false}; + std::atomic scroll_offset{0.0}; + std::atomic pending_wheel_delta_px{0.0}; + std::atomic pending_page_delta{0}; + std::atomic_bool drag_active{false}; + std::atomic_bool drag_end_requested{false}; + std::atomic drag_position_y{0.0}; + std::atomic drag_anchor_y{0.0}; + std::atomic viewport_width{0}; + std::atomic viewport_height{0}; + std::atomic viewport_version{0}; + std::atomic next_content_version{1}; Frame_Performance_Aggregate aggregate; - std::vector> stable_value_widths; + std::array field_layout_states{}; + RectF last_scroll_track_rect; + RectF last_scroll_thumb_rect; + double last_content_viewport_height{}; + double last_line_height{1.0}; + double last_max_scroll_offset{}; + double held_content_width{}; + std::mutex update_callback_mutex; + std::function update_callback; std::atomic> display_snapshot_slot; }; -struct Performance_Shower_Private : Typed_Render_Data, Hit_Testable, Wheel_Interactive { +inline double performance_clamp_scroll_offset(double offset, double max_offset) { + if (!std::isfinite(offset)) + return 0.0; + if (max_offset <= 0.0) + return 0.0; + return std::clamp(offset, 0.0, max_offset); +} + +inline double performance_atomic_exchange(std::atomic& value, double replacement = 0.0) { + double current = value.load(std::memory_order_acquire); + while (!value.compare_exchange_weak(current, replacement, std::memory_order_acq_rel, std::memory_order_acquire)) {} + return current; +} + +inline void performance_atomic_add(std::atomic& value, double delta) { + double current = value.load(std::memory_order_acquire); + while (!value.compare_exchange_weak(current, current + delta, std::memory_order_acq_rel, std::memory_order_acquire)) {} +} + +void schedule_performance_metrics_worker(const std::shared_ptr& state); + +struct Performance_Shower_Private { Frame_Performance_Aggregate aggregate; std::atomic_bool enabled_value{false}; std::shared_ptr metrics_worker = std::make_shared(); - std::shared_ptr display_snapshot; std::vector lines; - std::vector> stable_value_widths; - Rect panel_rect; - Rect scroll_bar_rect; - Rect scroll_thumb_rect; - std::atomic scroll_line{0}; - std::atomic max_scroll_line{0}; - int content_width{}; - int held_content_width{}; - ~Performance_Shower_Private() override { + std::vector render_lines; + std::array field_layout_states{}; + RectF panel_rect; + RectF scroll_bar_rect; + RectF scroll_thumb_rect; + double content_width{}; + ~Performance_Shower_Private() { metrics_worker->cancelled.store(true, std::memory_order_release); - for (auto& slot : metrics_worker->metric_slots) - slot.store({}, std::memory_order_release); + Performance_Frame_Record frame; + while (metrics_worker->metric_queue.try_pop(frame)) {} } - void enqueue_frame_metric(const Frame_Lifecycle_Record& frame) { - auto sample = std::make_shared(); - std::uint64_t sequence = metrics_worker->metric_write_sequence.fetch_add(1, std::memory_order_acq_rel); - sample->sequence = sequence; - sample->frame = frame; - auto replaced = metrics_worker->metric_slots[sequence % Performance_Metrics_Worker_State::Capacity].exchange(std::move(sample), std::memory_order_acq_rel); - if (replaced && replaced->sequence >= metrics_worker->metric_consumed_sequence.load(std::memory_order_acquire)) + void enqueue_frame_metric(std::shared_ptr frame) { + if (!frame) + return; + if (!metrics_worker->metric_queue.try_push(std::move(frame))) { metrics_worker->metric_capacity_dropped.fetch_add(1, std::memory_order_relaxed); - schedule_metrics_worker(); - } - void schedule_metrics_worker() { - if (metrics_worker->worker_active.exchange(true, std::memory_order_acq_rel)) return; - auto state = metrics_worker; - std::weak_ptr owner = q() ? std::weak_ptr(q()->shared_from_this()) : std::weak_ptr(); - Global::instance()->render_scheduler().post_cpu([state, owner]() { - run_metrics_worker(state, owner); - }); + } + schedule_performance_metrics_worker(metrics_worker); } - static void run_metrics_worker(const std::shared_ptr& state, const std::weak_ptr& owner) { - bool changed = false; - if (state->reset_requested.exchange(false, std::memory_order_acq_rel)) { - state->aggregate.clear(); - state->stable_value_widths.clear(); - changed = true; - } - std::uint64_t dropped = state->metric_capacity_dropped.exchange(0, std::memory_order_acq_rel); - if (dropped) { - state->aggregate.input_capacity_dropped += dropped; - changed = true; - } - std::uint64_t write_sequence = state->metric_write_sequence.load(std::memory_order_acquire); - std::uint64_t first_sequence = write_sequence > Performance_Metrics_Worker_State::Capacity ? write_sequence - Performance_Metrics_Worker_State::Capacity : 0; - if (state->metric_read_sequence < first_sequence) { - state->aggregate.input_capacity_dropped += first_sequence - state->metric_read_sequence; - state->metric_read_sequence = first_sequence; - changed = true; - } - for (std::uint64_t sequence = state->metric_read_sequence; sequence < write_sequence; ++sequence) { - auto sample = state->metric_slots[sequence % Performance_Metrics_Worker_State::Capacity].exchange({}, std::memory_order_acq_rel); - if (!sample || sample->sequence != sequence) - continue; - state->aggregate.update(sample->frame); - state->metric_consumed_sequence.store(sequence + 1, std::memory_order_release); - changed = true; - } - state->metric_read_sequence = write_sequence; - if (changed) - state->display_snapshot_slot.store(build_display_snapshot(state->aggregate, state->stable_value_widths), std::memory_order_release); - state->worker_active.store(false, std::memory_order_release); - if (!state->cancelled.load(std::memory_order_acquire) && state->metric_write_sequence.load(std::memory_order_acquire) > state->metric_read_sequence) { - if (!state->worker_active.exchange(true, std::memory_order_acq_rel)) { - Global::instance()->render_scheduler().post_cpu([state, owner]() { - run_metrics_worker(state, owner); - }); + static void run_metrics_worker(const std::shared_ptr& state) { + if (!state) + return; + while (!state->cancelled.load(std::memory_order_acquire)) { + state->work_requested.store(false, std::memory_order_release); + bool changed = false; + if (state->reset_requested.exchange(false, std::memory_order_acq_rel)) { + state->aggregate.clear(); + state->scroll_offset.store(0.0, std::memory_order_release); + changed = true; } + if (state->snapshot_rebuild_requested.exchange(false, std::memory_order_acq_rel)) + changed = true; + if (consume_scroll_commands(*state)) + changed = true; + std::uint64_t dropped = state->metric_capacity_dropped.exchange(0, std::memory_order_acq_rel); + if (dropped) { + state->aggregate.input_capacity_dropped += dropped; + changed = true; + } + Performance_Frame_Record frame; + while (state->metric_queue.try_pop(frame)) { + if (frame) + state->aggregate.update(*frame); + changed = true; + } + if (changed) { + state->display_snapshot_slot.store(build_display_snapshot(*state), std::memory_order_release); + invoke_update_callback(*state); + } + + if (state->work_requested.load(std::memory_order_acquire)) + continue; + + state->worker_active.store(false, std::memory_order_release); + if (!state->work_requested.load(std::memory_order_acquire) || state->worker_active.exchange(true, std::memory_order_acq_rel)) + return; } - (void)owner; + state->worker_active.store(false, std::memory_order_release); } - static std::shared_ptr build_display_snapshot(const Frame_Performance_Aggregate& aggregate, std::vector>& stable_value_widths) { + static std::shared_ptr build_display_snapshot(Performance_Metrics_Worker_State& worker_state) { Performance_Shower_Private builder; - builder.aggregate = aggregate; - builder.stable_value_widths = stable_value_widths; + builder.aggregate = worker_state.aggregate; + builder.field_layout_states = worker_state.field_layout_states; builder.rebuild_lines(); - stable_value_widths = builder.stable_value_widths; auto snapshot = std::make_shared(); - snapshot->lines = std::move(builder.lines); - return snapshot; - } - void prepare_data(const Plot_Render_Snapshot&) override { - sync_state_pipeline(); - auto published = metrics_worker->display_snapshot_slot.load(std::memory_order_acquire); - if (published && published != display_snapshot) { - display_snapshot = std::move(published); - lines = display_snapshot->lines; - held_content_width = 0; + snapshot->content_version = worker_state.next_content_version.fetch_add(1, std::memory_order_acq_rel); + Size viewport = current_viewport(worker_state); + snapshot->viewport_size = viewport; + snapshot->viewport_version = current_viewport_version(worker_state); + snapshot->viewport_rect = RectF{0.0, 0.0, static_cast(viewport.width), static_cast(viewport.height)}; + if (viewport.empty() || builder.lines.empty()) { + snapshot->lines = builder.stripped_lines(); + return snapshot; } - } - bool select_test(const PointF& pos) override { - return q()->enabled() && panel_rect.contains(Point{static_cast(pos.x), static_cast(pos.y)}); - } - void draw(Canvas& canvas, const Plot_Render_Snapshot& snapshot) override { - if (!q()->enabled() || lines.empty()) - return; - Performance_Shower_Render_State* state = render_state(); + + Performance_Shower_Render_State state; + Image measure_image(1, 1); + measure_image.fill(Color::transparent()); + Text_Metrics metrics; + { + Canvas measure_canvas(measure_image); + measure_canvas.set_font(state.font); + metrics = measure_canvas.measure_text("0123456789 ABC \xE4\xB8\xAD\xE6\x96\x87"); + if (metrics.line_height() <= 0.0) + metrics = measure_canvas.measure_text("M"); + int available_text_width = viewport.width - state.left_margin - state.right_margin - state.scroll_bar_width - state.scroll_bar_margin * 2; + builder.build_render_lines(measure_canvas, static_cast(std::max(1, available_text_width))); + } + worker_state.field_layout_states = builder.field_layout_states; + snapshot->lines = builder.display_lines(); + builder.update_content_width(&state, worker_state.held_content_width, viewport.width); + double line_height = std::max(1.0, metrics.line_height()); + double panel_width = std::min(builder.content_width, static_cast(std::max(0, viewport.width))); + double height = builder.panel_height(line_height, &state, viewport.height); + if (panel_width <= 0.0 || height <= 0.0) + return snapshot; + + double content_height = builder.content_height(line_height, &state); + double content_viewport_height = builder.content_viewport_height(&state, height); + double max_scroll = std::max(0.0, content_height - content_viewport_height); + double scroll = performance_clamp_scroll_offset(worker_state.scroll_offset.load(std::memory_order_acquire), max_scroll); + worker_state.scroll_offset.store(scroll, std::memory_order_release); + builder.panel_rect = RectF{0.0, 0.0, panel_width, height}; + + Image image(static_cast(std::ceil(panel_width)), static_cast(std::ceil(height))); + image.fill(Color::transparent()); + Canvas canvas(image); canvas.save(); - canvas.set_font(state->font); - Text_Metrics metrics = canvas.measure_text("M"); - update_content_width(metrics, state); - int line_height = static_cast(metrics.line_height()); - int panel_width = std::min(content_width, std::max(0, snapshot.size.width)); - int height = panel_height(metrics, state, snapshot.size.height); - int visible_count = visible_line_count(metrics, state, height); - int max_scroll = std::max(0, static_cast(lines.size()) - visible_count); - int scroll = std::clamp(scroll_line.load(std::memory_order_acquire), 0, max_scroll); - max_scroll_line.store(max_scroll, std::memory_order_release); - scroll_line.store(scroll, std::memory_order_release); - panel_rect = Rect{0, 0, panel_width, height}; - canvas.set_brush(Brush{state->background, Brush_Style::Solid}); - canvas.fill_rect(RectF{(double)panel_rect.x, (double)panel_rect.y, (double)panel_rect.width, (double)panel_rect.height}); - canvas.set_pen(Pen{state->foreground}); - Rect text_rect{ - panel_rect.x + state->left_margin, - panel_rect.y + state->top_margin, - panel_rect.width - state->right_margin - state->scroll_bar_width - state->scroll_bar_margin * 2, - panel_rect.height - state->bottom_margin + canvas.set_font(state.font); + canvas.set_brush(Brush{state.background, Brush_Style::Solid}); + canvas.fill_rect(RectF{0.0, 0.0, panel_width, height}); + canvas.set_pen(Pen{state.foreground}); + RectF text_rect{ + static_cast(state.left_margin), + static_cast(state.top_margin), + std::max(0.0, panel_width - state.left_margin - state.right_margin - state.scroll_bar_width - state.scroll_bar_margin * 2), + std::max(0.0, height - state.top_margin - state.bottom_margin) }; - canvas.set_clip_rect(RectF{(double)text_rect.x, (double)text_rect.y, (double)text_rect.width, (double)text_rect.height}); - int last_line = std::min(static_cast(lines.size()), scroll + visible_count); - for (int i = scroll; i < last_line; ++i) - canvas.draw_text(PointF{(double)(state->left_margin), (double)(state->top_margin + (i - scroll + 1) * line_height)}, lines[i]); + canvas.set_clip_rect(text_rect); + int first_line = std::max(0, static_cast(std::floor(scroll / line_height))); + double y = static_cast(state.top_margin) - (scroll - static_cast(first_line) * line_height); + for (int i = first_line; i < static_cast(builder.render_lines.size()) && y < text_rect.bottom(); ++i, y += line_height) { + if (y + line_height < text_rect.y) + continue; + builder.draw_render_line(canvas, builder.render_lines[static_cast(i)], text_rect.x, y); + } canvas.reset_clip(); - update_scroll_bar_rects(state, max_scroll, scroll, visible_count); - if (scroll_bar_rect.width > 0 && scroll_bar_rect.height > 0) { - canvas.set_brush(Brush{state->scroll_track, Brush_Style::Solid}); - canvas.fill_rect(RectF{(double)scroll_bar_rect.x, (double)scroll_bar_rect.y, (double)scroll_bar_rect.width, (double)scroll_bar_rect.height}); - canvas.set_brush(Brush{state->scroll_thumb, Brush_Style::Solid}); - canvas.fill_rect(RectF{(double)scroll_thumb_rect.x, (double)scroll_thumb_rect.y, (double)scroll_thumb_rect.width, (double)scroll_thumb_rect.height}); + builder.update_scroll_bar_rects(&state, max_scroll, scroll, content_viewport_height, content_height); + if (!builder.scroll_bar_rect.empty()) { + canvas.set_brush(Brush{state.scroll_track, Brush_Style::Solid}); + canvas.fill_rect(builder.scroll_bar_rect); + canvas.set_brush(Brush{state.scroll_thumb, Brush_Style::Solid}); + canvas.fill_rect(builder.scroll_thumb_rect); } canvas.restore(); - } - Rect pixel_bounds(const Size& plot_size) override { - if (!q()->enabled() || lines.empty()) - return {}; - Performance_Shower_Render_State* state = render_state(); - Font save_font = state->font; - Text_Metrics metrics; - metrics.width = 0; - metrics.ascent = 12; - metrics.descent = 4; - metrics.line_gap = 0; - update_content_width(metrics, state); - int height = panel_height(metrics, state, plot_size.height); - return Rect{0, 0, std::min(content_width, plot_size.width), height}; - } - void wheel_event(const Wheel_Event& event) override { - if (!q()->enabled() || !panel_rect.contains(Point{static_cast(event.position.x), static_cast(event.position.y)})) - return; - int max_scroll = max_scroll_line.load(std::memory_order_acquire); - if (!max_scroll) - return; - int delta = static_cast(event.angle_delta_y); - if (!delta) - return; - int step = std::max(1, std::abs(delta) / 120) * 3; - int current = scroll_line.load(std::memory_order_acquire); - int next = std::clamp(current + (delta > 0 ? -step : step), 0, max_scroll); - if (next == current) - return; - scroll_line.store(next, std::memory_order_release); - event.accept(); + snapshot->destination_rect = builder.panel_rect; + snapshot->scroll_track_rect = builder.scroll_bar_rect; + snapshot->scroll_thumb_rect = builder.scroll_thumb_rect; + snapshot->content_height = content_height; + snapshot->scroll_offset = scroll; + snapshot->max_scroll_offset = max_scroll; + snapshot->line_height = line_height; + snapshot->image = std::move(image); + worker_state.last_scroll_track_rect = builder.scroll_bar_rect; + worker_state.last_scroll_thumb_rect = builder.scroll_thumb_rect; + worker_state.last_content_viewport_height = content_viewport_height; + worker_state.last_line_height = line_height; + worker_state.last_max_scroll_offset = max_scroll; + return snapshot; } private: + static Size current_viewport(const Performance_Metrics_Worker_State& worker_state) { + int width = worker_state.viewport_width.load(std::memory_order_acquire); + int height = worker_state.viewport_height.load(std::memory_order_acquire); + if (width > 0 && height > 0) + return {width, height}; + if (!worker_state.aggregate.has_frame) + return {}; + const Frame_Lifecycle_Record& frame = worker_state.aggregate.latest; + return { + static_cast(std::min(frame.pixel_width, static_cast(std::numeric_limits::max()))), + static_cast(std::min(frame.pixel_height, static_cast(std::numeric_limits::max()))) + }; + } + static std::uint64_t current_viewport_version(const Performance_Metrics_Worker_State& worker_state) { + std::uint64_t version = worker_state.viewport_version.load(std::memory_order_acquire); + if (version || !worker_state.aggregate.has_frame) + return version; + return worker_state.aggregate.latest.viewport_version; + } + static void invoke_update_callback(Performance_Metrics_Worker_State& worker_state) { + std::function callback; + { + std::lock_guard lock(worker_state.update_callback_mutex); + callback = worker_state.update_callback; + } + if (callback) + callback(); + } + static bool consume_scroll_commands(Performance_Metrics_Worker_State& worker_state) { + double offset = worker_state.scroll_offset.load(std::memory_order_acquire); + double previous = offset; + double line_height = std::max(1.0, worker_state.last_line_height); + double wheel_delta = performance_atomic_exchange(worker_state.pending_wheel_delta_px); + if (wheel_delta != 0.0) + offset += wheel_delta; + int page_delta = worker_state.pending_page_delta.exchange(0, std::memory_order_acq_rel); + if (page_delta) + offset += static_cast(page_delta) * std::max(line_height, worker_state.last_content_viewport_height * 0.8); + bool dragging = worker_state.drag_active.load(std::memory_order_acquire); + bool drag_end = worker_state.drag_end_requested.exchange(false, std::memory_order_acq_rel); + if (dragging && worker_state.last_max_scroll_offset > 0.0) { + const RectF& track = worker_state.last_scroll_track_rect; + const RectF& thumb = worker_state.last_scroll_thumb_rect; + double travel = track.height - thumb.height; + if (track.height > 0.0 && thumb.height > 0.0 && travel > 0.0) { + double thumb_top = worker_state.drag_position_y.load(std::memory_order_acquire) + - worker_state.drag_anchor_y.load(std::memory_order_acquire); + double ratio = (thumb_top - track.y) / travel; + offset = std::clamp(ratio, 0.0, 1.0) * worker_state.last_max_scroll_offset; + } + } + if (drag_end) + worker_state.drag_active.store(false, std::memory_order_release); + offset = performance_clamp_scroll_offset(offset, worker_state.last_max_scroll_offset); + worker_state.scroll_offset.store(offset, std::memory_order_release); + return wheel_delta != 0.0 || page_delta != 0 || drag_end || std::abs(offset - previous) > 0.01; + } static std::string value(double number, int precision = 2) { return format_number(number, {.precision = precision}); } - int panel_height(const Text_Metrics& metrics, Performance_Shower_Render_State* state, int plot_height) const { - int content_height = static_cast(metrics.line_height()) * static_cast(lines.size()) + state->top_margin + state->bottom_margin; - return std::min(content_height, std::max(0, plot_height)); + double panel_height(double line_height, Performance_Shower_Render_State* state, int plot_height) const { + return std::min(content_height(line_height, state), static_cast(std::max(0, plot_height))); } - int visible_line_count(const Text_Metrics& metrics, Performance_Shower_Render_State* state, int height) const { - int text_height = std::max(0, height - state->top_margin - state->bottom_margin); - return std::max(1, text_height / std::max(1, static_cast(metrics.line_height()))); + double content_height(double line_height, Performance_Shower_Render_State* state) const { + return line_height * static_cast(render_lines.size()) + state->top_margin + state->bottom_margin; } - void update_scroll_bar_rects(Performance_Shower_Render_State* state, int max_scroll, int scroll, int visible_count) { - if (!max_scroll || panel_rect.width <= 0 || panel_rect.height <= 0) { + double content_viewport_height(Performance_Shower_Render_State* state, double height) const { + return std::max(0.0, height - state->top_margin - state->bottom_margin); + } + void update_scroll_bar_rects(Performance_Shower_Render_State* state, double max_scroll, double scroll, double visible_height, double total_height) { + if (max_scroll <= 0.0 || panel_rect.width <= 0.0 || panel_rect.height <= 0.0 || total_height <= 0.0) { scroll_bar_rect = {}; scroll_thumb_rect = {}; return; } - int track_height = std::max(0, panel_rect.height - state->scroll_bar_margin * 2); - scroll_bar_rect = Rect{panel_rect.x + panel_rect.width - state->scroll_bar_width - state->scroll_bar_margin + 1, state->scroll_bar_margin, state->scroll_bar_width, track_height}; - int line_count = std::max(visible_count, static_cast(lines.size())); - int thumb_height = std::max(16, scroll_bar_rect.height * visible_count / std::max(1, line_count)); + double track_height = std::max(0.0, panel_rect.height - static_cast(state->scroll_bar_margin * 2)); + scroll_bar_rect = RectF{ + panel_rect.x + panel_rect.width - static_cast(state->scroll_bar_width + state->scroll_bar_margin), + static_cast(state->scroll_bar_margin), + static_cast(state->scroll_bar_width), + track_height + }; + double thumb_height = std::max(16.0, scroll_bar_rect.height * visible_height / std::max(visible_height, total_height)); thumb_height = std::min(thumb_height, scroll_bar_rect.height); - int travel = std::max(0, scroll_bar_rect.height - thumb_height); - int thumb_y = scroll_bar_rect.y + (max_scroll ? travel * scroll / max_scroll : 0); - scroll_thumb_rect = Rect{scroll_bar_rect.x, thumb_y, scroll_bar_rect.width, thumb_height}; + double travel = std::max(0.0, scroll_bar_rect.height - thumb_height); + double thumb_y = scroll_bar_rect.y + (max_scroll > 0.0 ? travel * scroll / max_scroll : 0.0); + scroll_thumb_rect = RectF{scroll_bar_rect.x, thumb_y, scroll_bar_rect.width, thumb_height}; } - int& stable_value_width(const std::string& key) { - for (auto& field : stable_value_widths) { - if (field.first == key) - return field.second; + static bool ends_with(std::string_view text, std::string_view suffix) { + return text.size() >= suffix.size() && text.substr(text.size() - suffix.size()) == suffix; + } + static bool contains(std::string_view text, std::string_view needle) { + return text.find(needle) != std::string_view::npos; + } + static Performance_Field_Id field_id_from_key(std::string_view key) { + if (key == "frame_id") + return Performance_Field_Id::Frame_Id; + if (key == "input_version") + return Performance_Field_Id::Input_Version; + if (key == "render_period") + return Performance_Field_Id::Render_Period; + if (key == "paint_period") + return Performance_Field_Id::Paint_Period; + if (key == "user_period") + return Performance_Field_Id::User_Period; + if (key == "target_period") + return Performance_Field_Id::Target_Period; + if (key == "wait_21") + return Performance_Field_Id::Wait_21; + if (key == "wait_12") + return Performance_Field_Id::Wait_12; + if (key == "wait_23") + return Performance_Field_Id::Wait_23; + if (key == "wait_32") + return Performance_Field_Id::Wait_32; + if (key == "taskflow_workers") + return Performance_Field_Id::Worker_Count; + if (key == "taskflow_active") + return Performance_Field_Id::Active_Workers; + if (key == "taskflow_queued_frames") + return Performance_Field_Id::Queued_Frame_Jobs; + if (key == "taskflow_active_frames") + return Performance_Field_Id::Active_Frame_Jobs; + if (key == "taskflow_submitted") + return Performance_Field_Id::Submitted_Tasks; + if (key == "taskflow_started") + return Performance_Field_Id::Started_Tasks; + if (key == "taskflow_completed") + return Performance_Field_Id::Completed_Tasks; + if (key == "taskflow_rejected") + return Performance_Field_Id::Rejected_Frame_Jobs; + if (key == "taskflow_queue_wait_avg") + return Performance_Field_Id::Queue_Wait_Avg; + if (key == "taskflow_queue_wait_p95") + return Performance_Field_Id::Queue_Wait_P95; + if (key == "taskflow_run_avg") + return Performance_Field_Id::Task_Run_Avg; + if (key == "taskflow_run_p95") + return Performance_Field_Id::Task_Run_P95; + if (key == "frame_worker_tasks") + return Performance_Field_Id::Frame_Task_Count; + if (key == "frame_worker_peak") + return Performance_Field_Id::Frame_Peak_Parallelism; + if (key == "frame_worker_wait_max") + return Performance_Field_Id::Frame_Wait_Max; + if (key == "frame_worker_run_total") + return Performance_Field_Id::Frame_Run_Total; + if (key == "cache_hit_count") + return Performance_Field_Id::Cache_Hit; + if (key == "cache_miss_count") + return Performance_Field_Id::Cache_Miss; + if (key == "cache_hit_ratio") + return Performance_Field_Id::Cache_Ratio; + if (key == "cache_rebuild_count") + return Performance_Field_Id::Cache_Rebuild_Count; + if (key == "cache_rebuild_time") + return Performance_Field_Id::Cache_Rebuild_Time; + if (key == "cache_compose_time") + return Performance_Field_Id::Cache_Compose_Time; + if (key == "raw_input_count") + return Performance_Field_Id::Raw_Input_Count; + if (key == "prepared_output_count") + return Performance_Field_Id::Prepared_Output_Count; + if (key == "observed_frame_count") + return Performance_Field_Id::Observed_Frame_Count; + if (key == "consumed_outcome_count") + return Performance_Field_Id::Consumed_Frame_Count; + if (key == "superseded_outcome_count") + return Performance_Field_Id::Superseded_Frame_Count; + if (key == "cancelled_outcome_count") + return Performance_Field_Id::Cancelled_Frame_Count; + if (key == "input_capacity_dropped") + return Performance_Field_Id::Dropped_Input_Count; + if (ends_with(key, "_cache_hit")) + return Performance_Field_Id::Cache_Hit; + if (ends_with(key, "_cache_miss")) + return Performance_Field_Id::Cache_Miss; + if (ends_with(key, "_cache_rebuild")) + return Performance_Field_Id::Cache_Rebuild_Count; + if (ends_with(key, "_cache_ratio")) + return Performance_Field_Id::Cache_Ratio; + if (ends_with(key, "_cache_image_area")) + return Performance_Field_Id::Cache_Image_Area; + if (ends_with(key, "_cache_image_bytes")) + return Performance_Field_Id::Cache_Image_Bytes; + if (ends_with(key, "_cache_edit_version")) + return Performance_Field_Id::Cache_Edit_Version; + if (ends_with(key, "_cache_ready_version")) + return Performance_Field_Id::Cache_Ready_Version; + if (contains(key, "ratio")) { + if (ends_with(key, "_instant")) + return Performance_Field_Id::Current_Percent; + if (ends_with(key, "_smooth")) + return Performance_Field_Id::Smooth_Percent; + if (ends_with(key, "_variation")) + return Performance_Field_Id::Variation_Percent; + if (ends_with(key, "_p95")) + return Performance_Field_Id::P95_Percent; + return Performance_Field_Id::Current_Percent; } - stable_value_widths.push_back({key, 0}); - return stable_value_widths.back().second; + if (ends_with(key, "_instant")) + return Performance_Field_Id::Current_Ms; + if (ends_with(key, "_smooth") || ends_with(key, "_srtt")) + return Performance_Field_Id::Smooth_Ms; + if (ends_with(key, "_variation") || ends_with(key, "_jitter")) + return Performance_Field_Id::Variation_Ms; + if (ends_with(key, "_p95")) + return Performance_Field_Id::P95_Ms; + if (ends_with(key, "_fps") || ends_with(key, "_count")) + return Performance_Field_Id::Generic_Count; + return Performance_Field_Id::Generic_Number; } - std::string stable_text(const std::string& key, std::string text) { - int& width = stable_value_width(key); - width = std::max(width, static_cast(text.size())); - if (text.size() < static_cast(width)) - text.insert(0, static_cast(width) - text.size(), ' '); - return text; + static std::string marked_value(Performance_Field_Id field_id, std::string text) { + std::string result; + result.reserve(text.size() + 12); + result.push_back('\x1f'); + result += std::to_string(static_cast(field_id)); + result.push_back('\x1e'); + result += text; + result.push_back('\x1d'); + return result; } std::string number_text(const std::string& key, double number, int precision = 2) { - return stable_text(key, value(number, precision)); + return marked_value(field_id_from_key(key), value(number, precision)); } std::string count_text(const std::string& key, std::uint64_t count) { - return stable_text(key, std::to_string(count)); + return marked_value(field_id_from_key(key), std::to_string(count)); } std::string duration_line(const std::string& key, const std::string& name, const Psc::Value_Statistics& statistics) { return name + " current:" + number_text(key + "_instant", statistics.instant) + "ms smooth:" + number_text(key + "_smooth", statistics.smooth) + "ms var:" + number_text(key + "_variation", statistics.variation) + "ms P95:" + number_text(key + "_p95", statistics.p95) + "ms"; @@ -545,7 +835,7 @@ private: std::string cache_ratio_text(const std::string& key, std::uint64_t hit, std::uint64_t miss) { std::uint64_t total = hit + miss; if (!total) - return stable_text(key, "0.00") + "%"; + return number_text(key, 0.0) + "%"; return number_text(key, static_cast(hit) * 100.0 / static_cast(total)) + "%"; } std::string ns_text(const std::string& key, std::uint64_t ns) { @@ -568,7 +858,7 @@ private: } std::string ns_ratio_text(const std::string& key, std::uint64_t numerator, std::uint64_t denominator) { if (!denominator) - return stable_text(key, "0.00") + "%"; + return number_text(key, 0.0) + "%"; return number_text(key, static_cast(numerator) * 100.0 / static_cast(denominator)) + "%"; } bool should_show_cache(const Renderable_Cache_Aggregate_Stat& stat) const { @@ -663,6 +953,8 @@ private: } const Frame_Lifecycle_Record& frame = aggregate.latest; const Refresh_Control_Snapshot& refresh = frame.refresh; + const Frame_Worker_Stat& worker = frame.worker; + const Render_Executor_Snapshot& executor = worker.executor_at_finish; std::uint64_t aggregate_cache_total = aggregate.cache_hit_count + aggregate.cache_miss_count; std::uint64_t latest_render_time = frame_render_time_ns(frame); double cache_hit_ratio = aggregate_cache_total ? static_cast(aggregate.cache_hit_count) * 100.0 / static_cast(aggregate_cache_total) : 0.0; @@ -670,6 +962,10 @@ private: lines.push_back("frame:" + count_text("frame_id", frame.frame_id) + " input_ver:" + count_text("input_version", frame.input_version) + " outcome:" + outcome_text(frame.outcome) + " limit:" + limit_source_text(refresh.source)); lines.push_back("period render:" + period_text("render_period", refresh.render_period_ns) + " paint:" + period_text("paint_period", refresh.paint_period_ns) + " user:" + period_text("user_period", refresh.user_period_ns) + " target:" + period_text("target_period", refresh.target_period_ns)); lines.push_back("wait 21:" + ns_text("wait_21", frame.wait_21_ns) + "ms 12:" + ns_text("wait_12", frame.wait_12_ns) + "ms 23:" + ns_text("wait_23", frame.wait_23_ns) + "ms 32:" + ns_text("wait_32", frame.wait_32_ns) + "ms"); + lines.push_back("taskflow workers:" + count_text("taskflow_workers", static_cast(executor.worker_count)) + " active:" + count_text("taskflow_active", static_cast(executor.active_workers)) + " queued_frames:" + count_text("taskflow_queued_frames", static_cast(executor.queued_frame_jobs)) + " active_frames:" + count_text("taskflow_active_frames", static_cast(executor.active_frame_jobs))); + lines.push_back("taskflow tasks submitted:" + count_text("taskflow_submitted", executor.submitted_tasks) + " started:" + count_text("taskflow_started", executor.started_tasks) + " completed:" + count_text("taskflow_completed", executor.completed_tasks) + " rejected_frames:" + count_text("taskflow_rejected", executor.rejected_frame_jobs)); + lines.push_back("taskflow wait avg:" + ns_text("taskflow_queue_wait_avg", executor.average_queue_wait_ns) + "ms p95:" + ns_text("taskflow_queue_wait_p95", executor.p95_queue_wait_ns) + "ms run avg:" + ns_text("taskflow_run_avg", executor.average_task_duration_ns) + "ms p95:" + ns_text("taskflow_run_p95", executor.p95_task_duration_ns) + "ms"); + lines.push_back("frame worker tasks:" + count_text("frame_worker_tasks", worker.task_count) + " peak_parallel:" + count_text("frame_worker_peak", worker.peak_parallelism) + " wait_max:" + ns_text("frame_worker_wait_max", worker.queue_wait_max_ns) + "ms run_total:" + ns_text("frame_worker_run_total", worker.worker_run_total_ns) + "ms"); lines.push_back(duration_line("request_wait", "lifecycle request_wait", aggregate.request_wait_ms)); lines.push_back(duration_line("render_queue_wait", "lifecycle queue_wait", aggregate.render_queue_wait_ms)); lines.push_back(duration_line("prepare", "lifecycle prepare", aggregate.prepare_ms)); @@ -692,13 +988,217 @@ private: lines.push_back("work raw:" + count_text("raw_input_count", frame.raw_input_count) + " prepared:" + count_text("prepared_output_count", frame.prepared_output_count)); lines.push_back("observed:" + count_text("observed_frame_count", aggregate.observed_frame_count) + " consumed:" + count_text("consumed_outcome_count", aggregate.consumed_outcome_count) + " superseded:" + count_text("superseded_outcome_count", aggregate.superseded_outcome_count) + " cancelled:" + count_text("cancelled_outcome_count", aggregate.cancelled_outcome_count) + " dropped:" + count_text("input_capacity_dropped", aggregate.input_capacity_dropped)); } - void update_content_width(const Text_Metrics& metrics, Performance_Shower_Render_State* state) { - int max_width = 0; - for (const std::string& line : lines) - max_width = std::max(max_width, static_cast(metrics.width * static_cast(line.size()))); - int measured_width = max_width + state->left_margin + state->right_margin + state->scroll_bar_width + state->scroll_bar_margin * 2; - held_content_width = std::max(held_content_width, measured_width); - content_width = held_content_width; + std::vector stripped_lines() const { + std::vector result; + result.reserve(lines.size()); + for (const auto& line : lines) + result.push_back(strip_markers(line)); + return result; + } + std::vector display_lines() const { + std::vector result; + result.reserve(render_lines.size()); + for (const auto& line : render_lines) + result.push_back(render_line_text(line)); + return result; + } + void build_render_lines(Canvas& canvas, double available_width) { + render_lines.clear(); + available_width = std::max(1.0, available_width); + for (const auto& raw_line : lines) { + std::vector runs = parse_runs(raw_line, canvas); + append_wrapped_runs(std::move(runs), canvas, available_width); + } + if (render_lines.empty()) { + Performance_Render_Line line; + Performance_Text_Run run; + run.text = "waiting for frame stats"; + measure_run(canvas, run); + append_run(line, run); + render_lines.push_back(std::move(line)); + } + } + void update_content_width(Performance_Shower_Render_State* state, double& held_width, int viewport_width) { + double max_width = 0.0; + for (const auto& line : render_lines) + max_width = std::max(max_width, line.width); + double measured_width = max_width + + static_cast(state->left_margin + state->right_margin + state->scroll_bar_width + state->scroll_bar_margin * 2); + held_width = std::max(held_width, measured_width); + content_width = std::min(held_width, static_cast(std::max(0, viewport_width))); + if (content_width <= 0.0) + content_width = std::min(measured_width, static_cast(std::max(0, viewport_width))); + } + void draw_render_line(Canvas& canvas, const Performance_Render_Line& line, double x, double y) const { + double cursor = x; + for (const auto& run : line.runs) { + if (run.text.empty()) + continue; + if (run.value) { + double text_x = cursor + std::max(0.0, run.reserved_width - run.measured_width); + canvas.draw_text(PointF{text_x, y}, run.text); + cursor += run.reserved_width; + } + else { + canvas.draw_text(PointF{cursor, y}, run.text); + cursor += run.measured_width; + } + } + } + static double run_width(const Performance_Text_Run& run) { + return run.value ? run.reserved_width : run.measured_width; + } + static void append_run(Performance_Render_Line& line, const Performance_Text_Run& run) { + line.runs.push_back(run); + line.width += run_width(run); + } + void append_wrapped_runs(std::vector runs, Canvas& canvas, double available_width) { + Performance_Render_Line current; + for (auto& original_run : runs) { + Performance_Text_Run run = std::move(original_run); + while (!run.text.empty()) { + double remaining = available_width - current.width; + double width = run_width(run); + if (current.runs.empty() || width <= remaining) { + if (run.value || width <= available_width) { + append_run(current, run); + break; + } + Performance_Text_Run fragment = split_text_run(canvas, run, available_width); + if (fragment.text.empty()) { + append_run(current, run); + break; + } + append_run(current, fragment); + push_non_empty_line(current); + continue; + } + push_non_empty_line(current); + } + } + push_non_empty_line(current); + } + void push_non_empty_line(Performance_Render_Line& line) { + if (line.runs.empty()) + return; + render_lines.push_back(std::move(line)); + line = {}; + } + Performance_Text_Run split_text_run(Canvas& canvas, Performance_Text_Run& run, double available_width) { + Performance_Text_Run fragment; + if (run.value || run.text.empty()) + return fragment; + std::size_t split = split_utf8_text_to_width(canvas, run.text, available_width); + if (!split || split >= run.text.size()) + return fragment; + fragment.text = run.text.substr(0, split); + measure_run(canvas, fragment); + std::size_t rest = split; + while (rest < run.text.size() && run.text[rest] == ' ') + ++rest; + run.text = run.text.substr(rest); + measure_run(canvas, run); + return fragment; + } + static std::size_t next_utf8_boundary(std::string_view text, std::size_t pos) { + if (pos >= text.size()) + return text.size(); + unsigned char c = static_cast(text[pos]); + std::size_t advance = 1; + if ((c & 0x80u) == 0) + advance = 1; + else if ((c & 0xE0u) == 0xC0u) + advance = 2; + else if ((c & 0xF0u) == 0xE0u) + advance = 3; + else if ((c & 0xF8u) == 0xF0u) + advance = 4; + return std::min(text.size(), pos + advance); + } + static std::size_t split_utf8_text_to_width(Canvas& canvas, const std::string& text, double max_width) { + std::size_t best = 0; + std::size_t best_space = 0; + for (std::size_t pos = 0; pos < text.size();) { + std::size_t next = next_utf8_boundary(text, pos); + std::string candidate = text.substr(0, next); + if (canvas.measure_text(candidate).width > max_width) + break; + best = next; + if (text[pos] == ' ') + best_space = next; + pos = next; + } + if (best_space > 0) + return best_space; + if (best > 0) + return best; + return next_utf8_boundary(text, 0); + } + std::vector parse_runs(const std::string& line, Canvas& canvas) { + std::vector runs; + std::size_t pos = 0; + while (pos < line.size()) { + if (line[pos] == '\x1f') { + std::size_t sep = line.find('\x1e', pos + 1); + std::size_t end = sep == std::string::npos ? std::string::npos : line.find('\x1d', sep + 1); + if (sep != std::string::npos && end != std::string::npos) { + unsigned long id = std::stoul(line.substr(pos + 1, sep - pos - 1)); + Performance_Text_Run run; + run.value = true; + if (id < performance_field_count) + run.field_id = static_cast(id); + run.text = line.substr(sep + 1, end - sep - 1); + measure_run(canvas, run); + runs.push_back(std::move(run)); + pos = end + 1; + continue; + } + } + std::size_t next = line.find('\x1f', pos); + Performance_Text_Run run; + run.text = line.substr(pos, next == std::string::npos ? std::string::npos : next - pos); + measure_run(canvas, run); + runs.push_back(std::move(run)); + if (next == std::string::npos) + break; + pos = next; + } + return runs; + } + void measure_run(Canvas& canvas, Performance_Text_Run& run) { + run.measured_width = run.text.empty() ? 0.0 : canvas.measure_text(run.text).width; + if (!run.value) { + run.reserved_width = run.measured_width; + return; + } + auto index = static_cast(run.field_id); + if (index >= field_layout_states.size()) + index = static_cast(Performance_Field_Id::Generic_Number); + field_layout_states[index].max_value_width = std::max(field_layout_states[index].max_value_width, run.measured_width); + run.reserved_width = field_layout_states[index].max_value_width; + } + static std::string render_line_text(const Performance_Render_Line& line) { + std::string text; + for (const auto& run : line.runs) + text += run.text; + return text; + } + static std::string strip_markers(const std::string& line) { + std::string text; + for (std::size_t pos = 0; pos < line.size();) { + if (line[pos] == '\x1f') { + std::size_t sep = line.find('\x1e', pos + 1); + std::size_t end = sep == std::string::npos ? std::string::npos : line.find('\x1d', sep + 1); + if (sep != std::string::npos && end != std::string::npos) { + text.append(line, sep + 1, end - sep - 1); + pos = end + 1; + continue; + } + } + text.push_back(line[pos]); + ++pos; + } + return text; } }; } // namespace renderive diff --git a/Core/plottable/Performance_Snapshot.h b/Core/plottable/Performance_Snapshot.h new file mode 100644 index 0000000..19b33be --- /dev/null +++ b/Core/plottable/Performance_Snapshot.h @@ -0,0 +1,26 @@ +#pragma once +#include "../base/Geometry.h" +#include "../render/Image.h" +#include +#include +#include + +namespace renderive { + +struct Performance_Display_Snapshot { + Image image; + RectF destination_rect; + RectF viewport_rect; + RectF scroll_track_rect; + RectF scroll_thumb_rect; + Size viewport_size; + double content_height{}; + double scroll_offset{}; + double max_scroll_offset{}; + double line_height{}; + std::uint64_t viewport_version{}; + std::uint64_t content_version{}; + std::vector lines; +}; + +} // namespace renderive diff --git a/Core/plottable/Waterfall_p.h b/Core/plottable/Waterfall_p.h index 146af85..4bc6de2 100644 --- a/Core/plottable/Waterfall_p.h +++ b/Core/plottable/Waterfall_p.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -24,7 +25,6 @@ #include "Waterfall.h" namespace renderive { struct Waterfall_Data { - std::uint64_t sequence{}; int tick{}; std::pmr::vector frequency{memory_resource(Memory_Domain::Waterfall)}; std::shared_ptr completion_state; @@ -254,10 +254,8 @@ struct Waterfall_Private : Typed_Render_Data pending_latest_row; std::pmr::vector pending_image_rows{memory_resource(Memory_Domain::Waterfall)}; std::pmr::vector> pending_image_update_states{memory_resource(Memory_Domain::Update_Completion)}; - std::array>, Row_Queue_Capacity> row_slots; - std::atomic row_write_sequence{0}; - std::atomic row_consumed_sequence{0}; - std::uint64_t row_read_sequence{}; + rigtorp::MPMCQueue> row_queue; + std::atomic_uint64_t dropped_row_count{0}; std::uint64_t last_line_commit_ns{}; std::uint64_t image_color_map_hash{}; Waterfall_Image_Snapshot image_snapshot; @@ -278,6 +276,8 @@ struct Waterfall_Private : Typed_Render_Data row; + while (row_queue.try_pop(row)) { if (row && row->completion_state) row->completion_state->complete_all(std::make_error_code(std::errc::operation_canceled), Update_Outcome::Cancelled); } @@ -318,28 +318,24 @@ struct Waterfall_Private : Typed_Render_Data row) { if (!row) return; - std::uint64_t sequence = row_write_sequence.fetch_add(1, std::memory_order_acq_rel); - row->sequence = sequence; - auto replaced = row_slots[sequence % Row_Queue_Capacity].exchange(row, std::memory_order_acq_rel); - if (replaced && replaced->sequence >= row_consumed_sequence.load(std::memory_order_acquire) && replaced->completion_state) - replaced->completion_state->complete_all({}, Update_Outcome::Superseded); + if (!row_queue.try_push(row)) { + if (row->completion_state) + row->completion_state->complete_all({}, Update_Outcome::Superseded); + dropped_row_count.fetch_add(1, std::memory_order_relaxed); + return; + } if (row->completion_state) row->completion_state->complete_until(Update_Stage::Input_Released); if (q()) q()->mark_render_dirty(); } void consume_row_queue(std::pmr::vector& output) { - std::uint64_t write_sequence = row_write_sequence.load(std::memory_order_acquire); - std::uint64_t first_sequence = write_sequence > Row_Queue_Capacity ? write_sequence - Row_Queue_Capacity : 0; - row_read_sequence = std::max(row_read_sequence, first_sequence); - for (std::uint64_t sequence = row_read_sequence; sequence < write_sequence; ++sequence) { - auto row = row_slots[sequence % Row_Queue_Capacity].exchange({}, std::memory_order_acq_rel); - if (!row || row->sequence != sequence) + std::shared_ptr row; + while (row_queue.try_pop(row)) { + if (!row) continue; output.push_back(std::move(*row)); - row_consumed_sequence.store(sequence + 1, std::memory_order_release); } - row_read_sequence = write_sequence; } void commit_row_ticket(Waterfall_Data& row) { if (!row.completion_state) diff --git a/Core/plottable/export.h b/Core/plottable/export.h index 76c4ebf..db02fc1 100644 --- a/Core/plottable/export.h +++ b/Core/plottable/export.h @@ -1,10 +1,13 @@ #pragma once +#include +#include #include "Afterglow.h" #include "Audio_Frequency.h" #include "Hover_Info.h" #include "Interpolation.h" #include "Multi_Select_Rect.h" #include "Planisphere.h" +#include "Performance_Snapshot.h" #include "Spectrum.h" #include "Sweep_Frequency.h" #include "Waterfall.h" @@ -16,4 +19,11 @@ LIB_DECL std::shared_ptr attach_performance_shower(Plot_Core LIB_DECL void detach_performance_shower(Plot_Core& plot); LIB_DECL bool performance_shower_enabled(const Plot_Core& plot); LIB_DECL void set_performance_shower_enabled(Plot_Core& plot, bool enabled); +LIB_DECL std::shared_ptr performance_shower_snapshot(const Plot_Core& plot); +LIB_DECL void set_performance_shower_update_callback(Plot_Core& plot, std::function callback); +LIB_DECL void notify_performance_shower_viewport_changed(Plot_Core& plot); +LIB_DECL bool performance_shower_handle_wheel(Plot_Core& plot, PointF position, double angle_delta_y, double pixel_delta_y); +LIB_DECL bool performance_shower_handle_pointer_press(Plot_Core& plot, PointF position); +LIB_DECL bool performance_shower_handle_pointer_move(Plot_Core& plot, PointF position); +LIB_DECL bool performance_shower_handle_pointer_release(Plot_Core& plot, PointF position); } // namespace renderive diff --git a/Core/primitive/Curve.cpp b/Core/primitive/Curve.cpp index 025f45b..d50859b 100644 --- a/Core/primitive/Curve.cpp +++ b/Core/primitive/Curve.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "../base/Memory.h" #include "../render/Canvas.h" @@ -19,7 +20,6 @@ struct Curve_Input_Data : Input_Data {}; struct Curve_Render_State : Render_State, Curve_Prop {}; struct Curve_Input_Block { - std::uint64_t sequence{}; Curve_Input_Mode mode = Curve_Input_Mode::None; std::pmr::vector samples{memory_resource(Memory_Domain::Curve)}; std::pmr::vector points{memory_resource(Memory_Domain::Curve)}; @@ -172,10 +172,8 @@ struct Curve_Private : Typed_Render_Data samples{memory_resource(Memory_Domain::Curve)}; std::pmr::vector data_points{memory_resource(Memory_Domain::Curve)}; Curve_Input_Mode mode = Curve_Input_Mode::None; - std::array>, Input_Queue_Capacity> input_slots; - std::atomic input_write_sequence{0}; - std::atomic input_consumed_sequence{0}; - std::uint64_t input_read_sequence{}; + rigtorp::MPMCQueue> input_queue; + std::atomic_uint64_t dropped_input_count{0}; std::pmr::vector> pending_geometry_update_states{memory_resource(Memory_Domain::Update_Completion)}; Curve_Geometry_Result geometry_result; bool has_geometry_result{}; @@ -183,10 +181,13 @@ struct Curve_Private : Typed_Render_Data block; + while (input_queue.try_pop(block)) { if (block && block->completion_state) block->completion_state->complete_all(std::make_error_code(std::errc::operation_canceled), Update_Outcome::Cancelled); } @@ -200,11 +201,12 @@ struct Curve_Private : Typed_Render_Data block) { if (!block) return; - std::uint64_t sequence = input_write_sequence.fetch_add(1, std::memory_order_acq_rel); - block->sequence = sequence; - auto replaced = input_slots[sequence % Input_Queue_Capacity].exchange(block, std::memory_order_acq_rel); - if (replaced && replaced->sequence >= input_consumed_sequence.load(std::memory_order_acquire) && replaced->completion_state) - replaced->completion_state->complete_all({}, Update_Outcome::Superseded); + if (!input_queue.try_push(block)) { + if (block->completion_state) + block->completion_state->complete_all({}, Update_Outcome::Superseded); + dropped_input_count.fetch_add(1, std::memory_order_relaxed); + return; + } if (block->completion_state) block->completion_state->complete_until(Update_Stage::Input_Released); if (q()) @@ -212,20 +214,15 @@ struct Curve_Private : Typed_Render_Data Input_Queue_Capacity ? write_sequence - Input_Queue_Capacity : 0; - input_read_sequence = std::max(input_read_sequence, first_sequence); std::shared_ptr selected; - for (std::uint64_t sequence = input_read_sequence; sequence < write_sequence; ++sequence) { - auto block = input_slots[sequence % Input_Queue_Capacity].exchange({}, std::memory_order_acq_rel); - if (!block || block->sequence != sequence) + std::shared_ptr block; + while (input_queue.try_pop(block)) { + if (!block) continue; if (selected && selected->completion_state) selected->completion_state->complete_all({}, Update_Outcome::Superseded); selected = std::move(block); - input_consumed_sequence.store(sequence + 1, std::memory_order_release); } - input_read_sequence = write_sequence; if (!selected) return; mode = selected->mode; diff --git a/Core/primitive/Raster_Image.cpp b/Core/primitive/Raster_Image.cpp index 5e03308..8e30dd9 100644 --- a/Core/primitive/Raster_Image.cpp +++ b/Core/primitive/Raster_Image.cpp @@ -69,7 +69,10 @@ void Raster_Image::give_image_frame(const Image& image, Range domain_range, Rang if (!ok()) return; Raster_Image_Private* pd = d(); - auto lease = pd->state_control.mark_use_role(State_Edit); + auto record = pd->state_control.try_acquire_role(State_Edit); + if (record.result != Triple_Buffer_Result::Success) + return; + auto lease = record.lease; Raster_Image_Render_State* state = reinterpret_cast(pd->state_by_index(lease.index)); state->domain_range = domain_range; state->value_range = value_range; diff --git a/Core/render/Canvas.cpp b/Core/render/Canvas.cpp index 965166c..9883f3f 100644 --- a/Core/render/Canvas.cpp +++ b/Core/render/Canvas.cpp @@ -3,11 +3,59 @@ #include "Font_Face_p.h" #include "Image_p.h" #include "blend2d/blend2d.h" +#include +#include +#include +#include namespace renderive { + +namespace { +std::string& default_font_source_storage() { + static std::string source; + return source; +} +} // namespace + +std::shared_ptr Canvas::default_font_face() { + static std::shared_ptr face = []() -> std::shared_ptr { + static constexpr std::array candidates{ + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simsun.ttc", + "C:/Windows/Fonts/segoeui.ttf", + "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/tahoma.ttf" + }; + for (const char* path : candidates) { + std::error_code error; + if (!std::filesystem::exists(path, error)) + continue; + auto loaded = std::make_shared(); + if (loaded->impl_->face.create_from_file(path) == BL_SUCCESS) { + default_font_source_storage() = path; + return loaded; + } + } + return {}; + }(); + return face; +} + +Font Canvas::resolve_font(Font font) { + if (font.size <= 0.0) + font.size = 12.0; + if (!font.face || !font.face->valid()) + font.face = default_font_face(); + return font; +} + +bool Canvas::can_use_font(const Font& font) { + return font.size > 0.0 && font.face && font.face->valid(); +} + struct Canvas::Impl { Image& image; BLContext ctx; - Font current_font; + Font current_font = Canvas::resolve_font({}); bool antialias = true; Pen current_pen; Brush current_brush; @@ -29,6 +77,13 @@ struct Canvas::Impl { } ctx.set_fill_style(Convert::to_bl(current_brush.color)); } + void apply_text_fill() { + if (!current_pen.enabled()) { + ctx.set_fill_style(BLRgba(0.0, 0.0, 0.0, 0.0)); + return; + } + ctx.set_fill_style(Convert::to_bl(current_pen.color)); + } void apply_pen(double stroke_width = -1.0) { if (!current_pen.enabled()) { ctx.set_stroke_style(BLRgba(0.0, 0.0, 0.0, 0.0)); @@ -71,7 +126,7 @@ void Canvas::set_brush(const Brush& brush) { impl_->current_brush = brush; } void Canvas::set_font(const Font& font) { - impl_->current_font = font; + impl_->current_font = resolve_font(font); } void Canvas::set_antialias(bool enabled) { impl_->antialias = enabled; @@ -180,16 +235,17 @@ void Canvas::draw_image(const RectF& target, const Image& image, const RectF& so static_cast(source.width), static_cast(source.height))); } void Canvas::draw_text(PointF position, std::string_view text) { - if (text.empty() || !impl_->current_font.valid()) + if (text.empty() || !can_use_font(impl_->current_font)) return; BLFont bl_font; bl_font.create_from_face(impl_->current_font.face->impl_->face, static_cast(impl_->current_font.size)); - impl_->apply_brush(); + const BLFontMetrics& metrics = bl_font.metrics(); + impl_->apply_text_fill(); impl_->ctx.set_stroke_style(BLRgba(0.0, 0.0, 0.0, 0.0)); - impl_->ctx.fill_utf8_text(BLPoint(position.x, position.y + impl_->current_font.size), bl_font, text.data(), text.size()); + impl_->ctx.fill_utf8_text(BLPoint(position.x, position.y + metrics.ascent), bl_font, text.data(), text.size()); } void Canvas::draw_text(const RectF& rect, Text_Align align, std::string_view text) { - if (text.empty() || !impl_->current_font.valid()) + if (text.empty() || !can_use_font(impl_->current_font)) return; auto metrics = measure_text(text); double x = rect.x; @@ -199,21 +255,20 @@ void Canvas::draw_text(const RectF& rect, Text_Align align, std::string_view tex else if ((align & Text_Align::Right) == Text_Align::Right) x += rect.width - metrics.width; if ((align & Text_Align::VCenter) == Text_Align::VCenter) - y += (rect.height - metrics.height) / 2.0 + metrics.ascent; + y += (rect.height - metrics.height) / 2.0; else if ((align & Text_Align::Bottom) == Text_Align::Bottom) - y += rect.height - metrics.descent; - else - y += metrics.ascent; + y += rect.height - metrics.height; draw_text(PointF{x, y}, text); } Text_Metrics Canvas::measure_text(std::string_view text) const { return measure_text(impl_->current_font, text); } Text_Metrics Canvas::measure_text(const Font& font, std::string_view text) const { - if (text.empty() || !font.valid()) + Font resolved = resolve_font(font); + if (text.empty() || !can_use_font(resolved)) return {}; BLFont bl_font; - bl_font.create_from_face(font.face->impl_->face, static_cast(font.size)); + bl_font.create_from_face(resolved.face->impl_->face, static_cast(resolved.size)); BLGlyphBuffer gb; gb.set_utf8_text(text.data(), text.size()); bl_font.shape(gb); @@ -231,4 +286,41 @@ Text_Metrics Canvas::measure_text(const Font& font, std::string_view text) const Image& Canvas::image() const { return impl_->image; } + +Canvas::Font_Diagnostics Canvas::default_font_diagnostics() { + Font_Diagnostics diagnostics; + auto face = default_font_face(); + diagnostics.source = default_font_source_storage(); + diagnostics.face_valid = face && face->valid(); + Font font; + font.face = face; + font.size = 12.0; + diagnostics.font_valid = can_use_font(font); + if (!diagnostics.font_valid) + return diagnostics; + + Image image(1, 1); + image.fill(Color::transparent()); + Canvas canvas(image); + canvas.set_font(font); + Text_Metrics metrics = canvas.measure_text("0123456789 ABC \xE4\xB8\xAD\xE6\x96\x87"); + diagnostics.test_width = metrics.width; + diagnostics.test_height = metrics.height; + return diagnostics; +} + +void Canvas::diagnose_default_font_once() { + static bool done = false; + if (done) + return; + done = true; + Font_Diagnostics diagnostics = default_font_diagnostics(); + std::cout << "Renderive default font: source=" + << (diagnostics.source.empty() ? "" : diagnostics.source) + << " face_valid=" << (diagnostics.face_valid ? "true" : "false") + << " font_valid=" << (diagnostics.font_valid ? "true" : "false") + << " test_width=" << diagnostics.test_width + << " test_height=" << diagnostics.test_height + << std::endl; +} } // namespace renderive diff --git a/Core/render/Canvas.h b/Core/render/Canvas.h index 6835d2b..88670da 100644 --- a/Core/render/Canvas.h +++ b/Core/render/Canvas.h @@ -7,6 +7,7 @@ #include "Image_View.h" #include #include +#include #include namespace renderive { @@ -16,6 +17,14 @@ class Font_Face; class Canvas { public: + struct Font_Diagnostics { + bool face_valid{}; + bool font_valid{}; + double test_width{}; + double test_height{}; + std::string source; + }; + explicit Canvas(Image& image); ~Canvas(); @@ -58,7 +67,14 @@ public: Image& image() const; + [[nodiscard]] static Font_Diagnostics default_font_diagnostics(); + static void diagnose_default_font_once(); + private: + static std::shared_ptr default_font_face(); + static Font resolve_font(Font font); + static bool can_use_font(const Font& font); + struct Impl; std::unique_ptr impl_; }; diff --git a/Demo_Gallery/base/Tool.h b/Demo_Gallery/base/Tool.h index ceb5abc..d35d5bc 100644 --- a/Demo_Gallery/base/Tool.h +++ b/Demo_Gallery/base/Tool.h @@ -13,7 +13,6 @@ #include #include #include -#include "Renderive/export.h" namespace renderive { static Time_Of_Day current_time_of_day() { return renderive::qt_to_time_of_day(QTime::currentTime()); diff --git a/NotoSansCJK-Regular.otf b/NotoSansCJK-Regular.otf new file mode 100644 index 0000000..9bb532c Binary files /dev/null and b/NotoSansCJK-Regular.otf differ diff --git a/Qt/plot/Plot.cpp b/Qt/plot/Plot.cpp index baa52ac..c577e75 100644 --- a/Qt/plot/Plot.cpp +++ b/Qt/plot/Plot.cpp @@ -4,8 +4,11 @@ #include "../bridge/Qt_Presentation_Sink.h" #include "Core/architecture/Render_Time.h" #include "Core/base/global.h" +#include "Core/plottable/export.h" #include +#include #include +#include namespace renderive { @@ -31,8 +34,8 @@ std::string to_utf8_string(const QString& text) { } // namespace -void start_render_scheduler() { - Global::instance()->start_render_scheduler(); +void start_render_scheduler(Render_Runtime_Config config) { + Global::instance()->start_render_scheduler(config); } Abs_Plot_Private::Abs_Plot_Private(std::unique_ptr refresh_strategy) @@ -44,6 +47,17 @@ Abs_Plot_Private::~Abs_Plot_Private() = default; void Abs_Plot_Private::attach_widget(Abs_Plot* plot) { presentation_sink = std::make_shared(plot); core->set_presentation_sink(presentation_sink); + QPointer guarded_plot(plot); + auto pending = performance_update_pending; + set_performance_shower_update_callback(*core, [guarded_plot, pending]() mutable { + if (!guarded_plot || pending->exchange(true, std::memory_order_acq_rel)) + return; + QTimer::singleShot(0, guarded_plot.data(), [guarded_plot, pending]() mutable { + pending->store(false, std::memory_order_release); + if (guarded_plot) + guarded_plot->update(); + }); + }); } Abs_Plot::Abs_Plot(Abs_Plot_Private* private_data) @@ -56,6 +70,7 @@ Abs_Plot::Abs_Plot(Abs_Plot_Private* private_data) Abs_Plot::~Abs_Plot() { if (d) { + set_performance_shower_update_callback(*d->core, {}); d->core->pause_render(); delete d; d = nullptr; @@ -132,16 +147,40 @@ void Abs_Plot_Private::paint_event(Abs_Plot* plot, QPaintEvent* event) { else { painter.fillRect(plot->rect(), plot->background_color()); } + auto performance_snapshot = performance_shower_snapshot(*core); + if (performance_snapshot && !performance_snapshot->image.empty()) { + Size current_size = core->viewport_size(); + auto ctx = core->render_context(); + std::uint64_t current_viewport_version = ctx ? ctx->viewport_version.load(std::memory_order_acquire) : 0; + if (performance_snapshot->viewport_size == current_size && performance_snapshot->viewport_version == current_viewport_version) { + QImage overlay = Qt_Image_Adapter::to_qimage(performance_snapshot->image.view()); + const RectF& target = performance_snapshot->destination_rect; + painter.drawImage(QRectF(target.x, target.y, target.width, target.height), overlay); + } + } painter.end(); core->end_present(std::move(frame), paint_begin_time, steady_now_ns()); } +void Abs_Plot_Private::request_performance_overlay_update(Abs_Plot* plot) { + if (!plot || performance_update_pending->exchange(true, std::memory_order_acq_rel)) + return; + QPointer guarded_plot(plot); + auto pending = performance_update_pending; + QTimer::singleShot(0, plot, [guarded_plot, pending]() mutable { + pending->store(false, std::memory_order_release); + if (guarded_plot) + guarded_plot->update(); + }); +} + bool Abs_Plot_Private::event(Abs_Plot* plot, QEvent* event, bool base_result) { switch (event->type()) { case QEvent::Resize: { auto* resize_event = static_cast(event); core->set_viewport_size(Qt_Event_Adapter::to_size(resize_event->size())); + notify_performance_shower_viewport_changed(*core); core->dispatch_event(Qt_Event_Adapter::to_resize_event(resize_event)); core->mark_render_state_dirty(); break; @@ -163,18 +202,48 @@ bool Abs_Plot_Private::event(Abs_Plot* plot, QEvent* event, bool base_result) { core->dispatch_event(leave_event); break; } - case QEvent::Wheel: - core->dispatch_event(Qt_Event_Adapter::to_wheel_event(static_cast(event))); + case QEvent::Wheel: { + auto wheel_event = Qt_Event_Adapter::to_wheel_event(static_cast(event)); + if (performance_shower_handle_wheel(*core, wheel_event.position, wheel_event.angle_delta_y, wheel_event.pixel_delta_y)) { + event->accept(); + request_performance_overlay_update(plot); + return true; + } + core->dispatch_event(wheel_event); break; - case QEvent::MouseMove: - core->dispatch_event(Qt_Event_Adapter::to_pointer_event(static_cast(event), Event_Type::Pointer_Move)); + } + case QEvent::MouseMove: { + auto pointer_event = Qt_Event_Adapter::to_pointer_event(static_cast(event), Event_Type::Pointer_Move); + if (performance_shower_handle_pointer_move(*core, pointer_event.position)) { + event->accept(); + request_performance_overlay_update(plot); + return true; + } + core->dispatch_event(pointer_event); break; - case QEvent::MouseButtonPress: - core->dispatch_event(Qt_Event_Adapter::to_pointer_event(static_cast(event), Event_Type::Pointer_Press)); + } + case QEvent::MouseButtonPress: { + auto* mouse_event = static_cast(event); + auto pointer_event = Qt_Event_Adapter::to_pointer_event(mouse_event, Event_Type::Pointer_Press); + if (mouse_event->button() == Qt::LeftButton && performance_shower_handle_pointer_press(*core, pointer_event.position)) { + event->accept(); + request_performance_overlay_update(plot); + return true; + } + core->dispatch_event(pointer_event); break; - case QEvent::MouseButtonRelease: - core->dispatch_event(Qt_Event_Adapter::to_pointer_event(static_cast(event), Event_Type::Pointer_Release)); + } + case QEvent::MouseButtonRelease: { + auto* mouse_event = static_cast(event); + auto pointer_event = Qt_Event_Adapter::to_pointer_event(mouse_event, Event_Type::Pointer_Release); + if (mouse_event->button() == Qt::LeftButton && performance_shower_handle_pointer_release(*core, pointer_event.position)) { + event->accept(); + request_performance_overlay_update(plot); + return true; + } + core->dispatch_event(pointer_event); break; + } case QEvent::KeyPress: core->dispatch_event(Qt_Event_Adapter::to_key_event(static_cast(event), Event_Type::Key_Press)); break; diff --git a/Qt/plot/Plot.h b/Qt/plot/Plot.h index 79d8c0e..c9298d6 100644 --- a/Qt/plot/Plot.h +++ b/Qt/plot/Plot.h @@ -1,5 +1,6 @@ #pragma once #include "Core/architecture/global.h" +#include "Core/architecture/Render_Config.h" #include "Core/architecture/Renderable.h" #include #include @@ -10,7 +11,7 @@ namespace renderive { -void LIB_DECL start_render_scheduler(); +void LIB_DECL start_render_scheduler(Render_Runtime_Config config = {}); struct Abs_Plot_Private; class Plot_Core; diff --git a/Qt/plot/Plot_p.h b/Qt/plot/Plot_p.h index cdfe53e..0718b02 100644 --- a/Qt/plot/Plot_p.h +++ b/Qt/plot/Plot_p.h @@ -2,6 +2,7 @@ #include "Plot.h" #include "Core/plot/Plot_Core.h" #include "Core/plot/Refresh_Strategy.h" +#include #include namespace renderive { @@ -15,10 +16,12 @@ struct Abs_Plot_Private { void attach_widget(Abs_Plot* plot); bool event(Abs_Plot* plot, QEvent* event, bool base_result); void paint_event(Abs_Plot* plot, QPaintEvent* event); + void request_performance_overlay_update(Abs_Plot* plot); Refresh_Strategy* strategy{}; std::unique_ptr core; std::shared_ptr presentation_sink; + std::shared_ptr performance_update_pending = std::make_shared(false); }; } // namespace renderive diff --git a/Renderive_三帧负反馈统一改进计划_Codex5.5 (1).md b/Renderive_三帧负反馈统一改进计划_Codex5.5 (1).md deleted file mode 100644 index b8e9463..0000000 --- a/Renderive_三帧负反馈统一改进计划_Codex5.5 (1).md +++ /dev/null @@ -1,928 +0,0 @@ -# Renderive 三帧负反馈统一改进计划 - -## 1. 结论 - -当前代码并非全部不可用。轴映射正交化、Core/Qt 分层、后台 Scheduler、真实定时器、Renderable Tree 和 Update Completion 可以保留。 - -需要整体替换的是一条完整链路: - -```text -Plot 脏状态 -→ 刷新策略 -→ 三帧角色交换 -→ 后台帧内并行计算 -→ 完整帧发布 -→ Qt paintEvent 消费 -→ 帧反馈 -→ 下一次刷新决策 -``` - -当前链路同时混入了三套不一致模型: - -1. `Triple_Role_Buffer_Control` 中仍有 `wait_*`、自旋和等待交换。 -2. Curve、Waterfall 各自维护跨帧异步 worker、generation、published snapshot,并在 worker 完成后再次标脏。 -3. `Frame_Feedback_Controller` 使用 Input/Producer/Consumer/Balanced 等启发式分类和 adaptive interval,不是规定的 Render/Paint/User 三限制源模型。 - -本次改造只保留一套模型: - -> 三个完整帧在 Rendering、Middle、Painting 三个角色间循环;所有交换只尝试、不等待;Curve 和 Waterfall 只是当前帧内部的并行计算;自动策略只根据 Render、Paint、User 三个周期确定限制来源;Performance 在帧从 Painting 返回 Middle 时无锁采样并独立生成显示快照。 - -本计划作为一次性批量任务执行。不要在中途保留旧实现、兼容转发或两套调度路径。完成所有修改后统一构建和验收。 - ---- - -## 2. 本次保留、重写和删除范围 - -### 2.1 保留 - -以下实现方向正确,不要重写: - -- `Axis_Mapping_2D`、`Axis_Basis_2D` 及 domain/value 轴语义。 -- Frequency/Time 轴横纵切换和正反流向映射。 -- `Core` 与 `Qt` 的分层结构。 -- `Render_Scheduler` 的 Scheduler 线程、CPU 线程池和 `post_at()` 真定时器。 -- Renderable Tree 快照和事件树传播。 -- `Update_State` 的 Rendered/Presented 完成语义。 -- Blend2D `Image/Canvas` 后台绘制路径。 -- `Color_Scale` 单一状态源。 - -除编译适配外,本批次不要继续修改轴模型。 - -### 2.2 整体重写 - -以下文件或模块需要按照本计划整体替换: - -```text -Core/architecture/Triple_Buffer.h -Core/architecture/Frame_Scheduler.h -Core/architecture/Frame_Scheduler.cpp -Core/architecture/Render_Config.h -Core/plot/Plot_Frame_Pipeline.h -Core/plot/Plot_Frame_Pipeline.cpp -Core/plot/Refresh_Strategy.h -Core/plot/Latency_Eager_Refresh_Strategy.h -Core/plot/Latency_Eager_Refresh_Strategy.cpp -Core/plot/Explicit_Refresh_Strategy.h -Core/plot/Explicit_Refresh_Strategy.cpp -Core/plot/Plot_Core.cpp -Core/plot/Present_Frame.h -Core/plot/Present_Frame.cpp -``` - -### 2.3 删除 - -直接删除以下旧语义,不保留兼容层: - -- `wait_mark_use_role()`。 -- `wait_swap_role()`。 -- `wait_index_free()`。 -- Triple Buffer 中所有 `yield`、`atomic::wait` 和等待循环。 -- `Frame_Buffer_State` 以及 `Plot_Frame::state`。 -- `Bottleneck_State::Input_Limited`。 -- `Bottleneck_State::Producer_Limited`。 -- `Bottleneck_State::Consumer_Limited`。 -- `Bottleneck_State::Manual_Limited`。 -- `Bottleneck_State::Balanced`。 -- `adaptive_min_render_interval_ms` 和根据占用率缓慢增减 interval 的控制逻辑。 -- Curve 的 `Curve_Geometry_Worker_State`、`published_result`、独立 generation 和 worker 完成后 `mark_render_dirty()`。 -- Waterfall 的 `Waterfall_Image_Worker_State`、`published_snapshot`、独立 generation 和 worker 完成后 `mark_render_dirty()`。 -- Performance worker 完成后调用主 Plot `mark_render_dirty()` 的路径。 -- `Refresh_Strategy` 中大量默认空实现虚函数。 -- 为旧接口增加的所有转发 getter、setter 和兼容适配。 - ---- - -## 3. 唯一三帧模型 - -### 3.1 三个逻辑角色 - -三个物理 `Plot_Frame` 只通过角色映射决定当前职责: - -```cpp -enum Frame_Role : std::uint8_t { - Frame_Rendering, - Frame_Middle, - Frame_Painting -}; -``` - -角色含义: - -| 角色 | 含义 | -|---|---| -| `Frame_Rendering` | 后台线程当前可以绘制或正在绘制的帧 | -| `Frame_Middle` | 中间帧;可能是已完成待 Paint 的帧,也可能是刚从 Painting 返回、等待成为 Rendering 的帧 | -| `Frame_Painting` | Qt 前台当前持有或最近一次显示的帧 | - -`Plot_Frame` 不再保存一份可与角色映射冲突的 `Free/Producing/Ready/Consuming` 状态。 - -帧是否包含可供 Paint 的新内容,只由帧版本和 transition 时间判断,不能再维护第二套状态机。 - -### 3.2 唯一角色交换 - -三帧只有两种交换: - -```text -Rendering ↔ Middle -Middle ↔ Painting -``` - -对应帧循环: - -```text -2 → 1:Middle 帧在 Rendering ↔ Middle 交换后成为下一张 Rendering 帧 -1 → 2:当前 Rendering 帧完成后成为 Middle 帧 -2 → 3:Middle 新帧进入 paintEvent -3 → 2:旧 Painting 帧在下一次 Middle ↔ Painting 交换时返回 Middle -``` - -所有交换必须调用 `try_swap_role()`,调用一次后立即返回结果。 - -禁止: - -- 等待物理缓冲释放。 -- 原地自旋重试。 -- 在一次 API 调用内循环直到成功。 -- 用 condition variable 等三帧交换。 - -### 3.3 为什么 3→2 不在 `end_present()` 中交换 - -`paintEvent` 进行期间,后台可能已经完成一次 `Rendering ↔ Middle` 交换。如果 `end_present()` 再强制执行 `Painting ↔ Middle`,可能把尚未 Paint 的新 Middle 帧错误移动到 Painting 角色。 - -因此采用以下语义: - -1. `begin_present()` 成功交换 `Middle ↔ Painting`。 -2. 交换时,旧 Painting 帧完成 `3→2`,在此处完成统计上报。 -3. 新 Painting 帧开始本次 Paint,记录 `paint_begin`。 -4. `end_present()` 只记录 `paint_end` 并释放 Painting 租约,不改变角色映射。 -5. 该帧在下一次成功 `Middle ↔ Painting` 交换时完成 `3→2`。 - -这样不会与后台发布发生角色冲突。 - ---- - -## 4. 帧生命周期信息 - -### 4.1 原始信息只保存在帧上 - -每个帧只保存原始时间点和本帧结果: - -```cpp -enum class Frame_Outcome : std::uint8_t { - Presented, - Superseded, - Render_Acquire_Dropped, - Publish_Dropped, - Paint_Acquire_Dropped, - Worker_Budget_Dropped, - Cancelled -}; - -struct Frame_Lifecycle_Record { - std::uint64_t frame_id{}; - std::uint64_t input_version{}; - std::uint64_t render_role_enter_ns{}; - std::uint64_t render_begin_ns{}; - std::uint64_t render_end_ns{}; - std::uint64_t transition_12_ns{}; - std::uint64_t transition_23_ns{}; - std::uint64_t paint_begin_ns{}; - std::uint64_t paint_end_ns{}; - std::uint64_t transition_32_ns{}; - std::uint64_t wait_21_ns{}; - std::uint64_t wait_12_ns{}; - std::uint64_t wait_23_ns{}; - std::uint64_t wait_32_ns{}; - Frame_Outcome outcome{}; - Refresh_Control_Snapshot refresh; -}; -``` - -具体定义: - -| 字段 | 计算方式 | -|---|---| -| `wait_21` | `render_begin - render_role_enter`;帧成为 Rendering 角色后等待真正开始绘制的时间 | -| `wait_12` | `transition_12 - render_end`;绘制结束后到成功发布 Middle 的时间 | -| `wait_23` | `paint_begin - transition_12`;Middle 新帧等待进入 Paint 的时间 | -| `wait_32` | `transition_32 - paint_end`;Paint 完成后等待返回 Middle 的时间 | - -只保存时间点或最终的四个等待值之一,避免重复。推荐保存时间点并在帧完成时计算等待值;Performance 快照再计算 FPS 和比例。 - -### 4.2 删除帧内动态统计容器 - -当前 `Frame_Lifecycle_Record` 中的以下内容会在热路径分配内存并让 Performance 采样变重: - -```text -std::vector -std::vector -std::string object_name -std::string parent_object_name -``` - -本批次从核心帧记录中删除。 - -需要 Renderable 级分析时,另做显式 Debug Trace 模式,不默认进入每一帧,也不参与负反馈控制。 - -### 4.3 无帧可写时的失败累计 - -当 `try_begin_render()` 尚未取得帧时,不存在当前帧可记录失败。`Plot_Frame_Pipeline` 只保留一个最小累计器: - -```cpp -struct Frame_Attempt_Accumulator { - std::uint64_t render_role_busy_count{}; - std::uint64_t publish_busy_count{}; - std::uint64_t paint_role_busy_count{}; - std::uint64_t worker_budget_busy_count{}; -}; -``` - -这些值在下一张成功创建的帧中一次性转移并清零。 - -不要在 Plot、Refresh Strategy 和 Performance 中各保存一份失败计数。 - ---- - -## 5. 负反馈控制器 - -### 5.1 只允许三个限制来源 - -替换旧 `Bottleneck_State`: - -```cpp -enum class Refresh_Limit_Source : std::uint8_t { - Unknown, - Render, - Paint, - User -}; -``` - -限制来源只由三个周期比较得到: - -```text -render_period = smooth(render_duration + wait_21) -paint_period = smooth(paint_duration + wait_23) -user_period = 1 / user_max_fps - -target_period = max(render_period, paint_period, user_period) -limit_source = argmax(render_period, paint_period, user_period) -``` - -对应最大 FPS: - -```text -render_capacity_fps = 1 / render_period -paint_capacity_fps = 1 / paint_period -user_limit_fps = 1 / user_period -actual_fps = min(render_capacity_fps, paint_capacity_fps, user_limit_fps) -``` - -### 5.2 平滑与防抖 - -允许控制器保存三组 EWMA/SRTT 滤波状态: - -```text -render period filter -paint period filter -当前 limit source 的防抖状态 -``` - -禁止继续保存几十个派生比例作为调度权威来源。 - -限制来源切换规则: - -- 候选周期比当前周期大 3% 以上,连续 3 个有效样本后切换。 -- 三者差异在 3% 内时保持当前限制来源。 -- 首帧尚无 Render/Paint 样本时,第一帧立即渲染;后续至少遵守 User 上限。 - -### 5.3 帧上的控制快照 - -每帧只记录一次控制结果: - -```cpp -struct Refresh_Control_Snapshot { - Refresh_Limit_Source source{}; - std::uint64_t render_period_ns{}; - std::uint64_t paint_period_ns{}; - std::uint64_t user_period_ns{}; - std::uint64_t target_period_ns{}; -}; -``` - -Performance 只能读取这份结果,不能自己重新实现限制来源判断。 - ---- - -## 6. 自动刷新策略 - -### 6.1 Render 受限 - -当 `source == Render`: - -```text -当前帧完成并尝试发布 -→ 检查正式 dirty version -→ 如果仍脏且没有活动 Frame Job -→ 立即尝试开始下一帧 -``` - -不使用 timer。 - -渲染耗时本身就是自然限速。 - -### 6.2 Paint 受限 - -当 `source == Paint`: - -- 后台最多保留一张待 Paint 的 Middle 新帧。 -- Middle 已有新帧时,不继续追帧,不反复生成并 supersede。 -- `end_present()` 后如果 Middle 中已有新帧,且 Qt 更新事件未挂起,立即请求一次 Qt 更新。 -- Qt 更新事件必须合并,同一时刻只能有一个 pending update。 -- Paint 完成后向 Scheduler 投递一次重新评估,使后台在有空闲角色且仍脏时开始下一帧。 - -不使用 timer。 - -### 6.3 User 受限 - -只有 `source == User` 时使用 `post_at()`: - -```text -next_deadline = last_render_begin + user_period -``` - -输入继续变脏时只合并 dirty version,不重复创建 timer。 - -### 6.4 Unknown - -`Unknown` 只允许出现在预热阶段: - -- 第一帧立即渲染。 -- 已有 User 上限时,第二帧起不得超过 User 上限。 -- 获得一次 Render 和一次 Paint 样本后必须进入 Render、Paint 或 User 之一。 - ---- - -## 7. 手动刷新策略 - -手动策略独立实现,不使用自动策略的追帧逻辑。 - -语义: - -```text -replot() -→ pending_manual_request = true -→ Scheduler 尝试渲染一帧 -→ 连续请求合并为一个 pending 标志 -``` - -要求: - -- `replot()` 不得调用 `mark_render_state_dirty()` 伪造输入版本。 -- 增加内部业务入口 `request_manual_refresh()`。 -- 手动请求在渲染忙时保持 pending,当前帧完成后再尝试一次。 -- 手动模式不根据帧反馈自动追帧。 -- 帧统计仍然正常生成,供 Performance 展示。 - ---- - -## 8. Refresh Strategy 接口重构 - -删除当前大量默认空虚函数,改成单一事件入口: - -```cpp -enum class Refresh_Trigger : std::uint8_t { - Started, - Input_Dirty, - Render_Finished, - Paint_Finished, - Frame_Returned, - Timer_Fired, - Manual_Request, - Paused -}; - -struct Refresh_Runtime_Snapshot { - bool enabled{}; - bool destroying{}; - bool dirty{}; - bool render_job_active{}; - bool middle_has_new_frame{}; - bool paint_active{}; - std::uint64_t now_ns{}; -}; - -struct Refresh_Action { - bool try_render{}; - bool request_paint{}; - std::uint64_t render_deadline_ns{}; -}; - -class Refresh_Strategy { -public: - virtual ~Refresh_Strategy() = default; - virtual Refresh_Action on_trigger( - Refresh_Trigger trigger, - const Refresh_Runtime_Snapshot& runtime, - const Frame_Lifecycle_Record* frame) = 0; -}; -``` - -两个具体策略都完整实现这个唯一函数。 - -公共 Plot API 不暴露角色、buffer、worker 或 Private 指针。 - ---- - -## 9. Plot Frame Pipeline - -`Plot_Frame_Pipeline` 只负责: - -- 三个帧对象。 -- 三个角色映射。 -- try lease。 -- 两种 try swap。 -- 帧版本。 -- Qt update pending 合并。 -- 最小 attempt accumulator。 - -建议内部接口: - -```cpp -Render_Begin_Result try_begin_render(std::uint64_t now_ns); -Publish_Result try_publish_rendered(Render_Frame_Lease&& lease, std::uint64_t now_ns); -Paint_Begin_Result try_begin_paint(std::uint64_t now_ns); -void finish_paint(Paint_Frame_Lease&& lease, std::uint64_t now_ns); -bool middle_has_new_frame() const; -``` - -### 9.1 `try_begin_render` - -- 尝试租用 `Frame_Rendering`。 -- 失败立即返回并累计 `render_role_busy_count`。 -- 成功后重置该帧新一轮 metadata。 -- `wait_21 = now - render_role_enter_ns`。 -- 捕获当前正式 input version。 - -### 9.2 `try_publish_rendered` - -- 先释放 Rendering 写租约。 -- 尝试交换 `Frame_Rendering ↔ Frame_Middle`。 -- 成功:记录当前帧 `transition_12` 和 `wait_12`。 -- 被换入 Rendering 的旧 Middle 帧记录新的 `render_role_enter_ns`。 -- 旧 Middle 如果包含未 Paint 的新帧,标记 `Superseded` 并上报。 -- 失败:当前帧标记 `Publish_Dropped`,不上锁、不等待、不重试交换。 - -### 9.3 `try_begin_paint` - -- 只有 Middle 版本新于 Painting 版本时尝试交换。 -- 尝试交换 `Frame_Middle ↔ Frame_Painting`。 -- 交换成功时: - - 旧 Painting 完成 `3→2`。 - - 记录旧帧 `transition_32` 和 `wait_32`。 - - 将旧帧统计无锁交给 Performance。 - - 新 Painting 记录 `transition_23`、`paint_begin` 和 `wait_23`。 -- 失败立即返回,不等待。 - -### 9.4 `finish_paint` - -- 只写 `paint_end`。 -- 完成 Presented update states。 -- 释放 Painting 租约。 -- 不交换角色。 -- 根据刷新策略决定是否请求下一次 Paint 或唤醒后台评估。 - ---- - -## 10. Curve 和 Waterfall 只做帧内并行 - -### 10.1 统一 Frame Task Group - -在 `Core/architecture` 增加内部任务组: - -```cpp -class Frame_Task_Group { -public: - bool try_start(std::size_t task_count, Task continuation); - void complete_one(); -}; -``` - -要求: - -- 任务组属于一个 `Frame_Job`。 -- Frame Job 捕获不可变 Plot/Axis/Renderable/Input 快照。 -- CPU task 只能写各自独占输出区。 -- 最后一个 task 完成后,向 Scheduler 投递 continuation。 -- Scheduler 不使用阻塞 wait。 -- CPU pool 必须有有界 worker budget。 -- 拿不到预算时,本帧直接单线程计算或丢弃,行为必须固定,不得无限排队。 - -本批次选择统一行为: - -> worker budget 不足时直接在 Scheduler 后台线程单线程计算当前帧,保证语义正确,不新建异步积压。 - -### 10.2 Curve - -保留: - -- 原始输入快照。 -- Axis mapping。 -- Peak Envelope/点转换算法。 - -删除: - -- 独立 Curve worker state。 -- 跨帧 `published_result`。 -- Curve generation。 -- worker 完成后再次标脏。 - -新流程: - -```text -当前 Frame Job prepare Curve -→ 根据屏幕 domain 范围拆分数据段 -→ CPU pool 并行生成互不重叠的几何分段 -→ continuation 按 domain 顺序合并 -→ 当前后台帧中直接 draw -``` - -最终 Blend2D 绘制仍在后台 Scheduler 帧内执行即可;本批次不再增加 Curve Tile raster pipeline。 - -### 10.3 Waterfall - -删除: - -- 跨帧 image request。 -- 独立 published image snapshot。 -- image generation。 -- worker 完成后标脏。 - -新流程: - -```text -当前 Frame Job 捕获 Waterfall ring、时间轴、频率轴、Color Scale 快照 -→ 计算可见二维 Tile 网格 -→ 每个 worker 只写一个 Tile 输出区 -→ continuation 合成 Tile 到当前 Rendering 帧 -``` - -Tile 必须是真正二维分块,不是只按 Y 切条带。 - -建议默认 Tile: - -```text -64×64 -``` - -但 Tile 大小只保留一个内部常量,本批次不增加公共配置接口。 - -时间轴横纵方向和流向继续完全由 `Axis_Mapping_2D/Axis_Basis_2D` 决定。 - ---- - -## 11. Performance Shower - -### 11.1 不再作为被测 Renderable - -`Performance_Shower` 不再继承普通 `Renderable`,避免: - -- 参与被测帧 prepare。 -- 参与被测帧 draw。 -- measure_text 和 draw_text 污染 render duration。 -- worker 完成后标脏主 Plot。 - -替换为独立性能监视对象和独立显示快照。 - -### 11.2 采样时机 - -只在旧 Painting 帧成功完成 `3→2` 时采样。 - -此时帧已经拥有: - -- Render 时间。 -- 12 等待。 -- 23 等待。 -- Paint 时间。 -- 32 等待。 -- Refresh limit source。 - -采样路径必须: - -- 不加 mutex。 -- 不等待。 -- 不分配内存。 -- 不格式化字符串。 -- 不写日志。 -- 队列满直接丢样本并增加一个 dropped sample 计数。 - -### 11.3 队列 - -每个 Plot 只有一个 Qt Paint 生产线程和一个 Performance worker 消费线程,使用固定容量 SPSC ring。 - -禁止继续使用当前“先增加 write sequence、后发布 atomic shared_ptr slot”的队列。 - -`Frame_Performance_Sample` 必须是小型 POD,只包含核心帧时间和控制快照。 - -### 11.4 独立后台显示 - -Performance worker: - -```text -消费 POD sample -→ 聚合 EWMA/min/max/percentile -→ 按独立频率生成文字和面板 Image -→ 原子发布不可变 Performance_Display_Snapshot -``` - -`Performance_Display_Snapshot`: - -```cpp -struct Performance_Display_Snapshot { - Image image; - std::uint64_t version{}; -}; -``` - -默认显示刷新频率作为 Performance 自身业务配置,例如 5 Hz。 - -该频率只控制性能面板快照,不控制主 Plot。 - -Qt 层使用独立 overlay 或独立 Widget 展示该 Image。不得通过主 Plot `mark_render_dirty()` 请求刷新。 - ---- - -## 12. Plot Core 执行流程 - -自动模式完整流程: - -```text -输入线程更新正式 dirty version -→ 向 Scheduler 合并投递 Input_Dirty -→ Strategy 根据当前 source 返回 action -→ Plot try_begin_render -→ 捕获不可变 Frame Job 快照 -→ Curve/Waterfall 帧内并行计算 -→ Scheduler continuation 完成本帧 draw -→ try_publish_rendered -→ 按 source 执行: - Render:仍脏则立即 try 下一帧 - Paint:等待 Paint 完成事件驱动 - User:按 user deadline 定时 -→ Qt begin_present try Middle↔Painting -→ paintEvent 只绘制完整 Image -→ end_present 记录 paint_end -→ 下一次 Middle↔Painting 时旧帧 3→2 并交给 Performance -``` - -整个系统中不能存在: - -```text -Curve 完成 → mark dirty → 再渲染一帧 -Waterfall 完成 → mark dirty → 再渲染一帧 -Performance 完成 → mark dirty → 再渲染一帧 -``` - ---- - -## 13. 生命周期 - -每个 `Frame_Job` 必须持有: - -- `shared_ptr` 生命周期令牌。 -- 当前 Renderable Tree 不可变快照。 -- 当前 Axis/Input 快照。 -- 当前 Rendering 帧租约。 -- 当前 task group。 - -Plot 销毁时: - -- 标记 context destroying。 -- 新 task 不再启动。 -- 已启动 task 完成后发现 destroying,只释放资源,不 publish。 -- 不捕获裸 `Plot_Core_Private*` 跨 CPU task。 -- Scheduler continuation 使用 weak/context token。 - ---- - -## 14. 单次批量执行顺序 - -以下是同一个任务内部的依赖顺序,不是多个独立计划: - -1. 定义新的 Frame Role、Outcome、Refresh Limit Source 和最小帧记录。 -2. 删除 Triple Buffer 全部等待接口,重写纯 try 角色交换。 -3. 重写 `Plot_Frame_Pipeline` 和 `Present_Frame` 的 12/23/32/21 生命周期。 -4. 重写 Refresh Strategy 和三限制源反馈控制器。 -5. 重写 Plot Core,使 Render/Paint/User 三条调度路径唯一化。 -6. 增加 Frame Task Group,把 Curve 和 Waterfall 改为帧内并行。 -7. 把 Performance 从 Renderable Tree 拆出,接入 3→2 SPSC 采样和独立 Image Snapshot。 -8. 删除所有旧 worker、旧状态、旧接口和兼容层。 -9. 新建统一 Core 测试目标,完成全部测试后一次性验收。 - -不得在第 2~8 步之间提交“临时兼容实现”。 - ---- - -## 15. Core 测试用例 - -新建唯一测试目标: - -```text -Renderive_Core_Tests -``` - -并接入 CTest。 - -### 15.1 Triple Frame 确定性测试 - -必须覆盖: - -1. 初始三个角色索引互不重复。 -2. `Rendering ↔ Middle` 成功后角色正确轮转。 -3. `Middle ↔ Painting` 成功后角色正确轮转。 -4. 物理帧被租用时 try 操作立即返回 Busy。 -5. 无任何等待、sleep、yield 或循环重试。 -6. Paint 进行时后台发布不会破坏 Painting 帧。 -7. Ready/Middle 未消费时新帧发布,旧帧正确记为 Superseded。 -8. publish 竞争失败时帧记为 Publish_Dropped,状态机仍可继续。 -9. 连续一百万次角色交换不存在重复角色或丢失物理帧。 - -### 15.2 时间记录测试 - -使用可注入 Fake Clock,精确验证: - -```text -wait_21 = render_begin - render_role_enter -wait_12 = transition_12 - render_end -wait_23 = paint_begin - transition_12 -wait_32 = transition_32 - paint_end -``` - -禁止依赖真实 sleep 测时间。 - -### 15.3 Feedback 测试 - -构造固定样本: - -| Render 周期 | Paint 周期 | User 周期 | 预期来源 | -|---:|---:|---:|---| -| 20 ms | 10 ms | 5 ms | Render | -| 8 ms | 25 ms | 5 ms | Paint | -| 5 ms | 8 ms | 33 ms | User | - -再验证: - -- Render 限制时 Render_Finished + dirty 返回立即渲染。 -- Paint 限制时 Render_Finished 不追帧;Paint_Finished 请求 Paint/Scheduler 评估。 -- User 限制时只返回 deadline。 -- 3% 内不反复切换来源。 -- 预热结束后不再返回 Unknown。 -- Feedback 使用 Plot 正式 input version,不自己生成另一套版本。 - -### 15.4 Explicit Strategy 测试 - -- 没有 `replot()` 不渲染。 -- 连续十次 `replot()` 合并为一次 pending。 -- 当前渲染忙时 pending 不丢失。 -- 完成后最多再执行一次。 -- `replot()` 不增加 input dirty version。 - -### 15.5 Curve 帧内并行测试 - -- 串行参考实现与并行结果点数、坐标、顺序一致。 -- 1、2、4、8 worker 输出一致。 -- 轴横纵交换和正反流向输出一致。 -- Curve worker 完成后不会调用 `mark_render_dirty()`。 -- 仓库中不存在 `Curve_Geometry_Worker_State` 和 `published_result`。 - -### 15.6 Waterfall Tile 测试 - -- 串行整图参考与二维 Tile 合成逐像素一致。 -- 时间轴横向正流、横向反流、纵向正流、纵向反流全部正确。 -- Tile 之间无重叠写。 -- 1、2、4、8 worker 输出一致。 -- Waterfall worker 完成后不会调用 `mark_render_dirty()`。 -- 仓库中不存在 `Waterfall_Image_Worker_State` 和 `published_snapshot`。 - -### 15.7 Performance 测试 - -- 只有成功 3→2 时产生样本。 -- 采样路径没有动态分配。 -- SPSC 队列满时立即丢弃,不等待。 -- worker 独立频率发布 Image Snapshot。 -- worker 不调用主 Plot `mark_render_dirty()`。 -- Performance 开启和关闭时,主 Plot Feedback 的 render period 在误差范围内一致。 - -### 15.8 生命周期与压力测试 - -- Render 中销毁 Plot。 -- Curve/Waterfall task 运行中销毁 Renderable。 -- CPU continuation 晚于 Plot 销毁。 -- 输入线程、Scheduler、Qt 模拟线程持续并发 60 秒。 -- 高频 resize、换轴、删除图元、暂停和恢复。 -- 所有 Update Ticket 最终进入 Presented、Superseded 或 Cancelled,不能永久等待。 - ---- - -## 16. 静态验收 - -以下搜索结果必须为 0: - -```text -wait_mark_use_role -wait_swap_role -wait_index_free -std::this_thread::yield -Frame_Buffer_State -Curve_Geometry_Worker_State -Waterfall_Image_Worker_State -published_result -worker 完成路径中的 mark_render_dirty -``` - -以下类型只能各有一个定义: - -```text -Refresh_Limit_Source -Frame_Lifecycle_Record -Refresh_Control_Snapshot -Frame_Attempt_Accumulator -``` - -不得新增: - -- `x_axis/y_axis` 兼容接口。 -- 旧 Refresh Strategy 适配器。 -- 旧 Curve/Waterfall worker compatibility layer。 -- Performance 到主 Plot 的刷新转发器。 - ---- - -## 17. 构建与执行 - -在 `main.cmake` 中增加: - -```cmake -enable_testing() -add_executable(Renderive_Core_Tests ${renderive_core_test_sources}) -target_link_libraries(Renderive_Core_Tests PRIVATE Renderive_Core GTest::gtest GTest::gtest_main) -add_test(NAME Renderive_Core_Tests COMMAND Renderive_Core_Tests) -``` - -使用项目指定环境构建: - -```text -"C:\Program Files\JetBrains\CLion 2026.1\bin\cmake\win\x64\bin\cmake.exe" --build D:\ae\proj\Radio\cmake-build-vs2019_debug --target Renderive_Core Renderive_Core_Tests -j 30 -``` - -执行: - -```text -ctest --test-dir D:\ae\proj\Radio\cmake-build-vs2019_debug -R Renderive_Core_Tests --output-on-failure -``` - -另增加: - -- MSVC ASan 构建和完整测试。 -- 可用 Clang 环境中的 TSan 并发测试。 -- Release 下 60 秒持续压力测试。 - ---- - -## 18. 最终验收标准 - -只有全部满足才算完成: - -- 三个物理帧只通过 Rendering/Middle/Painting 角色循环。 -- 所有角色操作纯 try,无等待和自旋。 -- 帧完整记录 12、23、32、21 以及 Render/Paint 时间。 -- 限制来源只有 Render、Paint、User、Unknown。 -- 实际控制周期来自三者最大周期,记录明确限制来源。 -- Render 限制使用渲染完成后的快速追帧。 -- Paint 限制使用 Paint 完成后的快速事件驱动。 -- 只有 User 限制使用定时器。 -- 手动刷新独立,不伪造 input version。 -- Curve 和 Waterfall 只有帧内并行,没有独立跨帧发布。 -- Waterfall 使用二维 Tile。 -- Performance 在 3→2 无锁采样,独立后台生成 Image Snapshot。 -- Performance 不参与被测 Plot 的 Renderable Tree,不标脏主 Plot。 -- 所有 Core Tests、ASan 和压力测试通过。 -- 旧实现和兼容层全部删除。 - ---- - -## 19. 给 Codex 5.5 的执行要求 - -将本文件作为唯一设计依据执行。 - -执行时必须遵守: - -1. 不修改本文件定义的核心语义。 -2. 不重新引入跨帧 Curve/Waterfall worker。 -3. 不重新引入任何三缓冲等待接口。 -4. 不增加兼容转发。 -5. 不同时保留旧新两套 Refresh Strategy。 -6. 每新增一个成员,都在最终报告中列出唯一写入者、唯一读取者、生命周期和为什么不能计算得到。 -7. 不 reset、checkout 或覆盖当前工作树中的无关修改。 -8. 修改完成后统一构建、统一运行测试、统一提交验收报告。 -9. 若某项无法完成,保留编译通过的单一实现并明确报告,不得留下空接口、TODO 或假实现。 diff --git a/Renderive_最新验收_文本_Performance_Paint崩溃修复计划.md b/Renderive_最新验收_文本_Performance_Paint崩溃修复计划.md new file mode 100644 index 0000000..33e786c --- /dev/null +++ b/Renderive_最新验收_文本_Performance_Paint崩溃修复计划.md @@ -0,0 +1,763 @@ +# 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/main.cmake b/main.cmake index 25b6fc3..81f8ee9 100644 --- a/main.cmake +++ b/main.cmake @@ -1,5 +1,5 @@ include_guard(GLOBAL) -set(renderive_dependencies global::GTest global::blend2d) +set(renderive_dependencies global::GTest global::blend2d global::taskflow global::mpmcqueue) rcl_add_dependency_action_targets(renderive_env ${renderive_dependencies}) library_is_installed_with_rely(renderive_dependencies_installed ${renderive_dependencies}) if (NOT renderive_dependencies_installed) @@ -16,9 +16,13 @@ else () ) find_package(blend2d REQUIRED) find_package(GTest REQUIRED) + find_package(Taskflow CONFIG REQUIRED) + find_package(MPMCQueue CONFIG REQUIRED) set(renderive_public_libraries blend2d::blend2d GTest::GTest + Taskflow::Taskflow + MPMCQueue::MPMCQueue Psc_Core_Static ) set(qt_libraries diff --git a/test/Renderive_Core_Tests.cpp b/test/Renderive_Core_Tests.cpp index acb6e36..85cf430 100644 --- a/test/Renderive_Core_Tests.cpp +++ b/test/Renderive_Core_Tests.cpp @@ -3,9 +3,16 @@ #include "Core/plot/Latency_Eager_Refresh_Strategy.h" #include "Core/plot/Plot_Frame_Pipeline.h" #include "Core/plot/Plot_Core.h" +#include "Core/execution/Render_Executor.h" +#include "Core/plottable/Performance_Shower_p.h" #include "Core/plottable/export.h" +#include "Core/render/Image.h" #include +#include +#include #include +#include +#include namespace renderive { namespace { @@ -28,8 +35,99 @@ Frame_Lifecycle_Record render_frame(std::uint64_t begin_ns, std::uint64_t end_ns return frame; } +bool wait_until(std::function predicate) { + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (std::chrono::steady_clock::now() < deadline) { + if (predicate()) + return true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return predicate(); +} + +std::shared_ptr performance_frame(std::uint64_t frame_id, int width = 240, int height = 80) { + auto frame = std::make_shared(); + frame->frame_id = frame_id; + frame->input_version = frame_id + 1; + frame->render_begin_ns = 10; + frame->prepare_begin_ns = 11; + frame->prepare_end_ns = 12; + frame->draw_begin_ns = 13; + frame->draw_end_ns = 14; + frame->render_end_ns = 20; + frame->transition_12_ns = 25; + frame->paint_begin_ns = 30; + frame->paint_end_ns = 40; + frame->transition_32_ns = 45; + frame->pixel_width = static_cast(width); + frame->pixel_height = static_cast(height); + frame->viewport_version = 1; + frame->outcome = Frame_Outcome::Presented; + frame->worker.executor_at_finish.worker_count = 4; + frame->worker.executor_at_finish.active_workers = 1; + frame->worker.executor_at_finish.completed_tasks = frame_id; + return frame; +} + +bool valid_utf8(const std::string& text) { + for (std::size_t i = 0; i < text.size();) { + unsigned char c = static_cast(text[i]); + std::size_t n = 0; + if ((c & 0x80u) == 0) + n = 1; + else if ((c & 0xE0u) == 0xC0u) + n = 2; + else if ((c & 0xF0u) == 0xE0u) + n = 3; + else if ((c & 0xF8u) == 0xF0u) + n = 4; + else + return false; + if (i + n > text.size()) + return false; + for (std::size_t j = 1; j < n; ++j) { + if ((static_cast(text[i + j]) & 0xC0u) != 0x80u) + return false; + } + i += n; + } + return true; +} + } // namespace +TEST(Renderive_Canvas, TextUsesPenAndFillUsesBrush) { + auto diagnostics = Canvas::default_font_diagnostics(); + ASSERT_TRUE(diagnostics.face_valid); + ASSERT_TRUE(diagnostics.font_valid); + ASSERT_GT(diagnostics.test_width, 0.0); + ASSERT_GT(diagnostics.test_height, 0.0); + + Image image(400, 100); + image.fill(Color::transparent()); + { + Canvas canvas(image); + canvas.set_brush(Brush{Color::white(), Brush_Style::Solid}); + canvas.set_pen(Pen{Color::black()}); + canvas.fill_rect(RectF{0.0, 0.0, 400.0, 100.0}); + canvas.draw_text(PointF{10.0, 10.0}, "0123456789 ABC \xE4\xB8\xAD\xE6\x96\x87"); + canvas.draw_text(RectF{10.0, 50.0, 300.0, 30.0}, Text_Align::Left | Text_Align::Top, "0123"); + } + + EXPECT_EQ(premultiplied_to_color(image.row(2)[2]), Color::white()); + int dark_pixels = 0; + for (int y = 8; y < 82; ++y) { + const Pixel* row = image.row(y); + ASSERT_NE(row, nullptr); + for (int x = 8; x < 320; ++x) { + Color color = premultiplied_to_color(row[x]); + if (color.alpha && color.red < 120 && color.green < 120 && color.blue < 120) + ++dark_pixels; + } + } + EXPECT_GT(dark_pixels, 20); +} + TEST(Renderive_Triple_Buffer, RoleSwapsAreTryOnlyAndDeterministic) { Triple_Role_Buffer_Control control; control.reset(); @@ -41,7 +139,7 @@ TEST(Renderive_Triple_Buffer, RoleSwapsAreTryOnlyAndDeterministic) { EXPECT_EQ(first.result, Triple_Buffer_Result::Success); EXPECT_TRUE(unique_roles(control.read_view())); - auto painting = control.try_mark_use_role(Frame_Painting); + auto painting = control.try_acquire_role(Frame_Painting); ASSERT_EQ(painting.result, Triple_Buffer_Result::Success); auto blocked = control.try_swap_role(Frame_Middle, Frame_Painting, [](const Triple_Buffer_View&) { return true; @@ -66,9 +164,10 @@ TEST(Renderive_Frame_Pipeline, PublishAndPaintUseMiddleVersionOnly) { Plot_Frame_Pipeline pipeline; auto lease = pipeline.try_begin_render(); ASSERT_TRUE(static_cast(lease)); - lease.frame->metadata.frame_id = 1; - lease.frame->metadata.render_begin_ns = 10; - lease.frame->metadata.render_end_ns = 20; + ASSERT_TRUE(lease.frame->metadata); + lease.frame->metadata->frame_id = 1; + lease.frame->metadata->render_begin_ns = 10; + lease.frame->metadata->render_end_ns = 20; lease.frame->version.store(1, std::memory_order_release); lease.reset(); @@ -83,6 +182,48 @@ TEST(Renderive_Frame_Pipeline, PublishAndPaintUseMiddleVersionOnly) { EXPECT_FALSE(pipeline.middle_has_new_frame()); } +TEST(Renderive_Frame_Pipeline, PaintAcquireFailureKeepsPendingRequest) { + Plot_Frame_Pipeline pipeline; + auto lease = pipeline.try_begin_render(); + ASSERT_TRUE(static_cast(lease)); + ASSERT_TRUE(lease.frame->metadata); + lease.frame->metadata->frame_id = 1; + lease.frame->metadata->render_begin_ns = 10; + lease.frame->metadata->render_end_ns = 20; + lease.frame->version.store(1, std::memory_order_release); + lease.reset(); + + auto published = pipeline.try_publish_rendered(25); + ASSERT_TRUE(published.request_present); + EXPECT_TRUE(pipeline.paint_request_pending.load(std::memory_order_acquire)); + + auto busy = pipeline.frame_control.try_acquire_role(Frame_Painting); + ASSERT_EQ(busy.result, Triple_Buffer_Result::Success); + auto failed = pipeline.try_begin_paint(30); + EXPECT_TRUE(failed.paint_acquire_dropped); + EXPECT_FALSE(failed.update_was_pending); + EXPECT_TRUE(pipeline.paint_request_pending.load(std::memory_order_acquire)); + pipeline.frame_control.unmark_use(busy.lease); + + auto consumed = pipeline.try_begin_paint(35); + EXPECT_TRUE(consumed.update_was_pending); + EXPECT_TRUE(static_cast(consumed.lease)); +} + +TEST(Renderive_MPMCQueue, UsesTryOnlyBoundedCapacity) { + rigtorp::MPMCQueue queue(2); + EXPECT_TRUE(queue.try_push(1)); + EXPECT_TRUE(queue.try_push(2)); + EXPECT_FALSE(queue.try_push(3)); + + int value = 0; + EXPECT_TRUE(queue.try_pop(value)); + EXPECT_EQ(value, 1); + EXPECT_TRUE(queue.try_pop(value)); + EXPECT_EQ(value, 2); + EXPECT_FALSE(queue.try_pop(value)); +} + TEST(Renderive_Frame_Feedback, SelectsRenderPaintAndUserSources) { Frame_Feedback_Controller controller; controller.set_max_render_fps(200.0); @@ -116,13 +257,199 @@ TEST(Renderive_Frame_Feedback, SelectsRenderPaintAndUserSources) { EXPECT_EQ(controller.state().source, Refresh_Limit_Source::User); } +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}; + auto first_stat = std::make_shared(); + ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ + 1, + 1, + Render_Task_Kind::Frame, + Task([&]() { + entered.store(true, std::memory_order_release); + while (!release.load(std::memory_order_acquire)) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }), + Task([&]() { + completed.store(true, std::memory_order_release); + }), + first_stat, + 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{ + 2, + 1, + Render_Task_Kind::Frame, + Task([]() {}), + Task([]() {}), + second_stat, + true + })); + release.store(true, std::memory_order_release); + ASSERT_TRUE(wait_until([&]() { + return completed.load(std::memory_order_acquire); + })); + executor.shutdown(); +} + TEST(Renderive_Performance_Shower, ToggleDoesNotReenterRenderableTreeUnsafely) { Plot_Core plot(std::make_unique()); plot.init(); + auto root = plot.root_renderable(); + ASSERT_TRUE(root); + EXPECT_TRUE(root->children_snapshot().empty()); set_performance_shower_enabled(plot, true); EXPECT_TRUE(performance_shower_enabled(plot)); + EXPECT_TRUE(root->children_snapshot().empty()); set_performance_shower_enabled(plot, false); EXPECT_FALSE(performance_shower_enabled(plot)); + EXPECT_TRUE(root->children_snapshot().empty()); +} + +TEST(Renderive_Performance_Shower, WorkerConsumesSharedFrameRecord) { + auto state = std::make_shared(); + auto frame = std::make_shared(); + frame->frame_id = 7; + frame->input_version = 3; + frame->render_begin_ns = 10; + frame->render_end_ns = 20; + frame->transition_12_ns = 25; + frame->paint_begin_ns = 30; + frame->paint_end_ns = 40; + frame->transition_32_ns = 45; + frame->pixel_width = 320; + frame->pixel_height = 240; + frame->outcome = Frame_Outcome::Presented; + Performance_Frame_Record record = frame; + ASSERT_TRUE(state->metric_queue.try_push(record)); + + schedule_performance_metrics_worker(state); + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + std::shared_ptr snapshot; + while (std::chrono::steady_clock::now() < deadline) { + snapshot = state->display_snapshot_slot.load(std::memory_order_acquire); + if (snapshot && !state->worker_active.load(std::memory_order_acquire)) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + state->cancelled.store(true, std::memory_order_release); + + ASSERT_TRUE(snapshot); + EXPECT_FALSE(snapshot->lines.empty()); + EXPECT_TRUE(state->aggregate.has_frame); + EXPECT_EQ(state->aggregate.latest.frame_id, 7); +} + +TEST(Renderive_Performance_Shower, NumericFieldWidthIsMonotonicAcrossResetAndResize) { + auto state = std::make_shared(); + auto frame_width = [&]() { + return state->field_layout_states[static_cast(Performance_Field_Id::Frame_Id)].max_value_width; + }; + double previous = 0.0; + double wide = 0.0; + for (std::uint64_t frame_id : {1ull, 99ull, 123456ull, 2ull, 8ull}) { + Performance_Frame_Record record = performance_frame(frame_id, 320, 120); + ASSERT_TRUE(state->metric_queue.try_push(record)); + schedule_performance_metrics_worker(state); + ASSERT_TRUE(wait_until([&]() { + auto snapshot = state->display_snapshot_slot.load(std::memory_order_acquire); + return snapshot && state->aggregate.latest.frame_id == frame_id && !state->worker_active.load(std::memory_order_acquire); + })); + double current = frame_width(); + EXPECT_GE(current, previous); + previous = current; + if (frame_id == 123456) + wide = current; + if (frame_id == 2 || frame_id == 8) + EXPECT_EQ(current, wide); + } + + state->reset_requested.store(true, std::memory_order_release); + schedule_performance_metrics_worker(state); + ASSERT_TRUE(wait_until([&]() { + return !state->worker_active.load(std::memory_order_acquire); + })); + EXPECT_EQ(frame_width(), wide); + + state->viewport_width.store(180, std::memory_order_release); + state->viewport_height.store(90, std::memory_order_release); + state->viewport_version.store(2, std::memory_order_release); + state->snapshot_rebuild_requested.store(true, std::memory_order_release); + schedule_performance_metrics_worker(state); + ASSERT_TRUE(wait_until([&]() { + auto snapshot = state->display_snapshot_slot.load(std::memory_order_acquire); + return snapshot && snapshot->viewport_version == 2 && !state->worker_active.load(std::memory_order_acquire); + })); + EXPECT_EQ(frame_width(), wide); + state->cancelled.store(true, std::memory_order_release); +} + +TEST(Renderive_Performance_Shower, ScrollCommandsUpdateOffsetAndClamp) { + Performance_Shower shower; + shower.setEnabled(true); + shower.set_viewport(Size{220, 70}, 1); + auto frame = performance_frame(1, 220, 70); + shower.collect_frame(frame); + ASSERT_TRUE(wait_until([&]() { + auto snapshot = shower.display_snapshot(); + return snapshot && snapshot->max_scroll_offset > 0.0; + })); + + auto snapshot = shower.display_snapshot(); + ASSERT_TRUE(snapshot); + EXPECT_TRUE(shower.handle_wheel(PointF{5.0, 5.0}, -120.0, 0.0)); + ASSERT_TRUE(wait_until([&]() { + auto next = shower.display_snapshot(); + return next && next->scroll_offset > snapshot->scroll_offset; + })); + + snapshot = shower.display_snapshot(); + ASSERT_TRUE(snapshot); + PointF page_down{snapshot->scroll_track_rect.x + 1.0, snapshot->scroll_thumb_rect.bottom() + 1.0}; + EXPECT_TRUE(shower.handle_pointer_press(page_down)); + ASSERT_TRUE(wait_until([&]() { + auto next = shower.display_snapshot(); + return next && next->scroll_offset > snapshot->scroll_offset; + })); + + snapshot = shower.display_snapshot(); + ASSERT_TRUE(snapshot); + PointF thumb_center{snapshot->scroll_thumb_rect.x + 1.0, snapshot->scroll_thumb_rect.y + snapshot->scroll_thumb_rect.height * 0.5}; + EXPECT_TRUE(shower.handle_pointer_press(thumb_center)); + EXPECT_TRUE(shower.handle_pointer_move(PointF{thumb_center.x, snapshot->scroll_track_rect.bottom()})); + EXPECT_TRUE(shower.handle_pointer_release(PointF{thumb_center.x, snapshot->scroll_track_rect.bottom()})); + ASSERT_TRUE(wait_until([&]() { + auto next = shower.display_snapshot(); + return next && next->max_scroll_offset > 0.0 && next->scroll_offset > next->max_scroll_offset * 0.8; + })); +} + +TEST(Renderive_Performance_Shower, Utf8WrappingKeepsValidText) { + Performance_Shower shower; + shower.setEnabled(true); + shower.set_viewport(Size{120, 90}, 1); + auto frame = performance_frame(1, 120, 90); + Renderable_Frame_Stat stat; + stat.object_name = "\xE4\xB8\xAD\xE6\x96\x87\xE6\xB5\x8B\xE8\xAF\x95\xE5\xAD\x97\xE6\xAE\xB5\xE5\xBE\x88\xE9\x95\xBF\xE5\xBE\x88\xE9\x95\xBF"; + stat.tree_depth = 0; + stat.render_count = 1; + stat.total_render_time_ns = 1000000; + frame->renderable_stats.push_back(stat); + shower.collect_frame(frame); + ASSERT_TRUE(wait_until([&]() { + auto snapshot = shower.display_snapshot(); + return snapshot && !snapshot->lines.empty(); + })); + auto snapshot = shower.display_snapshot(); + ASSERT_TRUE(snapshot); + for (const auto& line : snapshot->lines) + EXPECT_TRUE(valid_utf8(line)) << line; } } // namespace renderive