#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../architecture/Frame_Scheduler.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 "../render/Canvas.h" #include "Performance_Overlay.h" #include "Psc_Cpp_Core/Statistics/Statistics.h" namespace renderive { void write_frame_performance_log(std::shared_ptr frame); struct Performance_Overlay_Render_State : Render_State, Performance_Overlay_Options {}; struct Performance_Overlay_Input_Data : Input_Data {}; struct Renderable_Cache_Aggregate_Stat { std::string object_name; std::string parent_object_name; int tree_depth{}; std::uint64_t hit_count{}; std::uint64_t miss_count{}; std::uint64_t rebuild_count{}; std::uint64_t rebuild_time_ns{}; std::uint64_t compose_time_ns{}; std::uint64_t subtree_compose_time_ns{}; std::uint64_t last_rebuild_time_ns{}; std::uint64_t last_compose_time_ns{}; std::uint64_t last_image_bytes{}; std::uint64_t last_image_area{}; std::uint64_t last_edit_version{}; std::uint64_t last_ready_version{}; Psc::Value_Statistics rebuild_ms; Psc::Value_Statistics compose_ms; Psc::Value_Statistics subtree_compose_ms; }; struct Renderable_Aggregate_Stat { std::string object_name; std::string parent_object_name; int tree_depth{}; bool cache_enabled{}; std::uint64_t render_count{}; std::uint64_t self_draw_count{}; std::uint64_t total_render_time_ns{}; std::uint64_t self_draw_time_ns{}; std::uint64_t last_total_render_time_ns{}; std::uint64_t last_self_draw_time_ns{}; Psc::Value_Statistics total_render_ms; Psc::Value_Statistics self_draw_ms; Psc::Value_Statistics total_render_ratio; Psc::Value_Statistics self_draw_ratio; }; struct Performance_Aggregate { Psc::Value_Statistics request_wait_ms; Psc::Value_Statistics render_queue_wait_ms; Psc::Value_Statistics prepare_ms; Psc::Value_Statistics draw_ms; Psc::Value_Statistics render_ms; Psc::Value_Statistics ready_wait_ms; Psc::Value_Statistics update_wait_ms; Psc::Value_Statistics paint_ms; Psc::Value_Statistics frame_age_ms; Psc::Value_Statistics frame_total_ms; Psc::Value_Statistics cache_rebuild_ms; Psc::Value_Statistics cache_compose_ms; Psc::Value_Statistics cache_rebuild_ratio; Psc::Value_Statistics cache_compose_ratio; Frame_Lifecycle_Record latest; std::uint64_t observed_frame_count{}; std::uint64_t render_time_ns{}; std::uint64_t consumed_outcome_count{}; std::uint64_t superseded_outcome_count{}; std::uint64_t cancelled_outcome_count{}; std::uint64_t cache_hit_count{}; std::uint64_t cache_miss_count{}; std::uint64_t cache_rebuild_count{}; std::uint64_t cache_rebuild_time_ns{}; std::uint64_t cache_compose_time_ns{}; std::uint64_t input_capacity_dropped{}; bool has_frame{}; std::vector renderable_stats; std::vector renderable_cache_stats; void clear() { *this = {}; } static Frame_Lifecycle_Record summarize_frame(const Frame_Lifecycle_Record& frame) { Frame_Lifecycle_Record summary; summary.frame_id = frame.frame_id; summary.input_version = frame.input_version; summary.core_data_time_ns = frame.core_data_time_ns; summary.render_request_ns = frame.render_request_ns; summary.render_role_enter_ns = frame.render_role_enter_ns; summary.render_begin_ns = frame.render_begin_ns; summary.prepare_begin_ns = frame.prepare_begin_ns; summary.prepare_end_ns = frame.prepare_end_ns; summary.draw_begin_ns = frame.draw_begin_ns; summary.draw_end_ns = frame.draw_end_ns; summary.render_end_ns = frame.render_end_ns; summary.transition_12_ns = frame.transition_12_ns; summary.update_request_id = frame.update_request_id; summary.update_request_time_ns = frame.update_request_time_ns; summary.transition_23_ns = frame.transition_23_ns; summary.paint_begin_ns = frame.paint_begin_ns; summary.paint_end_ns = frame.paint_end_ns; summary.transition_32_ns = frame.transition_32_ns; summary.render_queue_wait_ns = frame.render_queue_wait_ns; summary.ready_queue_wait_ns = frame.ready_queue_wait_ns; summary.present_queue_wait_ns = frame.present_queue_wait_ns; summary.paint_return_wait_ns = frame.paint_return_wait_ns; summary.superseded_time_ns = frame.superseded_time_ns; summary.outcome = frame.outcome; summary.refresh = frame.refresh; summary.worker = frame.worker; summary.raw_input_count = frame.raw_input_count; summary.prepared_output_count = frame.prepared_output_count; summary.renderable_cache_hit_count = frame.renderable_cache_hit_count; summary.renderable_cache_miss_count = frame.renderable_cache_miss_count; summary.renderable_cache_rebuild_count = frame.renderable_cache_rebuild_count; summary.renderable_cache_rebuild_time_ns = frame.renderable_cache_rebuild_time_ns; summary.renderable_cache_compose_time_ns = frame.renderable_cache_compose_time_ns; summary.renderable_cache_image_bytes = frame.renderable_cache_image_bytes; summary.renderable_cache_image_area = frame.renderable_cache_image_area; summary.pixel_width = frame.pixel_width; summary.pixel_height = frame.pixel_height; summary.viewport_version = frame.viewport_version; return summary; } void update(const Frame_Lifecycle_Record& frame) { latest = summarize_frame(frame); has_frame = true; ++observed_frame_count; std::uint64_t frame_render_time_ns = duration_ns(frame.render_begin_ns, frame.render_end_ns); render_time_ns += frame_render_time_ns; update_duration(request_wait_ms, frame.render_request_ns, frame.render_begin_ns); update_duration(render_queue_wait_ms, frame.render_role_enter_ns, frame.render_begin_ns); update_duration(prepare_ms, frame.prepare_begin_ns, frame.prepare_end_ns); update_duration(draw_ms, frame.draw_begin_ns, frame.draw_end_ns); update_duration(render_ms, frame.render_begin_ns, frame.render_end_ns); update_duration(ready_wait_ms, frame.transition_12_ns, frame.paint_begin_ns ? frame.paint_begin_ns : frame.superseded_time_ns); update_duration(update_wait_ms, frame.update_request_time_ns, frame.paint_begin_ns); update_duration(paint_ms, frame.paint_begin_ns, frame.paint_end_ns); update_duration(frame_age_ms, frame.core_data_time_ns, frame.paint_end_ns); update_duration(frame_total_ms, frame.render_request_ns, frame.transition_32_ns ? frame.transition_32_ns : frame.superseded_time_ns); cache_hit_count += frame.renderable_cache_hit_count; cache_miss_count += frame.renderable_cache_miss_count; cache_rebuild_count += frame.renderable_cache_rebuild_count; cache_rebuild_time_ns += frame.renderable_cache_rebuild_time_ns; cache_compose_time_ns += frame.renderable_cache_compose_time_ns; if (frame.renderable_cache_rebuild_time_ns) cache_rebuild_ms.update(ns_to_ms(frame.renderable_cache_rebuild_time_ns)); if (frame.renderable_cache_compose_time_ns) cache_compose_ms.update(ns_to_ms(frame.renderable_cache_compose_time_ns)); if (frame_render_time_ns) { cache_rebuild_ratio.update(static_cast(frame.renderable_cache_rebuild_time_ns) * 100.0 / static_cast(frame_render_time_ns)); cache_compose_ratio.update(static_cast(frame.renderable_cache_compose_time_ns) * 100.0 / static_cast(frame_render_time_ns)); } for (const auto& stat : frame.renderable_stats) { Renderable_Aggregate_Stat& aggregate_stat = renderable_stat(stat.object_name, stat.parent_object_name, stat.tree_depth); aggregate_stat.cache_enabled = stat.cache_enabled; aggregate_stat.render_count += stat.render_count; aggregate_stat.self_draw_count += stat.self_draw_count; aggregate_stat.total_render_time_ns += stat.total_render_time_ns; aggregate_stat.self_draw_time_ns += stat.self_draw_time_ns; aggregate_stat.last_total_render_time_ns = stat.last_total_render_time_ns ? stat.last_total_render_time_ns : aggregate_stat.last_total_render_time_ns; aggregate_stat.last_self_draw_time_ns = stat.last_self_draw_time_ns ? stat.last_self_draw_time_ns : aggregate_stat.last_self_draw_time_ns; if (stat.total_render_time_ns) aggregate_stat.total_render_ms.update(ns_to_ms(stat.total_render_time_ns)); if (stat.self_draw_time_ns) aggregate_stat.self_draw_ms.update(ns_to_ms(stat.self_draw_time_ns)); if (frame_render_time_ns) { aggregate_stat.total_render_ratio.update(static_cast(stat.total_render_time_ns) * 100.0 / static_cast(frame_render_time_ns)); aggregate_stat.self_draw_ratio.update(static_cast(stat.self_draw_time_ns) * 100.0 / static_cast(frame_render_time_ns)); } } for (const auto& stat : frame.renderable_cache_stats) { Renderable_Cache_Aggregate_Stat& aggregate_stat = cache_stat(stat.object_name, stat.parent_object_name, stat.tree_depth); aggregate_stat.hit_count += stat.hit_count; aggregate_stat.miss_count += stat.miss_count; aggregate_stat.rebuild_count += stat.rebuild_count; aggregate_stat.rebuild_time_ns += stat.rebuild_time_ns; aggregate_stat.compose_time_ns += stat.compose_time_ns; aggregate_stat.last_rebuild_time_ns = stat.last_rebuild_time_ns ? stat.last_rebuild_time_ns : aggregate_stat.last_rebuild_time_ns; aggregate_stat.last_compose_time_ns = stat.last_compose_time_ns ? stat.last_compose_time_ns : aggregate_stat.last_compose_time_ns; aggregate_stat.last_image_bytes = stat.last_image_bytes ? stat.last_image_bytes : aggregate_stat.last_image_bytes; aggregate_stat.last_image_area = stat.last_image_area ? stat.last_image_area : aggregate_stat.last_image_area; aggregate_stat.last_edit_version = stat.last_edit_version ? stat.last_edit_version : aggregate_stat.last_edit_version; aggregate_stat.last_ready_version = stat.last_ready_version ? stat.last_ready_version : aggregate_stat.last_ready_version; if (stat.rebuild_time_ns) aggregate_stat.rebuild_ms.update(ns_to_ms(stat.rebuild_time_ns)); if (stat.compose_time_ns) aggregate_stat.compose_ms.update(ns_to_ms(stat.compose_time_ns)); } update_cache_subtree_compose(frame.renderable_cache_stats); switch (frame.outcome) { case Frame_Outcome::Presented: ++consumed_outcome_count; break; case Frame_Outcome::Superseded: ++superseded_outcome_count; break; case Frame_Outcome::Render_Acquire_Dropped: case Frame_Outcome::Publish_Dropped: case Frame_Outcome::Paint_Acquire_Dropped: case Frame_Outcome::Worker_Budget_Dropped: case Frame_Outcome::Cancelled: ++cancelled_outcome_count; break; } } private: Renderable_Aggregate_Stat& renderable_stat(const std::string& object_name, const std::string& parent_object_name, int tree_depth) { for (auto& stat : renderable_stats) { if (stat.object_name == object_name && stat.parent_object_name == parent_object_name && stat.tree_depth == tree_depth) return stat; } Renderable_Aggregate_Stat stat; stat.object_name = object_name; stat.parent_object_name = parent_object_name; stat.tree_depth = tree_depth; renderable_stats.push_back(std::move(stat)); return renderable_stats.back(); } void update_cache_subtree_compose(const std::vector& frame_stats) { for (const auto& stat : frame_stats) { std::uint64_t subtree_compose_time_ns = renderable_cache_subtree_compose_time(frame_stats, stat); if (!subtree_compose_time_ns) continue; Renderable_Cache_Aggregate_Stat& aggregate_stat = cache_stat(stat.object_name, stat.parent_object_name, stat.tree_depth); aggregate_stat.subtree_compose_time_ns += subtree_compose_time_ns; aggregate_stat.subtree_compose_ms.update(ns_to_ms(subtree_compose_time_ns)); } } Renderable_Cache_Aggregate_Stat& cache_stat(const std::string& object_name, const std::string& parent_object_name, int tree_depth) { for (auto& stat : renderable_cache_stats) { if (stat.object_name == object_name && stat.parent_object_name == parent_object_name) return stat; } Renderable_Cache_Aggregate_Stat stat; stat.object_name = object_name; stat.parent_object_name = parent_object_name; stat.tree_depth = tree_depth; renderable_cache_stats.push_back(std::move(stat)); return renderable_cache_stats.back(); } static double ns_to_ms(std::uint64_t ns) { return static_cast(ns) / 1000000.0; } static std::uint64_t duration_ns(std::uint64_t begin_ns, std::uint64_t end_ns) { return end_ns > begin_ns ? end_ns - begin_ns : 0; } static void update_duration(Psc::Value_Statistics& statistics, std::uint64_t begin_ns, std::uint64_t end_ns) { if (end_ns > begin_ns) statistics.update(static_cast(end_ns - begin_ns) / 1000000.0); } }; enum class Performance_Metric_Id : std::uint16_t { Frame_Id, Input_Version, Render_Period, Paint_Period, User_Period, Target_Period, Render_Queue_Wait, Ready_Queue_Wait, Present_Queue_Wait, Paint_Return_Wait, Worker_Count, Active_Workers, Queued_Frame_Jobs, Active_Frame_Jobs, Submitted_Jobs, Started_Task_Nodes, Completed_Task_Nodes, Rejected_Frame_Jobs, Queue_Wait_Avg, Queue_Wait_Max, Task_Run_Avg, Task_Run_Max, Frame_Task_Count, Frame_Peak_Parallelism, Frame_Wait_Max, Frame_Run_Total, Outcome, 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_Metric_Id::Count); struct Performance_Field_Layout_State { double max_value_width{}; }; struct Performance_Text_Span { bool value{}; Performance_Metric_Id field_id = Performance_Metric_Id::Generic_Number; std::string text; double measured_width{}; double reserved_width{}; }; struct Performance_Line_Layout { std::vector runs; double width{}; }; using Performance_Frame_Record = std::shared_ptr; struct Performance_Worker_State { static constexpr std::size_t Capacity = 256; Performance_Worker_State() : metric_queue(Capacity) {} rigtorp::MPMCQueue metric_queue; std::atomic dropped_frame_record_count{0}; std::atomic_bool enabled{false}; std::atomic_bool worker_active{false}; std::atomic_bool work_requested{false}; std::atomic_bool cancelled{false}; std::atomic_bool reset_requested{false}; 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}; Performance_Aggregate aggregate; 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 maximum_scroll_offset{}; double maximum_content_width{}; mutable std::mutex options_mutex; Performance_Overlay_Options options; std::mutex update_callback_mutex; std::function update_callback; std::atomic> published_snapshot; }; 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_Layout_Builder { Performance_Aggregate aggregate; std::vector lines; std::vector render_lines; std::array field_layout_states{}; RectF panel_rect; RectF scroll_bar_rect; RectF scroll_thumb_rect; double content_width{}; static std::string value(double number, int precision = 2) { return format_number(number, {.precision = precision}); } double panel_height(double line_height, Performance_Overlay_Render_State* state, int plot_height) const { return std::min(content_height(line_height, state), static_cast(std::max(0, plot_height))); } double content_height(double line_height, Performance_Overlay_Render_State* state) const { return line_height * static_cast(render_lines.size()) + state->top_margin + state->bottom_margin; } double content_viewport_height(Performance_Overlay_Render_State* state, double height) const { return std::max(0.0, height - state->top_margin - state->bottom_margin); } void update_scroll_bar_rects(Performance_Overlay_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; } 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); 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}; } 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_Metric_Id field_id_from_key(std::string_view key) { if (key == "frame_id") return Performance_Metric_Id::Frame_Id; if (key == "input_version") return Performance_Metric_Id::Input_Version; if (key == "render_period") return Performance_Metric_Id::Render_Period; if (key == "paint_period") return Performance_Metric_Id::Paint_Period; if (key == "user_period") return Performance_Metric_Id::User_Period; if (key == "target_period") return Performance_Metric_Id::Target_Period; if (key == "render_queue_wait") return Performance_Metric_Id::Render_Queue_Wait; if (key == "ready_queue_wait") return Performance_Metric_Id::Ready_Queue_Wait; if (key == "present_queue_wait") return Performance_Metric_Id::Present_Queue_Wait; if (key == "paint_return_wait") return Performance_Metric_Id::Paint_Return_Wait; if (key == "taskflow_workers") return Performance_Metric_Id::Worker_Count; if (key == "taskflow_active") return Performance_Metric_Id::Active_Workers; if (key == "taskflow_queued_frames") return Performance_Metric_Id::Queued_Frame_Jobs; if (key == "taskflow_active_frames") return Performance_Metric_Id::Active_Frame_Jobs; if (key == "taskflow_submitted") return Performance_Metric_Id::Submitted_Jobs; if (key == "taskflow_started") return Performance_Metric_Id::Started_Task_Nodes; if (key == "taskflow_completed") return Performance_Metric_Id::Completed_Task_Nodes; if (key == "taskflow_rejected") return Performance_Metric_Id::Rejected_Frame_Jobs; if (key == "taskflow_queue_wait_avg") return Performance_Metric_Id::Queue_Wait_Avg; if (key == "taskflow_queue_wait_p95") return Performance_Metric_Id::Queue_Wait_Max; if (key == "taskflow_run_avg") return Performance_Metric_Id::Task_Run_Avg; if (key == "taskflow_run_p95") return Performance_Metric_Id::Task_Run_Max; if (key == "frame_worker_tasks") return Performance_Metric_Id::Frame_Task_Count; if (key == "frame_worker_peak") return Performance_Metric_Id::Frame_Peak_Parallelism; if (key == "frame_worker_wait_max") return Performance_Metric_Id::Frame_Wait_Max; if (key == "frame_worker_run_total") return Performance_Metric_Id::Frame_Run_Total; if (key == "frame_outcome") return Performance_Metric_Id::Outcome; if (key == "cache_hit_count") return Performance_Metric_Id::Cache_Hit; if (key == "cache_miss_count") return Performance_Metric_Id::Cache_Miss; if (key == "cache_hit_ratio") return Performance_Metric_Id::Cache_Ratio; if (key == "cache_rebuild_count") return Performance_Metric_Id::Cache_Rebuild_Count; if (key == "cache_rebuild_time") return Performance_Metric_Id::Cache_Rebuild_Time; if (key == "cache_compose_time") return Performance_Metric_Id::Cache_Compose_Time; if (key == "raw_input_count") return Performance_Metric_Id::Raw_Input_Count; if (key == "prepared_output_count") return Performance_Metric_Id::Prepared_Output_Count; if (key == "observed_frame_count") return Performance_Metric_Id::Observed_Frame_Count; if (key == "consumed_outcome_count") return Performance_Metric_Id::Consumed_Frame_Count; if (key == "superseded_outcome_count") return Performance_Metric_Id::Superseded_Frame_Count; if (key == "cancelled_outcome_count") return Performance_Metric_Id::Cancelled_Frame_Count; if (key == "input_capacity_dropped") return Performance_Metric_Id::Dropped_Input_Count; if (ends_with(key, "_cache_hit")) return Performance_Metric_Id::Cache_Hit; if (ends_with(key, "_cache_miss")) return Performance_Metric_Id::Cache_Miss; if (ends_with(key, "_cache_rebuild")) return Performance_Metric_Id::Cache_Rebuild_Count; if (ends_with(key, "_cache_ratio")) return Performance_Metric_Id::Cache_Ratio; if (ends_with(key, "_cache_image_area")) return Performance_Metric_Id::Cache_Image_Area; if (ends_with(key, "_cache_image_bytes")) return Performance_Metric_Id::Cache_Image_Bytes; if (ends_with(key, "_cache_edit_version")) return Performance_Metric_Id::Cache_Edit_Version; if (ends_with(key, "_cache_ready_version")) return Performance_Metric_Id::Cache_Ready_Version; if (contains(key, "ratio")) { if (ends_with(key, "_instant")) return Performance_Metric_Id::Current_Percent; if (ends_with(key, "_smooth")) return Performance_Metric_Id::Smooth_Percent; if (ends_with(key, "_variation")) return Performance_Metric_Id::Variation_Percent; if (ends_with(key, "_p95")) return Performance_Metric_Id::P95_Percent; return Performance_Metric_Id::Current_Percent; } if (ends_with(key, "_instant")) return Performance_Metric_Id::Current_Ms; if (ends_with(key, "_smooth") || ends_with(key, "_srtt")) return Performance_Metric_Id::Smooth_Ms; if (ends_with(key, "_variation") || ends_with(key, "_jitter")) return Performance_Metric_Id::Variation_Ms; if (ends_with(key, "_p95")) return Performance_Metric_Id::P95_Ms; if (ends_with(key, "_fps") || ends_with(key, "_count")) return Performance_Metric_Id::Generic_Count; return Performance_Metric_Id::Generic_Number; } static constexpr std::array outcome_values() { return { Frame_Outcome::Presented, Frame_Outcome::Superseded, Frame_Outcome::Render_Acquire_Dropped, Frame_Outcome::Publish_Dropped, Frame_Outcome::Paint_Acquire_Dropped, Frame_Outcome::Worker_Budget_Dropped, Frame_Outcome::Cancelled }; } void seed_static_field_widths(Canvas& canvas) { for (Frame_Outcome outcome : outcome_values()) { Performance_Text_Span run; run.value = true; run.field_id = Performance_Metric_Id::Outcome; run.text = outcome_text(outcome); measure_run(canvas, run); } } static std::string marked_value(Performance_Metric_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 marked_value(field_id_from_key(key), value(number, precision)); } std::string count_text(const std::string& key, std::uint64_t 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"; } std::string percent_stat_line(const std::string& key, const std::string& name, const Psc::Value_Statistics& statistics) { return name + " current:" + number_text(key + "_instant", statistics.instant) + "% smooth:" + number_text(key + "_smooth", statistics.smooth) + "% var:" + number_text(key + "_variation", statistics.variation) + "% P95:" + number_text(key + "_p95", statistics.p95) + "%"; } std::string interval_line(const std::string& key, const std::string& name, double srtt, double jitter, double fps) { return name + " interval_smooth:" + number_text(key + "_srtt", srtt) + "ms var:" + number_text(key + "_jitter", jitter) + "ms actual:" + number_text(key + "_fps", fps) + "fps"; } static double percent(double value) { return value * 100.0; } std::string percent_text(const std::string& key, double ratio) { return number_text(key, percent(ratio)) + "%"; } static std::string limit_source_text(Refresh_Limit_Source source) { switch (source) { case Refresh_Limit_Source::Unknown: return "unknown"; case Refresh_Limit_Source::Render: return "render"; case Refresh_Limit_Source::Paint: return "paint"; case Refresh_Limit_Source::User: return "user"; } return "unknown"; } static std::string outcome_text(Frame_Outcome outcome) { switch (outcome) { case Frame_Outcome::Presented: return "presented"; case Frame_Outcome::Superseded: return "superseded"; case Frame_Outcome::Render_Acquire_Dropped: return "render acquire dropped"; case Frame_Outcome::Publish_Dropped: return "publish dropped"; case Frame_Outcome::Paint_Acquire_Dropped: return "paint acquire dropped"; case Frame_Outcome::Worker_Budget_Dropped: return "worker budget dropped"; case Frame_Outcome::Cancelled: return "cancelled"; } return "unknown"; } std::string period_text(const std::string& key, std::uint64_t ns) { return number_text(key, static_cast(ns) / 1000000.0) + "ms"; } static double cache_total(const Renderable_Cache_Aggregate_Stat& stat) { return static_cast(stat.hit_count + stat.miss_count); } 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 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) { return number_text(key, static_cast(ns) / 1000000.0); } std::string cache_key(const Renderable_Cache_Aggregate_Stat& stat) const { return stat.parent_object_name + "/" + stat.object_name; } std::string renderable_key(const Renderable_Aggregate_Stat& stat) const { return stat.parent_object_name + "/" + stat.object_name + "/" + std::to_string(stat.tree_depth); } static std::string indent_str(int depth) { return std::string(static_cast(std::max(0, depth)) * 2, ' '); } static std::string cache_mode_text(bool cache_enabled) { return cache_enabled ? "cached" : "direct"; } static std::uint64_t frame_render_time_ns(const Frame_Lifecycle_Record& frame) { return frame.render_end_ns > frame.render_begin_ns ? frame.render_end_ns - frame.render_begin_ns : 0; } std::string ns_ratio_text(const std::string& key, std::uint64_t numerator, std::uint64_t denominator) { if (!denominator) 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 { return cache_total(stat) > 0.0 || stat.rebuild_count || stat.rebuild_ms.times || stat.compose_ms.times || stat.subtree_compose_ms.times; } bool should_show_renderable(const Renderable_Aggregate_Stat& stat) const { return stat.render_count || stat.self_draw_count || stat.total_render_ms.times || stat.self_draw_ms.times; } std::string cache_duration_text(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"; } void append_cache_node_lines(const Renderable_Cache_Aggregate_Stat& stat) { std::string key = cache_key(stat); std::string indent = indent_str(stat.tree_depth); std::string name = stat.object_name.empty() ? "unnamed" : stat.object_name; lines.push_back("cache tree " + indent + name + " hit:" + count_text(key + "_cache_hit", stat.hit_count) + " stale:" + count_text(key + "_cache_miss", stat.miss_count) + " miss:" + count_text(key + "_cache_rebuild", stat.rebuild_count) + " ratio:" + cache_ratio_text(key + "_cache_ratio", stat.hit_count, stat.miss_count)); lines.push_back("cache image " + indent + name + " area:" + count_text(key + "_cache_image_area", stat.last_image_area) + " bytes:" + count_text(key + "_cache_image_bytes", stat.last_image_bytes) + " version:" + count_text(key + "_cache_edit_version", stat.last_edit_version) + "/" + count_text(key + "_cache_ready_version", stat.last_ready_version)); lines.push_back("cache rebuild " + indent + name + " " + cache_duration_text(key + "_rebuild", "rebuild", stat.rebuild_ms)); lines.push_back("cache compose " + indent + name + " " + cache_duration_text(key + "_compose", "self compose", stat.compose_ms)); lines.push_back("cache subtree " + indent + name + " " + cache_duration_text(key + "_subtree_compose", "subtree compose", stat.subtree_compose_ms)); } void append_renderable_node_lines(const Renderable_Aggregate_Stat& stat) { std::string key = renderable_key(stat); std::string indent = indent_str(stat.tree_depth); std::string name = (stat.object_name.empty() ? "unnamed" : stat.object_name); std::uint64_t latest_render_time = frame_render_time_ns(aggregate.latest); std::string name_indent = indent + std::string(name.size(), ' '); lines.push_back(indent + " " + name + " cache:" + cache_mode_text(stat.cache_enabled) + " render:" + count_text(key + "_render_count", stat.render_count) + " draws:" + count_text(key + "_self_draw_count", stat.self_draw_count) + " cur%:" + ns_ratio_text(key + "_current_total_ratio", stat.last_total_render_time_ns, latest_render_time) + " total%:" + ns_ratio_text(key + "_aggregate_total_ratio", stat.total_render_time_ns, aggregate.render_time_ns)); lines.push_back(name_indent + " total " + duration_line(key + "_total_render", "time", stat.total_render_ms)); lines.push_back(name_indent + " total% " + percent_stat_line(key + "_total_ratio", "ratio", stat.total_render_ratio)); lines.push_back(name_indent + " self " + duration_line(key + "_self_draw", "time", stat.self_draw_ms)); lines.push_back(name_indent + " self% " + percent_stat_line(key + "_self_ratio", "ratio", stat.self_draw_ratio)); } void append_renderable_tree_lines(const std::string& parent_object_name, int tree_depth, std::vector& emitted) { std::vector children; for (const auto& stat : aggregate.renderable_stats) { if (stat.parent_object_name == parent_object_name && stat.tree_depth == tree_depth && should_show_renderable(stat)) children.push_back(&stat); } std::stable_sort(children.begin(), children.end(), [](const auto* a, const auto* b) { return a->object_name < b->object_name; }); for (const auto* stat : children) { if (std::find(emitted.begin(), emitted.end(), stat) != emitted.end()) continue; emitted.push_back(stat); append_renderable_node_lines(*stat); append_renderable_tree_lines(stat->object_name, stat->tree_depth + 1, emitted); } } void append_renderable_tree_lines() { std::vector emitted; append_renderable_tree_lines({}, 0, emitted); for (const auto& stat : aggregate.renderable_stats) { if (!should_show_renderable(stat) || std::find(emitted.begin(), emitted.end(), &stat) != emitted.end()) continue; emitted.push_back(&stat); append_renderable_node_lines(stat); } } void append_cache_tree_lines(const std::string& parent_object_name, int tree_depth, std::vector& emitted) { std::vector children; for (const auto& stat : aggregate.renderable_cache_stats) { if (stat.parent_object_name == parent_object_name && stat.tree_depth == tree_depth && should_show_cache(stat)) children.push_back(&stat); } std::stable_sort(children.begin(), children.end(), [](const auto* a, const auto* b) { return a->object_name < b->object_name; }); for (const auto* stat : children) { if (std::find(emitted.begin(), emitted.end(), stat) != emitted.end()) continue; emitted.push_back(stat); append_cache_node_lines(*stat); append_cache_tree_lines(stat->object_name, stat->tree_depth + 1, emitted); } } void append_cache_tree_lines() { std::vector emitted; append_cache_tree_lines({}, 0, emitted); for (const auto& stat : aggregate.renderable_cache_stats) { if (!should_show_cache(stat) || std::find(emitted.begin(), emitted.end(), &stat) != emitted.end()) continue; emitted.push_back(&stat); append_cache_node_lines(stat); } } void rebuild_lines() { if (!aggregate.has_frame) { lines = {"waiting for frame stats"}; return; } const Frame_Lifecycle_Record& frame = aggregate.latest; const Refresh_Control_Snapshot& refresh = frame.refresh; const Frame_Worker_Stats& 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; lines.clear(); lines.push_back("frame:" + count_text("frame_id", frame.frame_id) + " input_ver:" + count_text("input_version", frame.input_version) + " outcome:" + marked_value(Performance_Metric_Id::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("queue render:" + ns_text("render_queue_wait", frame.render_queue_wait_ns) + "ms ready:" + ns_text("ready_queue_wait", frame.ready_queue_wait_ns) + "ms present:" + ns_text("present_queue_wait", frame.present_queue_wait_ns) + "ms return:" + ns_text("paint_return_wait", frame.paint_return_wait_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_jobs) + " started:" + count_text("taskflow_started", executor.started_task_nodes) + " completed:" + count_text("taskflow_completed", executor.completed_task_nodes) + " 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.max_queue_wait_ns) + "ms run avg:" + ns_text("taskflow_run_avg", executor.average_task_duration_ns) + "ms p95:" + ns_text("taskflow_run_p95", executor.max_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)); lines.push_back(duration_line("draw", "lifecycle draw", aggregate.draw_ms)); lines.push_back(duration_line("render", "lifecycle render_total", aggregate.render_ms)); lines.push_back(duration_line("ready_wait", "lifecycle ready_to_paint", aggregate.ready_wait_ms)); lines.push_back(duration_line("update_wait", "lifecycle update_wait", aggregate.update_wait_ms)); lines.push_back(duration_line("paint", "lifecycle paint", aggregate.paint_ms)); lines.push_back(duration_line("frame_age", "lifecycle input_to_display_age", aggregate.frame_age_ms)); lines.push_back(duration_line("frame_total", "lifecycle e2e_total", aggregate.frame_total_ms)); lines.push_back("cache hit:" + count_text("cache_hit_count", aggregate.cache_hit_count) + " miss:" + count_text("cache_miss_count", aggregate.cache_miss_count) + " ratio:" + number_text("cache_hit_ratio", cache_hit_ratio)); lines.push_back("cache rebuild count:" + count_text("cache_rebuild_count", aggregate.cache_rebuild_count) + " time:" + ns_text("cache_rebuild_time", aggregate.cache_rebuild_time_ns) + "ms compose:" + ns_text("cache_compose_time", aggregate.cache_compose_time_ns) + "ms"); lines.push_back(duration_line("cache_rebuild_slide", "cache total rebuild", aggregate.cache_rebuild_ms)); lines.push_back(duration_line("cache_compose_slide", "cache total compose", aggregate.cache_compose_ms)); lines.push_back(percent_stat_line("cache_rebuild_ratio_slide", "cache rebuild ratio", aggregate.cache_rebuild_ratio)); lines.push_back(percent_stat_line("cache_compose_ratio_slide", "cache compose ratio", aggregate.cache_compose_ratio)); lines.push_back("cache cur rebuild:" + ns_ratio_text("cache_rebuild_current_ratio", frame.renderable_cache_rebuild_time_ns, latest_render_time) + " compose:" + ns_ratio_text("cache_compose_current_ratio", frame.renderable_cache_compose_time_ns, latest_render_time) + " total rebuild:" + ns_ratio_text("cache_rebuild_aggregate_ratio", aggregate.cache_rebuild_time_ns, aggregate.render_time_ns) + " compose:" + ns_ratio_text("cache_compose_aggregate_ratio", aggregate.cache_compose_time_ns, aggregate.render_time_ns)); append_renderable_tree_lines(); append_cache_tree_lines(); 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)); } 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_Line_Layout line; Performance_Text_Span 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_Overlay_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_Line_Layout& 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_Span& run) { return run.value ? run.reserved_width : run.measured_width; } static void append_run(Performance_Line_Layout& line, const Performance_Text_Span& run) { line.runs.push_back(run); line.width += run_width(run); } void append_wrapped_runs(std::vector runs, Canvas& canvas, double available_width) { Performance_Line_Layout current; for (auto& original_run : runs) { Performance_Text_Span 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_Span 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_Line_Layout& line) { if (line.runs.empty()) return; render_lines.push_back(std::move(line)); line = {}; } Performance_Text_Span split_text_run(Canvas& canvas, Performance_Text_Span& run, double available_width) { Performance_Text_Span 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_Span 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_Span 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_Span& 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_Metric_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_Line_Layout& 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; } }; struct Performance_Overlay_Private { std::atomic_bool enabled_value{false}; std::shared_ptr metrics_worker; Performance_Overlay_Private() : metrics_worker(std::make_shared()) {} ~Performance_Overlay_Private() { if (!metrics_worker) return; metrics_worker->cancelled.store(true, std::memory_order_release); Performance_Frame_Record frame; while (metrics_worker->metric_queue.try_pop(frame)) {} } void enqueue_frame_metric(std::shared_ptr frame) { if (!frame) return; if (!metrics_worker->metric_queue.try_push(std::move(frame))) { metrics_worker->dropped_frame_record_count.fetch_add(1, std::memory_order_relaxed); return; } schedule_performance_metrics_worker(metrics_worker); } 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->dropped_frame_record_count.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->enabled.load(std::memory_order_acquire)) { state->published_snapshot.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; } state->worker_active.store(false, std::memory_order_release); } static std::shared_ptr build_display_snapshot(Performance_Worker_State& worker_state) { Performance_Layout_Builder layout; layout.aggregate = worker_state.aggregate; layout.field_layout_states = worker_state.field_layout_states; layout.rebuild_lines(); auto snapshot = std::make_shared(); 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() || layout.lines.empty()) { snapshot->lines = layout.stripped_lines(); return snapshot; } Performance_Overlay_Render_State state; { std::lock_guard lock(worker_state.options_mutex); static_cast(state) = worker_state.options; } 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"); layout.seed_static_field_widths(measure_canvas); int available_text_width = viewport.width - state.left_margin - state.right_margin - state.scroll_bar_width - state.scroll_bar_margin * 2; layout.build_render_lines(measure_canvas, static_cast(std::max(1, available_text_width))); } worker_state.field_layout_states = layout.field_layout_states; snapshot->lines = layout.display_lines(); layout.update_content_width(&state, worker_state.maximum_content_width, viewport.width); double line_height = std::max(1.0, metrics.line_height()); double panel_width = std::min(layout.content_width, static_cast(std::max(0, viewport.width))); double height = layout.panel_height(line_height, &state, viewport.height); if (panel_width <= 0.0 || height <= 0.0) return snapshot; double content_height = layout.content_height(line_height, &state); double content_viewport_height = layout.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); layout.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); 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(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(layout.render_lines.size()) && y < text_rect.bottom(); ++i, y += line_height) { if (y + line_height < text_rect.y) continue; layout.draw_render_line(canvas, layout.render_lines[static_cast(i)], text_rect.x, y); } canvas.reset_clip(); layout.update_scroll_bar_rects(&state, max_scroll, scroll, content_viewport_height, content_height); if (!layout.scroll_bar_rect.empty()) { canvas.set_brush(Brush{state.scroll_track, Brush_Style::Solid}); canvas.fill_rect(layout.scroll_bar_rect); canvas.set_brush(Brush{state.scroll_thumb, Brush_Style::Solid}); canvas.fill_rect(layout.scroll_thumb_rect); } canvas.restore(); snapshot->destination_rect = layout.panel_rect; snapshot->scroll_track_rect = layout.scroll_bar_rect; snapshot->scroll_thumb_rect = layout.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 = layout.scroll_bar_rect; worker_state.last_scroll_thumb_rect = layout.scroll_thumb_rect; worker_state.last_content_viewport_height = content_viewport_height; worker_state.last_line_height = line_height; worker_state.maximum_scroll_offset = max_scroll; return snapshot; } private: static Size current_viewport(const Performance_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_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_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_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.maximum_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.maximum_scroll_offset; } } if (drag_end) worker_state.drag_active.store(false, std::memory_order_release); offset = performance_clamp_scroll_offset(offset, worker_state.maximum_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; } }; } // namespace renderive