From 7a94264c130b3e523a3b91c6be93c3db3fd59c30 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sat, 15 Aug 2026 23:24:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 + Kernel/CMakeLists.txt | 11 + .../render_graph/External_Operation.cpp | 268 +++++++++++++++--- .../render_graph/External_Operation.hpp | 45 ++- .../renderive/render_graph/Frame_Analysis.cpp | 127 ++++++++- .../renderive/render_graph/Frame_Analysis.hpp | 40 ++- .../renderive/render_graph/Render_Plan.cpp | 88 +++--- .../renderive/render_graph/Render_Plan.hpp | 5 +- .../detail/Render_Graph_Runtime.cpp | 219 ++++++++++---- .../detail/Render_Graph_Runtime.hpp | 8 +- .../renderive/scene/base/Abstract_Frame.hpp | 3 +- .../src/renderive/scene/base/Scene_Base.cpp | 94 +++--- .../render_graph/Render_DAG_Test.cpp | 78 ++++- .../Render_Graph_Runtime_Test.cpp | 129 +++++++++ Qt/CMakeLists.txt | 15 +- cmake/RenderiveConfig.cmake.in | 38 +++ cmake/RenderiveInstall.cmake | 40 +++ export.h | 11 - render_2D/CMakeLists.txt | 11 + .../render_2D/renderable/Render_Partition.h | 47 ++- render_3D/CMakeLists.txt | 34 ++- render_3D/render_3D/Point_Scene.cpp | 133 +++++---- render_3D/render_3D/Point_Scene.h | 1 + .../detail/Gpu_Completion_Service.cpp | 31 ++ render_3D/render_3D/detail/Render_Domain.cpp | 47 ++- render_3D/render_3D/detail/Render_Domain.h | 4 + render_3D/tests/Render_Domain_Tests.cpp | 28 ++ web_server/app/Gallery_Capture_Json.h | 65 ++++- web_server/app/render_3D/Gallery_Scene3D.cpp | 4 +- web_server/tests/Web_Bridge_Tests.cpp | 12 +- webapp_gallery/src/protocol/gallery_types.ts | 12 +- webapp_gallery/tests/dag/dag_model.test.ts | 88 +++++- 32 files changed, 1450 insertions(+), 290 deletions(-) create mode 100644 cmake/RenderiveConfig.cmake.in create mode 100644 cmake/RenderiveInstall.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef39ea..38638ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 3.20) project(Renderive VERSION 1.0.0 LANGUAGES CXX) +include(GNUInstallDirs) set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(RENDERIVE_BUILD_2D "Build 2D renderer" ON) @@ -34,4 +35,7 @@ if (RENDERIVE_BUILD_WEB) add_subdirectory(web_server) endif () include("${CMAKE_CURRENT_LIST_DIR}/cmake/RenderivePackage.cmake") +if (TARGET Renderive_Kernel) + include("${CMAKE_CURRENT_LIST_DIR}/cmake/RenderiveInstall.cmake") +endif () include("${CMAKE_CURRENT_LIST_DIR}/third_party/build_infra/end.cmake") diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index af30f94..de22386 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -20,8 +20,11 @@ endif () set(Renderive_Kernel_source_dir "${CMAKE_CURRENT_LIST_DIR}/src") append_glob_source(Renderive_Kernel_sources "${Renderive_Kernel_source_dir}") add_library(Renderive_Kernel STATIC ${Renderive_Kernel_sources}) +add_library(Renderive::Kernel ALIAS Renderive_Kernel) +set_target_properties(Renderive_Kernel PROPERTIES EXPORT_NAME Kernel) target_include_directories(Renderive_Kernel PUBLIC "$" + "$" ) target_compile_features(Renderive_Kernel PUBLIC cxx_std_20) target_link_libraries(Renderive_Kernel PUBLIC TBB::tbb) @@ -39,6 +42,14 @@ endfunction() if (MSVC) target_compile_options(Renderive_Kernel PRIVATE /utf-8) endif () +install(TARGETS Renderive_Kernel + EXPORT RenderiveTargets + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +install(DIRECTORY "${Renderive_Kernel_source_dir}/renderive" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp") if (RENDERIVE_BUILD_TESTS) set(Renderive_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") append_glob_source(Renderive_Kernel_test_sources "${Renderive_Kernel_test_dir}") diff --git a/Kernel/src/renderive/render_graph/External_Operation.cpp b/Kernel/src/renderive/render_graph/External_Operation.cpp index 3b0a9a6..1bb7f85 100644 --- a/Kernel/src/renderive/render_graph/External_Operation.cpp +++ b/Kernel/src/renderive/render_graph/External_Operation.cpp @@ -1,23 +1,199 @@ #include "External_Operation.hpp" +#include +#include #include -#include +#include +#include #include +#include + +namespace { + +class Deadline_Service final { +public: + using Clock = std::chrono::steady_clock; + using Callback = std::function; + using Ticket = std::uint64_t; + + static Deadline_Service& instance() { + static Deadline_Service service; + return service; + } + + [[nodiscard]] Ticket schedule(Clock::time_point deadline, Callback callback) { + Ticket ticket{}; + { + std::lock_guard lock(mutex_); + ticket = ++sequence_; + auto iterator = entries_.emplace(deadline, Entry{ticket, std::move(callback)}); + tickets_.emplace(ticket, iterator); + } + condition_.notify_one(); + return ticket; + } + + void cancel(Ticket ticket) noexcept { + if (ticket == 0) + return; + bool notify{}; + { + std::lock_guard lock(mutex_); + const auto found = tickets_.find(ticket); + if (found == tickets_.end()) + return; + notify = found->second == entries_.begin(); + entries_.erase(found->second); + tickets_.erase(found); + } + if (notify) + condition_.notify_one(); + } + +private: + struct Entry { + Ticket ticket{}; + Callback callback; + }; + using Entries = std::multimap; + + Deadline_Service() : thread_([this] { run(); }) {} + ~Deadline_Service() { + { + std::lock_guard lock(mutex_); + stopping_ = true; + } + condition_.notify_one(); + if (thread_.joinable()) + thread_.join(); + } + + void run() noexcept { + std::unique_lock lock(mutex_); + for (;;) { + if (stopping_) + return; + if (entries_.empty()) { + condition_.wait(lock, [this] { + return stopping_ || !entries_.empty(); + }); + continue; + } + const auto first = entries_.begin(); + const auto deadline = first->first; + const auto ticket = first->second.ticket; + if (condition_.wait_until(lock, deadline, [this, deadline, ticket] { + return stopping_ || entries_.empty() || + entries_.begin()->first != deadline || + entries_.begin()->second.ticket != ticket; + })) + continue; + if (stopping_ || entries_.empty()) + continue; + const auto current = entries_.begin(); + if (current->first > Clock::now()) + continue; + auto callback = std::move(current->second.callback); + tickets_.erase(current->second.ticket); + entries_.erase(current); + lock.unlock(); + try { + callback(); + } catch (...) { + } + lock.lock(); + } + } + + std::mutex mutex_; + std::condition_variable condition_; + Entries entries_; + std::unordered_map tickets_; + Ticket sequence_{}; + bool stopping_{}; + std::thread thread_; +}; + +std::exception_ptr cancellation_error(std::exception_ptr reason) noexcept { + if (reason) + return reason; + try { + throw External_Operation_Cancelled(); + } catch (...) { + return std::current_exception(); + } +} + +std::exception_ptr deadline_error() noexcept { + try { + throw External_Operation_Deadline_Exceeded(); + } catch (...) { + return std::current_exception(); + } +} + +} // namespace struct External_Operation::State { - enum class Status { - pending, - completed, - failed - }; + bool finish(External_Operation_Status terminal, + std::exception_ptr terminal_error, + std::function commit = {}) noexcept { + Deadline_Service::Ticket deadline_ticket{}; + { + std::lock_guard lock(mutex); + if (status != External_Operation_Status::pending || terminal_claimed) + return false; + terminal_claimed = true; + deadline_ticket = std::exchange(this->deadline_ticket, 0); + deadline.reset(); + } + + if (deadline_ticket != 0) + Deadline_Service::instance().cancel(deadline_ticket); + if (commit) { + try { + commit(); + } catch (...) { + terminal = External_Operation_Status::failed; + terminal_error = std::current_exception(); + } + } + + Completion callback; + std::exception_ptr callback_error; + { + std::lock_guard lock(mutex); + status = terminal; + error = std::move(terminal_error); + callback_error = error; + callback = std::move(completion); + } + if (callback) { + try { + callback(std::move(callback_error)); + } catch (...) { + } + } + return true; + } std::mutex mutex; Completion completion; std::exception_ptr error; - Status status{Status::pending}; + External_Operation_Status status{External_Operation_Status::pending}; bool subscribed{}; + bool terminal_claimed{}; + std::optional deadline; + Deadline_Service::Ticket deadline_ticket{}; }; +External_Operation_Cancelled::External_Operation_Cancelled() + : std::runtime_error("external operation cancelled") {} +External_Operation_Cancelled::External_Operation_Cancelled(const char* message) + : std::runtime_error(message) {} +External_Operation_Deadline_Exceeded::External_Operation_Deadline_Exceeded() + : External_Operation_Cancelled("external operation deadline exceeded") {} + External_Operation::External_Operation(std::shared_ptr state) noexcept : state_(std::move(state)) {} @@ -35,7 +211,7 @@ void External_Operation::on_complete(Completion completion) const { throw std::logic_error( "external operation already has a completion subscriber"); state_->subscribed = true; - if (state_->status == State::Status::pending) { + if (state_->status == External_Operation_Status::pending) { state_->completion = std::move(completion); return; } @@ -50,6 +226,18 @@ void External_Operation::on_complete(Completion completion) const { } } +bool External_Operation::cancel(std::exception_ptr reason) const noexcept { + return state_ && state_->finish(External_Operation_Status::cancelled, + cancellation_error(std::move(reason))); +} + +External_Operation_Status External_Operation::status() const noexcept { + if (!state_) + return External_Operation_Status::cancelled; + std::lock_guard lock(state_->mutex); + return state_->status; +} + External_Operation::operator bool() const noexcept { return static_cast(state_); } @@ -61,8 +249,9 @@ External_Operation External_Operation_Source::operation() const noexcept { return External_Operation(state_); } -bool External_Operation_Source::complete() noexcept { - return finish(false, {}); +bool External_Operation_Source::complete(std::function commit) noexcept { + return state_ && state_->finish(External_Operation_Status::completed, {}, + std::move(commit)); } bool External_Operation_Source::fail(std::exception_ptr error) noexcept { @@ -73,34 +262,49 @@ bool External_Operation_Source::fail(std::exception_ptr error) noexcept { error = std::current_exception(); } } - return finish(true, std::move(error)); + return state_ && state_->finish(External_Operation_Status::failed, + std::move(error)); } -bool External_Operation_Source::finish( - bool failed, std::exception_ptr error) noexcept { - if (!state_) - return false; +bool External_Operation_Source::cancel(std::exception_ptr reason) noexcept { + return state_ && state_->finish(External_Operation_Status::cancelled, + cancellation_error(std::move(reason))); +} - External_Operation::Completion completion; - std::exception_ptr completion_error; +void External_Operation_Source::set_deadline(Clock::time_point deadline) { + if (!state_) + throw std::logic_error("external operation source is empty"); + + Deadline_Service::Ticket previous_ticket{}; { std::lock_guard lock(state_->mutex); - if (state_->status != External_Operation::State::Status::pending) - return false; - state_->status = failed - ? External_Operation::State::Status::failed - : External_Operation::State::Status::completed; - state_->error = std::move(error); - completion_error = state_->error; - completion = std::move(state_->completion); + if (state_->status != External_Operation_Status::pending || + state_->terminal_claimed) + return; + if (state_->deadline && *state_->deadline <= deadline) + return; + + std::weak_ptr weak = state_; + const auto ticket = Deadline_Service::instance().schedule( + deadline, [weak, deadline] { + const auto state = weak.lock(); + if (!state) + return; + { + std::lock_guard lock(state->mutex); + if (state->status != External_Operation_Status::pending || + state->terminal_claimed || !state->deadline || + *state->deadline != deadline) + return; + } + static_cast(state->finish( + External_Operation_Status::cancelled, deadline_error())); + }); + previous_ticket = std::exchange(state_->deadline_ticket, ticket); + state_->deadline = deadline; } - if (completion) { - try { - completion(std::move(completion_error)); - } catch (...) { - } - } - return true; + if (previous_ticket != 0) + Deadline_Service::instance().cancel(previous_ticket); } Node_Execution_Result::Node_Execution_Result( diff --git a/Kernel/src/renderive/render_graph/External_Operation.hpp b/Kernel/src/renderive/render_graph/External_Operation.hpp index 8028db8..a0a6605 100644 --- a/Kernel/src/renderive/render_graph/External_Operation.hpp +++ b/Kernel/src/renderive/render_graph/External_Operation.hpp @@ -1,22 +1,48 @@ #pragma once +#include #include #include #include #include +#include class External_Operation_Source; +class External_Operation_Cancelled : public std::runtime_error { +public: + External_Operation_Cancelled(); + explicit External_Operation_Cancelled(const char* message); +}; + +class External_Operation_Deadline_Exceeded final + : public External_Operation_Cancelled { +public: + External_Operation_Deadline_Exceeded(); +}; + +enum class External_Operation_Status : unsigned char { + pending, + completed, + failed, + cancelled +}; + class External_Operation { public: using Completion = std::function; External_Operation() = default; - // Completion callbacks are notification boundaries. Exceptions thrown by - // them are contained regardless of whether completion happened before or - // after subscription. + // Exactly one completion subscriber is allowed. Completion callbacks are + // notification boundaries: exceptions thrown by them are always contained. void on_complete(Completion completion) const; + + // Cancellation is terminal and idempotent. Producers must keep any backend + // resources needed by already-submitted work alive independently of the + // operation handle; late complete()/fail() calls then become harmless no-ops. + [[nodiscard]] bool cancel(std::exception_ptr reason = {}) const noexcept; + [[nodiscard]] External_Operation_Status status() const noexcept; [[nodiscard]] explicit operator bool() const noexcept; private: @@ -31,6 +57,8 @@ private: class External_Operation_Source { public: + using Clock = std::chrono::steady_clock; + External_Operation_Source(); External_Operation_Source(const External_Operation_Source&) = delete; External_Operation_Source& operator=(const External_Operation_Source&) = delete; @@ -38,11 +66,18 @@ public: External_Operation_Source& operator=(External_Operation_Source&&) noexcept = default; [[nodiscard]] External_Operation operation() const noexcept; - [[nodiscard]] bool complete() noexcept; + // The commit runs only if completion wins the terminal-state race, and it + // runs before the completion subscriber is notified. This lets an async + // producer publish result data without racing cancellation-driven teardown. + [[nodiscard]] bool complete(std::function commit = {}) noexcept; [[nodiscard]] bool fail(std::exception_ptr error) noexcept; + [[nodiscard]] bool cancel(std::exception_ptr reason = {}) noexcept; + + // A deadline is optional and may only move earlier. Expiry cancels the + // operation with External_Operation_Deadline_Exceeded. + void set_deadline(Clock::time_point deadline); private: - [[nodiscard]] bool finish(bool failed, std::exception_ptr error) noexcept; std::shared_ptr state_; }; diff --git a/Kernel/src/renderive/render_graph/Frame_Analysis.cpp b/Kernel/src/renderive/render_graph/Frame_Analysis.cpp index e62b99b..4e1cab0 100644 --- a/Kernel/src/renderive/render_graph/Frame_Analysis.cpp +++ b/Kernel/src/renderive/render_graph/Frame_Analysis.cpp @@ -12,6 +12,30 @@ std::uint64_t subtract_saturated(std::uint64_t value, std::uint64_t origin) { return value >= origin ? value - origin : 0; } +std::uint64_t add_saturated(std::uint64_t left, std::uint64_t right) { + return right > std::numeric_limits::max() - left + ? std::numeric_limits::max() + : left + right; +} + +std::uint64_t metric(const Node_Execution& execution, + Node_Metric_Kind kind) noexcept { + return execution.metrics.contains(kind) ? execution.metrics.get(kind) : 0; +} + +std::uint64_t render_domain_cpu_duration(const Node_Execution& execution) noexcept { + std::uint64_t total{}; + for (const auto kind : { + Node_Metric_Kind::apply_duration_ns, + Node_Metric_Kind::plan_emit_duration_ns, + Node_Metric_Kind::backend_execute_duration_ns, + Node_Metric_Kind::submit_duration_ns, + Node_Metric_Kind::readback_duration_ns}) { + total = add_saturated(total, metric(execution, kind)); + } + return total; +} + double percentile(std::vector values, double probability) { if (values.empty()) return 0.0; @@ -27,7 +51,12 @@ double percentile(std::vector values, double probability) { struct Node_Samples { std::vector durations; std::vector cpu_durations; + std::vector render_domain_cpu_durations; + std::vector total_cpu_work_durations; std::vector external_durations; + std::vector render_domain_queue_waits; + std::vector gpu_completion_waits; + std::vector gpu_execution_durations; std::vector waits; std::size_t critical_count{}; }; @@ -58,12 +87,39 @@ Node_Statistics statistics_for(Render_Node_Id id, const Node_Samples& samples) { const auto external_sum = std::accumulate( samples.external_durations.begin(), samples.external_durations.end(), std::uint64_t{}); + const auto render_domain_cpu_sum = std::accumulate( + samples.render_domain_cpu_durations.begin(), + samples.render_domain_cpu_durations.end(), std::uint64_t{}); + const auto total_cpu_work_sum = std::accumulate( + samples.total_cpu_work_durations.begin(), + samples.total_cpu_work_durations.end(), std::uint64_t{}); + const auto queue_wait_sum = std::accumulate( + samples.render_domain_queue_waits.begin(), + samples.render_domain_queue_waits.end(), std::uint64_t{}); + const auto gpu_completion_sum = std::accumulate( + samples.gpu_completion_waits.begin(), samples.gpu_completion_waits.end(), + std::uint64_t{}); + const auto gpu_execution_sum = std::accumulate( + samples.gpu_execution_durations.begin(), samples.gpu_execution_durations.end(), + std::uint64_t{}); result.average_cpu_ns = static_cast(cpu_sum) / samples.cpu_durations.size(); + result.average_render_domain_cpu_ns = static_cast(render_domain_cpu_sum) / + samples.render_domain_cpu_durations.size(); + result.average_total_cpu_work_ns = static_cast(total_cpu_work_sum) / + samples.total_cpu_work_durations.size(); result.average_external_ns = static_cast(external_sum) / samples.external_durations.size(); + result.average_render_domain_queue_wait_ns = static_cast(queue_wait_sum) / + samples.render_domain_queue_waits.size(); + result.average_gpu_completion_wait_ns = static_cast(gpu_completion_sum) / + samples.gpu_completion_waits.size(); + result.average_gpu_execution_ns = static_cast(gpu_execution_sum) / + samples.gpu_execution_durations.size(); result.p95_cpu_ns = percentile(samples.cpu_durations, 0.95); + result.p95_render_domain_cpu_ns = percentile(samples.render_domain_cpu_durations, 0.95); result.p95_external_ns = percentile(samples.external_durations, 0.95); + result.p95_gpu_execution_ns = percentile(samples.gpu_execution_durations, 0.95); result.critical_path_frequency = samples.critical_count; result.average_scheduler_wait_ns = samples.waits.empty() ? 0.0 @@ -95,13 +151,39 @@ Frame_Analysis analyze_frame(const Render_Plan& plan, const Frame_Snapshot& fram analysis.node_id = node.node_id; analysis.duration_ns = execution.duration_ns(); analysis.cpu_duration_ns = execution.cpu_duration_ns(); + analysis.render_domain_cpu_duration_ns = + render_domain_cpu_duration(execution); + analysis.total_cpu_work_duration_ns = add_saturated( + analysis.cpu_duration_ns, analysis.render_domain_cpu_duration_ns); analysis.external_duration_ns = execution.external_duration_ns(); + analysis.render_domain_queue_wait_ns = + metric(execution, Node_Metric_Kind::queue_wait_ns); + analysis.gpu_completion_wait_ns = + metric(execution, Node_Metric_Kind::gpu_fence_wait_ns); + analysis.gpu_execution_duration_ns = + metric(execution, Node_Metric_Kind::gpu_total_duration_ns); analysis.start_offset_ns = subtract_saturated(execution.start_time_ns, frame.render_start_ns); analysis.end_offset_ns = subtract_saturated(execution.end_time_ns, frame.render_start_ns); - result.total_work_duration_ns += analysis.cpu_duration_ns; - result.total_external_duration_ns += analysis.external_duration_ns; + result.scheduler_work_duration_ns = add_saturated( + result.scheduler_work_duration_ns, analysis.cpu_duration_ns); + result.render_domain_work_duration_ns = add_saturated( + result.render_domain_work_duration_ns, + analysis.render_domain_cpu_duration_ns); + result.total_work_duration_ns = add_saturated( + result.total_work_duration_ns, analysis.total_cpu_work_duration_ns); + result.total_external_duration_ns = add_saturated( + result.total_external_duration_ns, analysis.external_duration_ns); + result.total_render_domain_queue_wait_ns = add_saturated( + result.total_render_domain_queue_wait_ns, + analysis.render_domain_queue_wait_ns); + result.total_gpu_completion_wait_ns = add_saturated( + result.total_gpu_completion_wait_ns, + analysis.gpu_completion_wait_ns); + result.total_gpu_execution_duration_ns = add_saturated( + result.total_gpu_execution_duration_ns, + analysis.gpu_execution_duration_ns); } std::vector> predecessors(plan.graph.nodes.size()); @@ -162,7 +244,7 @@ Frame_Analysis analyze_frame(const Render_Plan& plan, const Frame_Snapshot& fram for (auto& node : result.nodes) { node.work_contribution = result.total_work_duration_ns == 0 ? 0.0 - : static_cast(node.cpu_duration_ns) / + : static_cast(node.total_cpu_work_duration_ns) / result.total_work_duration_ns; node.critical_path_contribution = !node.on_critical_path || result.critical_path_duration_ns == 0 @@ -198,17 +280,25 @@ Frame_Analysis analyze_frame(const Render_Plan& plan, const Frame_Snapshot& fram --active; } else { ++active; - result.peak_parallelism = std::max(result.peak_parallelism, active); + result.scheduler_peak_parallelism = std::max(result.scheduler_peak_parallelism, active); } previous_time = event.time; } - result.parallel_overlap_ns = result.total_work_duration_ns >= busy_duration - ? result.total_work_duration_ns - busy_duration + result.scheduler_parallel_overlap_ns = result.scheduler_work_duration_ns >= busy_duration + ? result.scheduler_work_duration_ns - busy_duration : 0; - result.average_parallelism = result.total_render_duration_ns == 0 + result.average_cpu_concurrency = result.total_render_duration_ns == 0 ? 0.0 : static_cast(result.total_work_duration_ns) / result.total_render_duration_ns; + result.scheduler_average_parallelism = result.total_render_duration_ns == 0 + ? 0.0 + : static_cast(result.scheduler_work_duration_ns) / + result.total_render_duration_ns; + result.render_domain_utilization = result.total_render_duration_ns == 0 + ? 0.0 + : static_cast(result.render_domain_work_duration_ns) / + result.total_render_duration_ns; result.workers.reserve(worker_work.size()); for (const auto& [worker_id, work] : worker_work) { result.workers.push_back({worker_id, work, @@ -242,7 +332,16 @@ std::vector analyze_node_statistics( auto& values = samples[node.node_id]; values.durations.push_back(node.duration_ns); values.cpu_durations.push_back(node.cpu_duration_ns); + values.render_domain_cpu_durations.push_back( + node.render_domain_cpu_duration_ns); + values.total_cpu_work_durations.push_back( + node.total_cpu_work_duration_ns); values.external_durations.push_back(node.external_duration_ns); + values.render_domain_queue_waits.push_back( + node.render_domain_queue_wait_ns); + values.gpu_completion_waits.push_back(node.gpu_completion_wait_ns); + values.gpu_execution_durations.push_back( + node.gpu_execution_duration_ns); values.waits.push_back(node.scheduler_wait_ns); values.critical_count += node.on_critical_path ? 1 : 0; } @@ -273,11 +372,15 @@ std::vector analyze_plan_versions( std::vector waits; render_durations.reserve(group.size()); double parallelism_sum{}; + double scheduler_parallelism_sum{}; + double render_domain_utilization_sum{}; for (const auto& frame : group) { render_durations.push_back(frame.total_render_duration_ns); - parallelism_sum += frame.average_parallelism; - statistics.peak_parallelism = std::max(statistics.peak_parallelism, - frame.peak_parallelism); + parallelism_sum += frame.average_cpu_concurrency; + scheduler_parallelism_sum += frame.scheduler_average_parallelism; + render_domain_utilization_sum += frame.render_domain_utilization; + statistics.scheduler_peak_parallelism = std::max(statistics.scheduler_peak_parallelism, + frame.scheduler_peak_parallelism); for (const auto& node : frame.nodes) waits.push_back(node.scheduler_wait_ns); } @@ -287,7 +390,9 @@ std::vector analyze_plan_versions( statistics.render_p95_ns = percentile(render_durations, 0.95); statistics.render_maximum_ns = *std::max_element(render_durations.begin(), render_durations.end()); - statistics.average_parallelism = parallelism_sum / group.size(); + statistics.average_cpu_concurrency = parallelism_sum / group.size(); + statistics.scheduler_average_parallelism = scheduler_parallelism_sum / group.size(); + statistics.render_domain_utilization = render_domain_utilization_sum / group.size(); statistics.scheduler_wait_average_ns = waits.empty() ? 0.0 : static_cast(std::accumulate(waits.begin(), waits.end(), diff --git a/Kernel/src/renderive/render_graph/Frame_Analysis.hpp b/Kernel/src/renderive/render_graph/Frame_Analysis.hpp index 4577298..26e4d7e 100644 --- a/Kernel/src/renderive/render_graph/Frame_Analysis.hpp +++ b/Kernel/src/renderive/render_graph/Frame_Analysis.hpp @@ -9,8 +9,18 @@ struct Node_Frame_Analysis { Render_Node_Id node_id{}; std::uint64_t duration_ns{}; + // CPU executed by the scheduler worker that entered the graph node. std::uint64_t cpu_duration_ns{}; + // CPU executed later on a backend/render affinity domain. This is derived + // from backend phase metrics and is deliberately not folded into + // external_duration_ns as if it were GPU time. + std::uint64_t render_domain_cpu_duration_ns{}; + std::uint64_t total_cpu_work_duration_ns{}; + // Wall time between scheduler handoff and external completion. std::uint64_t external_duration_ns{}; + std::uint64_t render_domain_queue_wait_ns{}; + std::uint64_t gpu_completion_wait_ns{}; + std::uint64_t gpu_execution_duration_ns{}; std::uint64_t start_offset_ns{}; std::uint64_t end_offset_ns{}; std::uint64_t dependency_ready_time_ns{}; @@ -31,11 +41,22 @@ struct Frame_Analysis { Render_Plan_Version render_plan_version{}; std::uint64_t total_render_duration_ns{}; std::uint64_t critical_path_duration_ns{}; + // Total CPU work across scheduler workers and explicit backend domains. std::uint64_t total_work_duration_ns{}; + std::uint64_t scheduler_work_duration_ns{}; + std::uint64_t render_domain_work_duration_ns{}; std::uint64_t total_external_duration_ns{}; - std::uint64_t parallel_overlap_ns{}; - std::size_t peak_parallelism{}; - double average_parallelism{}; + std::uint64_t total_render_domain_queue_wait_ns{}; + std::uint64_t total_gpu_completion_wait_ns{}; + std::uint64_t total_gpu_execution_duration_ns{}; + // Overlap and peak are exact for scheduler-worker intervals. Backend phase + // metrics currently carry durations, not timestamps, so they are excluded + // from exact overlap/peak calculations instead of being guessed. + std::uint64_t scheduler_parallel_overlap_ns{}; + std::size_t scheduler_peak_parallelism{}; + double average_cpu_concurrency{}; + double scheduler_average_parallelism{}; + double render_domain_utilization{}; std::vector critical_path; std::vector bottleneck_nodes; std::vector nodes; @@ -53,9 +74,16 @@ struct Node_Statistics { std::uint64_t minimum_ns{}; std::uint64_t maximum_ns{}; double average_cpu_ns{}; + double average_render_domain_cpu_ns{}; + double average_total_cpu_work_ns{}; double average_external_ns{}; + double average_render_domain_queue_wait_ns{}; + double average_gpu_completion_wait_ns{}; + double average_gpu_execution_ns{}; double p95_cpu_ns{}; + double p95_render_domain_cpu_ns{}; double p95_external_ns{}; + double p95_gpu_execution_ns{}; std::size_t critical_path_frequency{}; double average_scheduler_wait_ns{}; }; @@ -67,8 +95,10 @@ struct Plan_Version_Statistics { double render_p50_ns{}; double render_p95_ns{}; std::uint64_t render_maximum_ns{}; - double average_parallelism{}; - std::size_t peak_parallelism{}; + double average_cpu_concurrency{}; + double scheduler_average_parallelism{}; + double render_domain_utilization{}; + std::size_t scheduler_peak_parallelism{}; double scheduler_wait_average_ns{}; double scheduler_wait_p95_ns{}; std::vector nodes; diff --git a/Kernel/src/renderive/render_graph/Render_Plan.cpp b/Kernel/src/renderive/render_graph/Render_Plan.cpp index 4d7d859..c77da3b 100644 --- a/Kernel/src/renderive/render_graph/Render_Plan.cpp +++ b/Kernel/src/renderive/render_graph/Render_Plan.cpp @@ -22,6 +22,50 @@ void normalize(Render_Graph& graph) { graph.edges.erase(std::unique(graph.edges.begin(), graph.edges.end()), graph.edges.end()); } +void validate_graph(const Render_Graph& graph) { + std::unordered_map indices; + indices.reserve(graph.nodes.size()); + for (std::size_t index = 0; index < graph.nodes.size(); ++index) { + const auto id = graph.nodes[index].node_id; + if (id == 0 || !indices.emplace(id, index).second) + throw std::invalid_argument("render graph has an invalid or duplicate node id"); + } + std::vector> successors(graph.nodes.size()); + std::vector indegree(graph.nodes.size()); + for (const auto& edge : graph.edges) { + const auto from = indices.find(edge.from); + const auto to = indices.find(edge.to); + if (from == indices.end() || to == indices.end()) + throw std::invalid_argument("render graph edge references an unknown node"); + if (from->second == to->second) + throw std::invalid_argument("render graph cycle"); + successors[from->second].push_back(to->second); + ++indegree[to->second]; + } + std::vector ready; + ready.reserve(graph.nodes.size()); + for (std::size_t index = 0; index < indegree.size(); ++index) { + if (indegree[index] == 0) + ready.push_back(index); + } + for (std::size_t cursor = 0; cursor < ready.size(); ++cursor) { + for (const auto successor : successors[ready[cursor]]) { + if (--indegree[successor] == 0) + ready.push_back(successor); + } + } + if (ready.size() != graph.nodes.size()) + throw std::invalid_argument("render graph cycle"); +} + +struct Render_Edge_Hash { + std::size_t operator()(const Render_Edge& edge) const noexcept { + const std::size_t first = std::hash{}(edge.from); + const std::size_t second = std::hash{}(edge.to); + return first ^ (second + 0x9e3779b9U + (first << 6U) + (first >> 2U)); + } +}; + bool same_topology(const Render_Graph& left, const Render_Graph& right) { if (left.nodes.size() != right.nodes.size() || left.edges != right.edges) return false; @@ -42,9 +86,7 @@ Render_Graph_Builder::Task Render_Graph_Builder::emplace( Render_Node_Kind kind) { if (node_id == 0) throw std::invalid_argument("render node id must not be zero"); - if (std::any_of(graph_.nodes.begin(), graph_.nodes.end(), [node_id](const Render_Node& node) { - return node.node_id == node_id; - })) + if (!node_ids_.insert(node_id).second) throw std::invalid_argument("duplicate render node id"); graph_.nodes.push_back({node_id, owner_id, std::move(name), kind, graph_.nodes.size()}); @@ -54,10 +96,10 @@ Render_Graph_Builder::Task Render_Graph_Builder::emplace( void Render_Graph_Builder::precede(Task from, Task to) { validate(from); validate(to); - if (from.index == to.index || reaches(to.index, from.index)) + if (from.index == to.index) throw std::invalid_argument("render graph cycle"); const Render_Edge edge{graph_.nodes[from.index].node_id, graph_.nodes[to.index].node_id}; - if (std::find(graph_.edges.begin(), graph_.edges.end(), edge) == graph_.edges.end()) + if (successors_[edge.from].insert(edge.to).second) graph_.edges.push_back(edge); } @@ -68,6 +110,7 @@ const Render_Graph& Render_Graph_Builder::graph() const noexcept { Render_Graph Render_Graph_Builder::finish() && { ++generation_; normalize(graph_); + validate_graph(graph_); return std::move(graph_); } @@ -77,32 +120,9 @@ void Render_Graph_Builder::validate(Task task) const { throw std::invalid_argument("invalid render graph task"); } -bool Render_Graph_Builder::reaches(std::size_t from, std::size_t target) const { - std::unordered_map indices; - indices.reserve(graph_.nodes.size()); - for (std::size_t index = 0; index < graph_.nodes.size(); ++index) - indices.emplace(graph_.nodes[index].node_id, index); - std::vector stack{from}; - std::vector visited(graph_.nodes.size()); - while (!stack.empty()) { - const std::size_t index = stack.back(); - stack.pop_back(); - if (index == target) - return true; - if (visited[index]) - continue; - visited[index] = true; - const Render_Node_Id id = graph_.nodes[index].node_id; - for (const Render_Edge& edge : graph_.edges) { - if (edge.from == id) - stack.push_back(indices.at(edge.to)); - } - } - return false; -} - std::shared_ptr Render_Plan_History::publish(Render_Graph graph) { normalize(graph); + validate_graph(graph); std::lock_guard lock(mutex_); if (!plans_.empty() && same_topology(plans_.back()->graph, graph)) return plans_.back(); @@ -151,14 +171,16 @@ Render_Plan_Difference compare_render_plans(const Render_Plan& before, if (!after_nodes.contains(id)) result.removed_nodes.push_back(id); } + std::unordered_set before_edges( + before.graph.edges.begin(), before.graph.edges.end()); + std::unordered_set after_edges( + after.graph.edges.begin(), after.graph.edges.end()); for (const auto& edge : after.graph.edges) { - if (std::find(before.graph.edges.begin(), before.graph.edges.end(), edge) == - before.graph.edges.end()) + if (!before_edges.contains(edge)) result.added_edges.push_back(edge); } for (const auto& edge : before.graph.edges) { - if (std::find(after.graph.edges.begin(), after.graph.edges.end(), edge) == - after.graph.edges.end()) + if (!after_edges.contains(edge)) result.removed_edges.push_back(edge); } std::sort(result.added_nodes.begin(), result.added_nodes.end()); diff --git a/Kernel/src/renderive/render_graph/Render_Plan.hpp b/Kernel/src/renderive/render_graph/Render_Plan.hpp index d60cddb..112306f 100644 --- a/Kernel/src/renderive/render_graph/Render_Plan.hpp +++ b/Kernel/src/renderive/render_graph/Render_Plan.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include using Render_Node_Id = std::uint64_t; @@ -50,8 +52,9 @@ public: [[nodiscard]] Render_Graph finish() &&; private: void validate(Task task) const; - bool reaches(std::size_t from, std::size_t target) const; Render_Graph graph_; + std::unordered_set node_ids_; + std::unordered_map> successors_; std::uint64_t generation_{1}; }; diff --git a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp index e508ffe..dbae8d2 100644 --- a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp +++ b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp @@ -9,7 +9,19 @@ #include #include "renderive/scheduling/detail/OneTBB_Runtime.hpp" namespace renderive::render_graph::detail { -struct Render_Graph_Runtime::State : std::enable_shared_from_this { +namespace { +std::exception_ptr make_cancellation_error(std::exception_ptr reason) noexcept { + if (reason) + return reason; + try { + throw External_Operation_Cancelled("render graph execution cancelled"); + } catch (...) { + return std::current_exception(); + } +} +} +struct Render_Graph_Runtime::State + : std::enable_shared_from_this { using Message = oneapi::tbb::flow::continue_msg; using Ready_Node = oneapi::tbb::flow::continue_node; using Execute_Node_Type = oneapi::tbb::flow::async_node; @@ -18,7 +30,9 @@ struct Render_Graph_Runtime::State : std::enable_shared_from_this execute; bool root{}; }; - explicit State(const Render_Plan& plan) : execution_count(plan.graph.nodes.size()) { + explicit State(const Render_Plan& plan) + : execution_count(plan.graph.nodes.size()), + external_operations(plan.graph.nodes.size()) { nodes.resize(plan.graph.nodes.size()); std::unordered_map indices; indices.reserve(plan.graph.nodes.size()); @@ -29,26 +43,39 @@ struct Render_Graph_Runtime::State : std::enable_shared_from_this(graph, [this, index](const Message&) { - mark_ready(index); - return Message{}; - }); - nodes[index].execute = std::make_unique(graph, oneapi::tbb::flow::unlimited, [this, index](const Message&, Execute_Node_Type::gateway_type& gateway) { - run_node(index, gateway); - }); - oneapi::tbb::flow::make_edge(*nodes[index].ready, *nodes[index].execute); + nodes[index].ready = std::make_unique( + graph, [this, index](const Message&) { + mark_ready(index); + return Message{}; + }); + nodes[index].execute = std::make_unique( + graph, oneapi::tbb::flow::unlimited, + [this, index](const Message&, + Execute_Node_Type::gateway_type& gateway) { + run_node(index, gateway); + }); + oneapi::tbb::flow::make_edge(*nodes[index].ready, + *nodes[index].execute); } for (const auto& edge : plan.graph.edges) - oneapi::tbb::flow::make_edge(*nodes[indices.at(edge.from)].execute, *nodes[indices.at(edge.to)].ready); + oneapi::tbb::flow::make_edge( + *nodes[indices.at(edge.from)].execute, + *nodes[indices.at(edge.to)].ready); } - void execute(std::span execution_slots, Execute_Node execute) { + void execute(std::span execution_slots, + Execute_Node execute) { if (running.exchange(true, std::memory_order_acq_rel)) throw std::logic_error("render graph runtime is already executing"); + + std::exception_ptr execution_error; try { if (!execute) - throw std::invalid_argument("render graph node executor is empty"); - if (!execution_slots.empty() && execution_slots.size() != execution_count) - throw std::invalid_argument("render graph execution slot count differs from plan"); + throw std::invalid_argument( + "render graph node executor is empty"); + if (!execution_slots.empty() && + execution_slots.size() != execution_count) + throw std::invalid_argument( + "render graph execution slot count differs from plan"); graph.reset(); executions.assign(execution_slots.begin(), execution_slots.end()); if (executions.empty()) @@ -59,6 +86,11 @@ struct Render_Graph_Runtime::State : std::enable_shared_from_thisstatus = Node_Execution_Status::ready; } } - void run_node(std::size_t index, Execute_Node_Type::gateway_type& gateway) noexcept { + void run_node(std::size_t index, + Execute_Node_Type::gateway_type& gateway) noexcept { if (failed.load(std::memory_order_acquire)) return; + if (cancellation_requested.load(std::memory_order_acquire)) { + fail(index, cancellation_error(), render_clock_now_ns(), true); + return; + } Node_Execution_Metrics* metrics{}; if (auto* execution = executions[index]) { execution->start_time_ns = render_clock_now_ns(); @@ -106,10 +171,17 @@ struct Render_Graph_Runtime::State : std::enable_shared_from_this(result.operation().cancel(error)); + fail(index, error, cpu_end, true); + return; + } if (!result.is_external()) { complete(index, cpu_end); gateway.try_put(Message{}); @@ -122,23 +194,54 @@ struct Render_Graph_Runtime::State : std::enable_shared_from_this(operation.cancel(cancellation_error())); auto self = shared_from_this(); auto* gateway_ptr = &gateway; - result.operation().on_complete([self = std::move(self), gateway_ptr, index](std::exception_ptr error) { - const std::uint64_t end = render_clock_now_ns(); - if (error) - self->fail(index, std::move(error), end); - else { - self->complete_external(index, end); - gateway_ptr->try_put(Message{}); - } - gateway_ptr->release_wait(); - }); + operation.on_complete( + [self = std::move(self), gateway_ptr, index, operation]( + std::exception_ptr error) { + const std::uint64_t end = render_clock_now_ns(); + self->clear_external(index); + if (error) { + self->fail( + index, std::move(error), end, + operation.status() == + External_Operation_Status::cancelled); + } else { + self->complete_external(index, end); + gateway_ptr->try_put(Message{}); + } + gateway_ptr->release_wait(); + }); } catch (...) { - fail(index, std::current_exception(), render_clock_now_ns()); + clear_external(index); + fail(index, std::current_exception(), render_clock_now_ns(), false); gateway.release_wait(); } } + void clear_external(std::size_t index) noexcept { + std::lock_guard lock(external_mutex); + external_operations[index] = External_Operation{}; + } + void cancel_external_operations(std::exception_ptr reason) noexcept { + std::vector active; + { + std::lock_guard lock(external_mutex); + active.reserve(external_operations.size()); + for (const auto& operation : external_operations) { + if (operation) + active.push_back(operation); + } + } + for (const auto& operation : active) + static_cast(operation.cancel(reason)); + } void complete(std::size_t index, std::uint64_t end) noexcept { if (auto* execution = executions[index]) { execution->cpu_end_time_ns = end; @@ -153,8 +256,10 @@ struct Render_Graph_Runtime::State : std::enable_shared_from_thisstatus = Node_Execution_Status::complete; } } - void fail(std::size_t index, std::exception_ptr error, std::uint64_t end) noexcept { - failed.store(true, std::memory_order_release); + void fail(std::size_t index, std::exception_ptr error, + std::uint64_t end, bool cancelled) noexcept { + const bool first_failure = + !failed.exchange(true, std::memory_order_acq_rel); if (auto* execution = executions[index]) { if (execution->start_time_ns == 0) execution->start_time_ns = end; @@ -163,11 +268,17 @@ struct Render_Graph_Runtime::State : std::enable_shared_from_thisstatus == Node_Execution_Status::waiting_external) execution->external_end_time_ns = end; execution->end_time_ns = end; - execution->status = Node_Execution_Status::failed; + execution->status = cancelled + ? Node_Execution_Status::cancelled + : Node_Execution_Status::failed; } - std::lock_guard lock(error_mutex); - if (!first_exception) - first_exception = std::move(error); + { + std::lock_guard lock(error_mutex); + if (!first_exception) + first_exception = error; + } + if (first_failure) + cancel_pending(std::move(error)); } oneapi::tbb::flow::graph graph; std::vector nodes; @@ -176,12 +287,24 @@ struct Render_Graph_Runtime::State : std::enable_shared_from_this external_operations; + std::mutex cancellation_mutex; + std::exception_ptr cancellation_exception; std::atomic_bool running{}; std::atomic_bool failed{}; + std::atomic_bool cancellation_requested{}; }; -Render_Graph_Runtime::Render_Graph_Runtime(const Render_Plan& plan) : state_(std::make_shared(plan)) {} -Render_Graph_Runtime::~Render_Graph_Runtime() = default; -void Render_Graph_Runtime::execute(std::span executions, Execute_Node execute_node) { +Render_Graph_Runtime::Render_Graph_Runtime(const Render_Plan& plan) + : state_(std::make_shared(plan)) {} +Render_Graph_Runtime::~Render_Graph_Runtime() { + state_->cancel_pending({}); +} +void Render_Graph_Runtime::execute( + std::span executions, Execute_Node execute_node) { state_->execute(executions, std::move(execute_node)); } +void Render_Graph_Runtime::cancel_pending(std::exception_ptr reason) noexcept { + state_->cancel_pending(std::move(reason)); +} } diff --git a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.hpp b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.hpp index 768937a..3cba66c 100644 --- a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.hpp +++ b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -9,14 +10,17 @@ namespace renderive::render_graph::detail { class Render_Graph_Runtime final { public: - using Execute_Node = std::function; + using Execute_Node = std::function; explicit Render_Graph_Runtime(const Render_Plan& plan); ~Render_Graph_Runtime(); Render_Graph_Runtime(const Render_Graph_Runtime&) = delete; Render_Graph_Runtime& operator=(const Render_Graph_Runtime&) = delete; Render_Graph_Runtime(Render_Graph_Runtime&&) = delete; Render_Graph_Runtime& operator=(Render_Graph_Runtime&&) = delete; - void execute(std::span executions, Execute_Node execute_node); + void execute(std::span executions, + Execute_Node execute_node); + void cancel_pending(std::exception_ptr reason = {}) noexcept; private: struct State; std::shared_ptr state_; diff --git a/Kernel/src/renderive/scene/base/Abstract_Frame.hpp b/Kernel/src/renderive/scene/base/Abstract_Frame.hpp index bc5d71e..7c25a63 100644 --- a/Kernel/src/renderive/scene/base/Abstract_Frame.hpp +++ b/Kernel/src/renderive/scene/base/Abstract_Frame.hpp @@ -13,7 +13,8 @@ enum class Node_Execution_Status : std::uint8_t { running, waiting_external, complete, - failed + failed, + cancelled }; enum class Node_Metric_Kind : std::uint8_t { diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index c0a118b..bff40a6 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -946,11 +947,18 @@ bool Scene_Base::consume_model_dirty() noexcept { return model_dirty_.exchange(false, std::memory_order_acq_rel); } void Scene_Base::shutdown() noexcept { + std::shared_ptr compiled_plan; { - std::unique_lock lock(task_mutex_); + std::lock_guard lock(task_mutex_); if (shutting_down_) return; shutting_down_ = true; + compiled_plan = compiled_render_plan_; + } + if (compiled_plan && compiled_plan->runtime) + compiled_plan->runtime->cancel_pending(); + { + std::unique_lock lock(task_mutex_); render_completed_.wait(lock, [this] { return pending_operations_ == 0; }); } execution_context_->wait(); @@ -1047,13 +1055,14 @@ std::shared_ptr Scene_Base::compile_render_pla Render_Graph graph; std::unordered_map functions; - const auto append_edge = [&graph](Render_Node_Id from, Render_Node_Id to) { + std::unordered_map> + graph_successors; + const auto append_edge = [&graph, &graph_successors](Render_Node_Id from, + Render_Node_Id to) { if (from == 0 || to == 0 || from == to) return; - const Render_Edge edge{from, to}; - if (std::find(graph.edges.begin(), graph.edges.end(), edge) == - graph.edges.end()) - graph.edges.push_back(edge); + if (graph_successors[from].insert(to).second) + graph.edges.push_back({from, to}); }; for (auto& item : active) { @@ -1072,41 +1081,40 @@ std::shared_ptr Scene_Base::compile_render_pla Execution_Binding{item.snapshot_index, Execution_Function{function}}); }, render_function); } - for (const auto& edge : state.render_graph->graph.edges) + std::unordered_map local_kinds; + local_kinds.reserve(state.render_graph->graph.nodes.size()); + for (const auto& node : state.render_graph->graph.nodes) + local_kinds.emplace(node.node_id, node.kind); + std::unordered_set prepare_has_predecessor; + std::unordered_set prepare_has_successor; + std::unordered_set paint_has_successor; + for (const auto& edge : state.render_graph->graph.edges) { append_edge(edge.from, edge.to); - const auto stage_boundary = [&](Render_Node_Kind kind, bool roots) { - std::vector result; - for (const auto& node : state.render_graph->graph.nodes) { - if (node.kind != kind) - continue; - const bool connected = std::any_of( - state.render_graph->graph.edges.begin(), - state.render_graph->graph.edges.end(), - [&](const Render_Edge& edge) { - const Render_Node_Id adjacent = roots ? edge.from : edge.to; - const bool matches = roots ? edge.to == node.node_id - : edge.from == node.node_id; - if (!matches) - return false; - return std::any_of( - state.render_graph->graph.nodes.begin(), - state.render_graph->graph.nodes.end(), - [&](const Render_Node& candidate) { - return candidate.node_id == adjacent && - candidate.kind == kind; - }); - }); - if (!connected) - result.push_back(node.node_id); + const auto from = local_kinds.find(edge.from); + const auto to = local_kinds.find(edge.to); + if (from == local_kinds.end() || to == local_kinds.end()) + throw std::logic_error( + "renderable graph edge references an unknown node"); + if (from->second == Render_Node_Kind::prepare && + to->second == Render_Node_Kind::prepare) { + prepare_has_successor.insert(edge.from); + prepare_has_predecessor.insert(edge.to); } - return result; - }; - item.prepare_roots = - stage_boundary(Render_Node_Kind::prepare, true); - item.prepare_terminals = - stage_boundary(Render_Node_Kind::prepare, false); - item.paint_terminals = - stage_boundary(Render_Node_Kind::paint, false); + if (from->second == Render_Node_Kind::paint && + to->second == Render_Node_Kind::paint) + paint_has_successor.insert(edge.from); + } + for (const auto& node : state.render_graph->graph.nodes) { + if (node.kind == Render_Node_Kind::prepare) { + if (!prepare_has_predecessor.contains(node.node_id)) + item.prepare_roots.push_back(node.node_id); + if (!prepare_has_successor.contains(node.node_id)) + item.prepare_terminals.push_back(node.node_id); + } else if (node.kind == Render_Node_Kind::paint && + !paint_has_successor.contains(node.node_id)) { + item.paint_terminals.push_back(node.node_id); + } + } } for (const auto& child : active) { @@ -1159,12 +1167,8 @@ std::shared_ptr Scene_Base::compile_render_pla std::vector terminals; terminals.reserve(graph.nodes.size()); for (const auto& node : graph.nodes) { - const bool has_successor = std::any_of( - graph.edges.begin(), graph.edges.end(), - [&node](const Render_Edge& edge) { - return edge.from == node.node_id; - }); - if (!has_successor) + const auto successors = graph_successors.find(node.node_id); + if (successors == graph_successors.end() || successors->second.empty()) terminals.push_back(node.node_id); } graph.nodes.push_back({scene_render_node_id_, 0, "Render Scene", diff --git a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp index d405fc7..85775ec 100644 --- a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp @@ -93,9 +93,9 @@ TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_f EXPECT_EQ(analysis.nodes[2].scheduler_wait_ns, 10u); EXPECT_EQ(analysis.total_render_duration_ns, 100u); EXPECT_EQ(analysis.total_work_duration_ns, 100u); - EXPECT_EQ(analysis.peak_parallelism, 2u); - EXPECT_DOUBLE_EQ(analysis.average_parallelism, 1.0); - EXPECT_EQ(analysis.parallel_overlap_ns, 30u); + EXPECT_EQ(analysis.scheduler_peak_parallelism, 2u); + EXPECT_DOUBLE_EQ(analysis.average_cpu_concurrency, 1.0); + EXPECT_EQ(analysis.scheduler_parallel_overlap_ns, 30u); EXPECT_EQ(analysis.critical_path, (std::vector{100, 102})); EXPECT_EQ(analysis.critical_path_duration_ns, 70u); EXPECT_DOUBLE_EQ(analysis.nodes[0].work_contribution, 0.4); @@ -278,3 +278,75 @@ TEST(render_dag_test, capture_repository_retains_only_recent_completed_sessions) EXPECT_FALSE(repository.session(1)); EXPECT_TRUE(repository.session(20)); } + +TEST(render_dag_test, render_domain_phases_are_counted_as_cpu_work_not_gpu_wall_time) { + Render_Graph_Builder builder; + builder.emplace(5001, 50, "3D Render", Render_Node_Kind::render); + Render_Plan_History history; + const auto plan = history.publish(std::move(builder).finish()); + + Frame_Snapshot snapshot; + snapshot.frame_id = 11; + snapshot.render_plan_version = plan->version; + snapshot.render_start_ns = 10'000; + snapshot.render_end_ns = 10'500; + snapshot.node_executions.resize(1); + auto& execution = snapshot.node_executions.front(); + execution.node_id = 5001; + execution.ready_time_ns = 10'000; + execution.start_time_ns = 10'000; + execution.cpu_end_time_ns = 10'020; + execution.external_start_time_ns = 10'020; + execution.external_end_time_ns = 10'500; + execution.end_time_ns = 10'500; + execution.worker_id = 2; + execution.status = Node_Execution_Status::complete; + execution.metrics.set(Node_Metric_Kind::queue_wait_ns, 30); + execution.metrics.set(Node_Metric_Kind::apply_duration_ns, 10); + execution.metrics.set(Node_Metric_Kind::plan_emit_duration_ns, 20); + execution.metrics.set(Node_Metric_Kind::backend_execute_duration_ns, 30); + execution.metrics.set(Node_Metric_Kind::submit_duration_ns, 40); + execution.metrics.set(Node_Metric_Kind::readback_duration_ns, 50); + execution.metrics.set(Node_Metric_Kind::gpu_fence_wait_ns, 300); + execution.metrics.set(Node_Metric_Kind::gpu_total_duration_ns, 250); + + const auto analysis = analyze_frame(*plan, snapshot); + ASSERT_EQ(analysis.nodes.size(), 1U); + EXPECT_EQ(analysis.scheduler_work_duration_ns, 20U); + EXPECT_EQ(analysis.render_domain_work_duration_ns, 150U); + EXPECT_EQ(analysis.total_work_duration_ns, 170U); + EXPECT_EQ(analysis.total_external_duration_ns, 480U); + EXPECT_EQ(analysis.total_render_domain_queue_wait_ns, 30U); + EXPECT_EQ(analysis.total_gpu_completion_wait_ns, 300U); + EXPECT_EQ(analysis.total_gpu_execution_duration_ns, 250U); + EXPECT_DOUBLE_EQ(analysis.scheduler_average_parallelism, 0.04); + EXPECT_DOUBLE_EQ(analysis.render_domain_utilization, 0.3); + EXPECT_DOUBLE_EQ(analysis.average_cpu_concurrency, 0.34); + + const auto statistics = analyze_node_statistics( + std::span(&analysis, 1)); + ASSERT_EQ(statistics.size(), 1U); + EXPECT_DOUBLE_EQ(statistics.front().average_render_domain_cpu_ns, 150.0); + EXPECT_DOUBLE_EQ(statistics.front().average_total_cpu_work_ns, 170.0); + EXPECT_DOUBLE_EQ(statistics.front().average_gpu_execution_ns, 250.0); +} + +TEST(render_dag_test, graph_cycle_is_validated_once_when_builder_finishes) { + Render_Graph_Builder builder; + const auto a = builder.emplace(6001, 60, "A", Render_Node_Kind::prepare); + const auto b = builder.emplace(6002, 60, "B", Render_Node_Kind::prepare); + const auto c = builder.emplace(6003, 60, "C", Render_Node_Kind::prepare); + builder.precede(a, b); + builder.precede(b, c); + builder.precede(c, a); + EXPECT_THROW(static_cast(std::move(builder).finish()), std::invalid_argument); +} + +TEST(render_dag_test, plan_history_rejects_invalid_manually_constructed_graphs) { + Render_Graph graph; + graph.nodes.push_back({7001, 70, "A", Render_Node_Kind::prepare, 0}); + graph.edges.push_back({7001, 7999}); + Render_Plan_History history; + EXPECT_THROW(static_cast(history.publish(std::move(graph))), + std::invalid_argument); +} diff --git a/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp b/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp index a1bddc7..7b10902 100644 --- a/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp @@ -193,3 +193,132 @@ TEST(render_graph_runtime_test, EXPECT_TRUE(source.complete()); execution.join(); } + +TEST(external_operation_test, cancellation_is_terminal_and_delivered_once) { + External_Operation_Source source; + const auto operation = source.operation(); + EXPECT_TRUE(operation.cancel()); + EXPECT_FALSE(source.complete()); + EXPECT_EQ(operation.status(), External_Operation_Status::cancelled); + + int completion_count{}; + std::exception_ptr completion_error; + operation.on_complete([&](std::exception_ptr error) { + completion_error = std::move(error); + ++completion_count; + }); + EXPECT_EQ(completion_count, 1); + ASSERT_TRUE(completion_error); + EXPECT_THROW(std::rethrow_exception(completion_error), + External_Operation_Cancelled); +} + +TEST(external_operation_test, deadline_cancels_pending_operation) { + External_Operation_Source source; + const auto operation = source.operation(); + std::mutex mutex; + std::condition_variable condition; + bool completed{}; + std::exception_ptr completion_error; + operation.on_complete([&](std::exception_ptr error) { + { + std::lock_guard lock(mutex); + completion_error = std::move(error); + completed = true; + } + condition.notify_one(); + }); + source.set_deadline(External_Operation_Source::Clock::now()); + { + std::unique_lock lock(mutex); + ASSERT_TRUE(condition.wait_for(lock, std::chrono::seconds(1), + [&] { return completed; })); + } + ASSERT_TRUE(completion_error); + EXPECT_THROW(std::rethrow_exception(completion_error), + External_Operation_Deadline_Exceeded); + EXPECT_EQ(operation.status(), External_Operation_Status::cancelled); +} + +TEST(render_graph_runtime_test, + cancellation_releases_external_wait_and_blocks_successor) { + const auto plan = two_node_plan(); + External_Operation_Source source; + std::vector execution_storage; + auto slots = execution_slots(*plan, execution_storage); + std::atomic submit_started{}; + std::atomic publish_executed{}; + std::exception_ptr graph_error; + + renderive::render_graph::detail::Render_Graph_Runtime runtime(*plan); + const auto execute_node = [&](std::size_t index, Node_Execution_Metrics*) { + if (index == 0) { + submit_started.store(true, std::memory_order_release); + submit_started.notify_all(); + return Node_Execution_Result::external(source.operation()); + } + publish_executed.store(true, std::memory_order_release); + return Node_Execution_Result::completed(); + }; + + std::thread execution([&] { + try { + runtime.execute(slots, execute_node); + } catch (...) { + graph_error = std::current_exception(); + } + }); + submit_started.wait(false, std::memory_order_acquire); + runtime.cancel_pending(); + execution.join(); + + ASSERT_TRUE(graph_error); + EXPECT_THROW(std::rethrow_exception(graph_error), + External_Operation_Cancelled); + EXPECT_FALSE(publish_executed.load(std::memory_order_acquire)); + EXPECT_EQ(execution_storage[0].status, Node_Execution_Status::cancelled); + EXPECT_EQ(source.operation().status(), External_Operation_Status::cancelled); +} + +TEST(external_operation_test, completion_commit_is_exclusive_with_cancellation) { + External_Operation_Source source; + const auto operation = source.operation(); + std::atomic published{}; + EXPECT_TRUE(operation.cancel()); + EXPECT_FALSE(source.complete([&] { + published.fetch_add(1, std::memory_order_relaxed); + })); + EXPECT_EQ(published.load(std::memory_order_relaxed), 0); +} + +TEST(external_operation_test, completion_commit_precedes_completion_notification) { + External_Operation_Source source; + const auto operation = source.operation(); + std::atomic published{}; + int observed{}; + operation.on_complete([&](std::exception_ptr error) { + EXPECT_FALSE(error); + observed = published.load(std::memory_order_acquire); + }); + + EXPECT_TRUE(source.complete([&] { + published.store(1, std::memory_order_release); + EXPECT_EQ(operation.status(), External_Operation_Status::pending); + })); + EXPECT_EQ(observed, 1); + EXPECT_EQ(operation.status(), External_Operation_Status::completed); +} + +TEST(external_operation_test, completion_commit_failure_fails_operation) { + External_Operation_Source source; + const auto operation = source.operation(); + std::exception_ptr completion_error; + operation.on_complete([&](std::exception_ptr error) { + completion_error = std::move(error); + }); + + EXPECT_TRUE(source.complete([] { throw std::runtime_error("publish failed"); })); + ASSERT_TRUE(completion_error); + EXPECT_THROW(std::rethrow_exception(completion_error), std::runtime_error); + EXPECT_EQ(operation.status(), External_Operation_Status::failed); +} diff --git a/Qt/CMakeLists.txt b/Qt/CMakeLists.txt index bd30fe2..7c47570 100644 --- a/Qt/CMakeLists.txt +++ b/Qt/CMakeLists.txt @@ -30,9 +30,11 @@ foreach (Renderive_Qt_source_dir IN LISTS Renderive_Qt_source_dirs) append_glob_source(Renderive_Qt_sources "${Renderive_Qt_source_dir}") endforeach () add_library(Renderive_Qt STATIC ${Renderive_Qt_sources}) -set_target_properties(Renderive_Qt PROPERTIES AUTOMOC ON) +add_library(Renderive::Qt ALIAS Renderive_Qt) +set_target_properties(Renderive_Qt PROPERTIES AUTOMOC ON EXPORT_NAME Qt) target_include_directories(Renderive_Qt PUBLIC "$" + "$" ) target_compile_features(Renderive_Qt PUBLIC cxx_std_20) target_link_libraries(Renderive_Qt PUBLIC @@ -44,6 +46,17 @@ target_link_libraries(Renderive_Qt PUBLIC if (MSVC) target_compile_options(Renderive_Qt PRIVATE /utf-8) endif () +install(TARGETS Renderive_Qt + EXPORT RenderiveTargets + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/bridge/" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/Qt/bridge" + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp") +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/plot/" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/Qt/plot" + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp") if (RENDERIVE_BUILD_TESTS) get_filename_component(Renderive_Qt_runtime_root "${Qt5_DIR}/../../.." ABSOLUTE) set(Renderive_Qt_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") diff --git a/cmake/RenderiveConfig.cmake.in b/cmake/RenderiveConfig.cmake.in new file mode 100644 index 0000000..f3dfc17 --- /dev/null +++ b/cmake/RenderiveConfig.cmake.in @@ -0,0 +1,38 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +find_dependency(Threads) +find_dependency(TBB CONFIG) + +set(Renderive_Kernel_FOUND TRUE) +set(Renderive_Render2D_FOUND @RENDERIVE_INSTALL_2D@) +set(Renderive_Render3D_FOUND @RENDERIVE_INSTALL_3D@) +set(Renderive_Qt_FOUND @RENDERIVE_INSTALL_QT@) + +if (@RENDERIVE_INSTALL_2D@) + find_dependency(blend2d CONFIG) +endif () + +if (@RENDERIVE_INSTALL_3D@) + find_dependency(Vulkan) + find_dependency(cglm CONFIG) + find_dependency(volk CONFIG) + find_dependency(VulkanMemoryAllocator CONFIG) + find_dependency(Freetype) + find_dependency(msdfgen CONFIG) + find_dependency(msdf-atlas-gen CONFIG) + find_dependency(tinyxml2 CONFIG) + find_dependency(tinyobjloader CONFIG) + find_dependency(ZLIB) + if (WIN32) + find_dependency(PThreads4W) + endif () +endif () + +if (@RENDERIVE_INSTALL_QT@) + find_dependency(Qt5 CONFIG COMPONENTS Core Gui Widgets) +endif () + +include("${CMAKE_CURRENT_LIST_DIR}/RenderiveTargets.cmake") +check_required_components(Renderive) diff --git a/cmake/RenderiveInstall.cmake b/cmake/RenderiveInstall.cmake new file mode 100644 index 0000000..c5704b6 --- /dev/null +++ b/cmake/RenderiveInstall.cmake @@ -0,0 +1,40 @@ +include(CMakePackageConfigHelpers) + +set(RENDERIVE_INSTALL_2D OFF) +set(RENDERIVE_INSTALL_3D OFF) +set(RENDERIVE_INSTALL_QT OFF) +if (TARGET Renderive_render_2D) + set(RENDERIVE_INSTALL_2D ON) +endif () +if (TARGET Renderive_render_3D) + set(RENDERIVE_INSTALL_3D ON) +endif () +if (TARGET Renderive_Qt) + set(RENDERIVE_INSTALL_QT ON) +endif () + +set(Renderive_install_cmake_dir "${CMAKE_INSTALL_LIBDIR}/cmake/Renderive") + +configure_package_config_file( + "${CMAKE_CURRENT_LIST_DIR}/RenderiveConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/RenderiveConfig.cmake" + INSTALL_DESTINATION "${Renderive_install_cmake_dir}") + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/RenderiveConfigVersion.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY SameMajorVersion) + +install(EXPORT RenderiveTargets + FILE RenderiveTargets.cmake + NAMESPACE Renderive:: + DESTINATION "${Renderive_install_cmake_dir}") +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/RenderiveConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/RenderiveConfigVersion.cmake" + DESTINATION "${Renderive_install_cmake_dir}") + +if (RENDERIVE_INSTALL_QT) + install(FILES "${PROJECT_SOURCE_DIR}/export.h" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") +endif () diff --git a/export.h b/export.h index 171dc9a..b4e7601 100644 --- a/export.h +++ b/export.h @@ -12,18 +12,7 @@ #include #include "render_2D/export.h" #include "Qt/plot/export.h" -#include "Widget/export.h" namespace renderive { -template -using Node = Flex_Qt::Node; -template -using Node_Manager = Flex_Qt::Node_Manager; -template -using Roll_Object = Flex_Qt::Roll_Object; -template -using Singleton_Widget = Flex_Qt::Singleton_Widget; -using Select_Color_Dialog = Flex_Qt::Select_Color_Dialog; -using Virtual_Keyboard = Flex_Qt::Virtual_Keyboard; inline Color qt_to_color(const QColor& color) { if (!color.isValid()) return Color::transparent(); diff --git a/render_2D/CMakeLists.txt b/render_2D/CMakeLists.txt index 8e74eec..bf669e3 100644 --- a/render_2D/CMakeLists.txt +++ b/render_2D/CMakeLists.txt @@ -22,14 +22,25 @@ endif () set(Renderive_render_2D_source_dir "${CMAKE_CURRENT_LIST_DIR}/render_2D") append_glob_source(Renderive_render_2D_sources "${Renderive_render_2D_source_dir}") add_library(Renderive_render_2D STATIC ${Renderive_render_2D_sources}) +add_library(Renderive::Render2D ALIAS Renderive_render_2D) +set_target_properties(Renderive_render_2D PROPERTIES EXPORT_NAME Render2D) target_include_directories(Renderive_render_2D PUBLIC "$" + "$" ) target_compile_features(Renderive_render_2D PUBLIC cxx_std_20) target_link_libraries(Renderive_render_2D PUBLIC Renderive_Kernel blend2d::blend2d) if (MSVC) target_compile_options(Renderive_render_2D PRIVATE /utf-8) endif () +install(TARGETS Renderive_render_2D + EXPORT RenderiveTargets + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +install(DIRECTORY "${Renderive_render_2D_source_dir}/" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/render_2D" + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp") if (RENDERIVE_BUILD_TESTS) set(Renderive_render_2D_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") append_glob_source(Renderive_render_2D_test_sources "${Renderive_render_2D_test_dir}") diff --git a/render_2D/render_2D/renderable/Render_Partition.h b/render_2D/render_2D/renderable/Render_Partition.h index 532753b..175913d 100644 --- a/render_2D/render_2D/renderable/Render_Partition.h +++ b/render_2D/render_2D/renderable/Render_Partition.h @@ -74,22 +74,51 @@ struct Adaptive_Render_Partitioner { std::chrono::microseconds(750)), std::chrono::nanoseconds(frame_interval_ns / 4)); const int previous = automatic_count_; - const auto estimated_serial_cost = elapsed_ns * std::max(1, active_count); - if (elapsed_ns > bottleneck_threshold && active_count < automatic_limit) { - automatic_count_ = std::min(automatic_limit, active_count * 2); + const auto elapsed_count = static_cast( + std::max(0, elapsed_ns.count())); + const auto active = static_cast(std::max(1, active_count)); + const auto maximum = std::numeric_limits::max(); + const auto measured_serial_ns = + active != 0 && elapsed_count > maximum / active + ? maximum + : elapsed_count * active; + if (serial_cost_ewma_ns_ == 0) { + serial_cost_ewma_ns_ = measured_serial_ns; + } else { + // 75% history + 25% newest sample without overflowing uint64_t. + serial_cost_ewma_ns_ = + (serial_cost_ewma_ns_ / 4) * 3 + measured_serial_ns / 4; } - else if (active_count > 1 && - estimated_serial_cost < bottleneck_threshold * 3 / 5) { - automatic_count_ = 1; - } - else { - automatic_count_ = std::clamp(active_count, 1, automatic_limit); + const auto target_ns = static_cast( + std::max(1, bottleneck_threshold.count())); + const auto required_partitions = + serial_cost_ewma_ns_ / target_ns + + (serial_cost_ewma_ns_ % target_ns != 0 ? 1U : 0U); + int desired = static_cast(std::min( + static_cast(automatic_limit), + std::max(1, required_partitions))); + // Collapse aggressively only when the measured serial work is clearly + // below 60% of the target. Otherwise use one-step hysteresis to avoid + // topology churn around a partition boundary. + const auto collapse_threshold = + (target_ns / 5) * 3 + ((target_ns % 5) * 3) / 5; + if (measured_serial_ns < collapse_threshold && active_count > 1) { + desired = 1; + } else if (desired > active_count) { + const int growth_limit = active_count <= automatic_limit / 2 + ? active_count * 2 + : automatic_limit; + desired = std::min(desired, std::max(active_count + 1, growth_limit)); + } else if (desired < active_count) { + desired = std::max(desired, active_count - 1); } + automatic_count_ = std::clamp(desired, 1, automatic_limit); return automatic_count_ != previous; } private: using Clock = std::chrono::steady_clock; Clock::time_point started_at_{}; + std::uint64_t serial_cost_ewma_ns_{}; int automatic_count_{1}; }; } // namespace detail diff --git a/render_3D/CMakeLists.txt b/render_3D/CMakeLists.txt index 0b3c7d2..1be8859 100644 --- a/render_3D/CMakeLists.txt +++ b/render_3D/CMakeLists.txt @@ -97,20 +97,48 @@ add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/datoviz" "${CMAKE_CURRENT_BINARY_DIR set(Renderive_render_3D_source_dir "${CMAKE_CURRENT_LIST_DIR}/render_3D") append_glob_source(Renderive_render_3D_sources "${Renderive_render_3D_source_dir}") add_library(Renderive_render_3D STATIC ${Renderive_render_3D_sources}) +add_library(Renderive::Render3D ALIAS Renderive_render_3D) +set_target_properties(Renderive_render_3D PROPERTIES EXPORT_NAME Render3D) target_include_directories(Renderive_render_3D PUBLIC "$" + "$" ) target_compile_features(Renderive_render_3D PUBLIC cxx_std_20) -target_link_libraries(Renderive_render_3D - PUBLIC Renderive_Kernel - PRIVATE ${Renderive_render_3D_datoviz_targets} tinyobjloader::tinyobjloader Threads::Threads +target_link_libraries(Renderive_render_3D PUBLIC Renderive_Kernel) +foreach (Renderive_render_3D_datoviz_target IN LISTS Renderive_render_3D_datoviz_targets) + target_link_libraries(Renderive_render_3D PRIVATE + "$") +endforeach () +target_link_libraries(Renderive_render_3D PRIVATE + tinyobjloader::tinyobjloader + Threads::Threads + cglm::cglm + volk::volk + GPUOpen::VulkanMemoryAllocator + Vulkan::Vulkan + Freetype::Freetype + msdfgen::msdfgen + msdf-atlas-gen::msdf-atlas-gen + tinyxml2::tinyxml2 + ZLIB::ZLIB ) +if (WIN32) + target_link_libraries(Renderive_render_3D PRIVATE PThreads4W::PThreads4W) +endif () if (MSVC) # Datoviz is embedded from OBJECT libraries in this static archive. Its Windows headers # otherwise declare every symbol as dllimport for this C++ translation unit. target_compile_definitions(Renderive_render_3D PRIVATE DVZ_SHARED) target_compile_options(Renderive_render_3D PRIVATE /utf-8) endif () +install(TARGETS Renderive_render_3D + EXPORT RenderiveTargets + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +install(DIRECTORY "${Renderive_render_3D_source_dir}/" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/render_3D" + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp") if (RENDERIVE_BUILD_TESTS) set(Renderive_render_3D_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") append_glob_source(Renderive_render_3D_test_sources "${Renderive_render_3D_test_dir}") diff --git a/render_3D/render_3D/Point_Scene.cpp b/render_3D/render_3D/Point_Scene.cpp index e9db3de..3afe336 100644 --- a/render_3D/render_3D/Point_Scene.cpp +++ b/render_3D/render_3D/Point_Scene.cpp @@ -115,6 +115,21 @@ using Manual_Frame = ::Manual_Refresh_Strategy<::Scene3D_Frame_Data>; using Low_Latency_Frame = ::Low_Latency_Strategy<::Scene3D_Frame_Data>; using Playback_Frame = ::Flow_Refresh_Strategy<::Scene3D_Frame_Data>; +struct Point_Backend_State final { + explicit Point_Backend_State(std::uint32_t gpu_index) + : render_domain(detail::Render_Domain::acquire(gpu_index)) {} + + ~Point_Backend_State() { + if (backend) + render_domain->invoke([this] { backend.reset(); }); + } + + std::shared_ptr render_domain; + std::unique_ptr backend; + mutable std::mutex frame_mutex; + std::shared_ptr latest; +}; + template struct Basic_Point_Scene final : ::Scene3D_Context, @@ -126,7 +141,7 @@ struct Basic_Point_Scene final Basic_Point_Scene(const Scene_Options& options, std::shared_ptr visual) requires (!std::same_as) - : Kernel_Scene(), render_domain(detail::Render_Domain::acquire(options.gpu_index)) { + : Kernel_Scene(), backend_state(std::make_shared(options.gpu_index)) { initialize(options, std::move(visual)); } @@ -136,13 +151,13 @@ struct Basic_Point_Scene final : Kernel_Scene(Observer_State<>{}, typename Strategy::Configuration{ .frequency_hz = options.maximum_frames_per_second, .replace_pending_frame = true}), - render_domain(detail::Render_Domain::acquire(options.gpu_index)) { + backend_state(std::make_shared(options.gpu_index)) { initialize(options, std::move(visual)); } ~Basic_Point_Scene() override { this->shutdown(); - render_domain->invoke([this] { backend.reset(); }); + backend_state.reset(); } void initialize(const Scene_Options& options, @@ -163,8 +178,9 @@ struct Basic_Point_Scene final } const detail::Scene_State initial{ options.viewport, options.clear_color, options.visual_family}; - render_domain->invoke([this, &options, &initial] { - backend = std::make_unique( + const auto state = backend_state; + state->render_domain->invoke([state, &options, &initial] { + state->backend = std::make_unique( options.gpu_index, options.validation_enabled, initial); }); } @@ -187,8 +203,9 @@ struct Basic_Point_Scene final ::renderive::Keyboard_Modifier modifiers) override { const Extent viewport = this->Scene_State_Strategy::template get< &detail::Scene_State::viewport>(); - render_domain->invoke([this, type, x, y, button, modifiers, viewport] { - backend->dispatch_pointer(type, x, y, button, modifiers, viewport); + const auto state = backend_state; + state->render_domain->invoke([state, type, x, y, button, modifiers, viewport] { + state->backend->dispatch_pointer(type, x, y, button, modifiers, viewport); }); this->notify_model_dirty(); } @@ -198,16 +215,20 @@ struct Basic_Point_Scene final ::renderive::Keyboard_Modifier modifiers) override { const Extent viewport = this->Scene_State_Strategy::template get< &detail::Scene_State::viewport>(); - render_domain->invoke( - [this, x, y, delta_x, delta_y, modifiers, viewport] { - backend->dispatch_wheel(x, y, delta_x, delta_y, modifiers, - viewport); + const auto state = backend_state; + state->render_domain->invoke( + [state, x, y, delta_x, delta_y, modifiers, viewport] { + state->backend->dispatch_wheel(x, y, delta_x, delta_y, modifiers, + viewport); }); this->notify_model_dirty(); } void dispatch_key(const ::renderive::Key_Event& event) override { - render_domain->invoke([this, event] { backend->dispatch_key(event); }); + const auto state = backend_state; + state->render_domain->invoke([state, event] { + state->backend->dispatch_key(event); + }); this->notify_model_dirty(); } @@ -253,8 +274,9 @@ struct Basic_Point_Scene final [[nodiscard]] std::shared_ptr latest_frame() const override { - std::lock_guard lock(frame_mutex); - return latest; + const auto state = backend_state; + std::lock_guard lock(state->frame_mutex); + return state->latest; } [[nodiscard]] Frame_Status frame_status() const override { @@ -289,22 +311,24 @@ struct Basic_Point_Scene final result.failed_operation_count = state.empty_acquire_count; result.pending_frame_count = state.pending_frame_count; } - std::lock_guard lock(frame_mutex); - if (latest) + const auto backend = backend_state; + std::lock_guard lock(backend->frame_mutex); + if (backend->latest) result.latest_sequence = - std::max(result.latest_sequence, latest->sequence); + std::max(result.latest_sequence, backend->latest->sequence); return result; } [[nodiscard]] Runtime_Statistics runtime_statistics() const noexcept override { - const auto render = render_domain->statistics(); + const auto render = backend_state->render_domain->statistics(); const auto completion = detail::Gpu_Completion_Service::instance().statistics(); return { {render.capacity, render.admitted, render.peak_admitted, render.queued, render.peak_queued, render.backpressure_count, render.backpressure_wait_ns}, {completion.capacity, completion.in_flight, completion.peak_in_flight, completion.watched, completion.peak_watched, completion.backpressure_count, - completion.backpressure_wait_ns} + completion.backpressure_wait_ns}, + render.unhandled_exception_count }; } @@ -327,6 +351,7 @@ struct Basic_Point_Scene final Node_Execution_Metrics* const metrics = context.metrics; const Node_Diagnostic_Sink diagnostics = context.diagnostics; const bool observe = metrics != nullptr; + const auto state = backend_state; auto source = std::make_shared(); const auto operation = source->operation(); using Pending_Frame = detail::Datoviz_Visual_Backend::Pending_Frame; @@ -340,8 +365,8 @@ struct Basic_Point_Scene final auto async_frame = std::make_shared(); auto render_completion = std::make_shared< detail::Render_Domain::Prepared_Task>( - render_domain->prepare( - [this, async_frame, metrics, diagnostics, source]() mutable { + state->render_domain->prepare( + [state, async_frame, metrics, diagnostics, source]() mutable { if (!async_frame->pending) { static_cast(source->fail( std::make_exception_ptr(std::logic_error( @@ -354,7 +379,7 @@ struct Basic_Point_Scene final async_frame->completion.wait_duration_ns; if (async_frame->completion.error) { try { - backend->discard(std::move(pending)); + state->backend->discard(std::move(pending)); static_cast(source->fail( std::move(async_frame->completion.error))); } catch (...) { @@ -364,23 +389,26 @@ struct Basic_Point_Scene final return; } try { - auto completed = backend->collect( + auto completed = state->backend->collect( std::move(pending)); auto frame = std::move(completed.frame); - if (metrics && frame) { - metrics->set( - Node_Metric_Kind::pixel_count, - static_cast( - frame->extent.width) * - frame->extent.height); - } - publish_trace(metrics, diagnostics, - std::move(completed.trace)); - { - std::lock_guard lock(frame_mutex); - latest = std::move(frame); - } - static_cast(source->complete()); + auto trace = std::make_shared( + std::move(completed.trace)); + static_cast(source->complete( + [state, metrics, diagnostics, frame = std::move(frame), + trace = std::move(trace)]() mutable { + if (metrics && frame) { + metrics->set( + Node_Metric_Kind::pixel_count, + static_cast( + frame->extent.width) * + frame->extent.height); + } + publish_trace(metrics, diagnostics, + std::move(*trace)); + std::lock_guard lock(state->frame_mutex); + state->latest = std::move(frame); + })); } catch (...) { static_cast(source->fail( std::current_exception())); @@ -389,10 +417,11 @@ struct Basic_Point_Scene final auto completion = std::make_shared< detail::Gpu_Completion_Service::Reservation>( detail::Gpu_Completion_Service::instance().prepare( - [this, async_frame, render_completion]( + [state, async_frame, render_completion]( Completion_Result result) noexcept { async_frame->completion = std::move(result); - render_domain->post(std::move(*render_completion)); + state->render_domain->post( + std::move(*render_completion)); }, observe)); const auto queued_at = observe @@ -400,11 +429,13 @@ struct Basic_Point_Scene final : std::chrono::steady_clock::time_point{}; try { - render_domain->post( - [this, scene_state, scene_revision, frame_sequence, prepared, - source, async_frame, completion, observe, - queued_at] { + state->render_domain->post( + [state, scene_state, scene_revision, frame_sequence, prepared, + source, async_frame, completion, observe, queued_at] { try { + if (source->operation().status() == + External_Operation_Status::cancelled) + return; std::uint64_t queue_wait_ns{}; if (observe) { const auto queue_wait = @@ -416,15 +447,14 @@ struct Basic_Point_Scene final ? static_cast(queue_wait) : 0; } - auto pending = backend->submit( + auto pending = state->backend->submit( scene_state, scene_revision, *prepared, frame_sequence, observe); if (!pending) { - { - std::lock_guard lock(frame_mutex); - latest.reset(); - } - static_cast(source->complete()); + static_cast(source->complete([state] { + std::lock_guard lock(state->frame_mutex); + state->latest.reset(); + })); return; } pending->trace.render_domain_queue_wait_ns = @@ -445,10 +475,7 @@ struct Basic_Point_Scene final std::shared_ptr visual; Renderable_Id point_id{}; - std::shared_ptr render_domain; - std::unique_ptr backend; - mutable std::mutex frame_mutex; - std::shared_ptr latest; + std::shared_ptr backend_state; }; std::unique_ptr make_scene_model( diff --git a/render_3D/render_3D/Point_Scene.h b/render_3D/render_3D/Point_Scene.h index 70ce3a9..af0e1ef 100644 --- a/render_3D/render_3D/Point_Scene.h +++ b/render_3D/render_3D/Point_Scene.h @@ -81,6 +81,7 @@ struct Runtime_Admission_Statistics { struct Runtime_Statistics { Runtime_Admission_Statistics render_domain; Runtime_Admission_Statistics gpu_completion; + std::uint64_t render_domain_unhandled_exception_count{}; }; struct Frame_Status { diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp index 8d83ecf..2481d00 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp @@ -186,6 +186,37 @@ void Gpu_Completion_Service::run() noexcept { } if (stopping_.load(std::memory_order_acquire) && active.empty() && pending_.empty()) return; + // Always probe every watched fence before entering a timed wait. The + // timed wait still prevents busy-spinning, while this full probe keeps + // completion latency bounded by one wait quantum instead of one quantum + // per active device group. + bool completed_any = false; + for (auto iterator = active.begin(); iterator != active.end();) { + VkDevice device{VK_NULL_HANDLE}; + VkFence fence{VK_NULL_HANDLE}; + Pending_Fence::Status status; + { + std::lock_guard lock((*iterator)->mutex); + status = (*iterator)->status; + device = (*iterator)->device; + fence = (*iterator)->fence; + } + if (status != Pending_Fence::Status::watched) { + ++iterator; + continue; + } + const VkResult result = vkGetFenceStatus(device, fence); + if (result == VK_NOT_READY) { + ++iterator; + continue; + } + auto pending = *iterator; + iterator = active.erase(iterator); + finish(pending, result); + completed_any = true; + } + if (completed_any) + continue; if (groups.empty()) { std::unique_lock lock(wait_mutex_); if (wake_generation_.load(std::memory_order_acquire) == wake_generation) { diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp index cca51be..9521a3e 100644 --- a/render_3D/render_3D/detail/Render_Domain.cpp +++ b/render_3D/render_3D/detail/Render_Domain.cpp @@ -38,7 +38,8 @@ std::shared_ptr Render_Domain::acquire(std::uint32_t gpu_index) { auto& entry = storage.domains[gpu_index]; if (auto domain = entry.lock()) return domain; - auto domain = std::shared_ptr(new Render_Domain()); + auto domain = std::shared_ptr(new Render_Domain(), + &Render_Domain::destroy); entry = domain; return domain; } @@ -47,12 +48,41 @@ Render_Domain::Render_Domain() { thread_ = std::thread([this] { run(); }); } Render_Domain::~Render_Domain() { - stopping_.store(true, std::memory_order_release); + request_stop(); + if (thread_.joinable()) + thread_.join(); +} +void Render_Domain::destroy(Render_Domain* domain) noexcept { + if (!domain) + return; + if (current_domain_ != domain) { + delete domain; + return; + } + + // The final shared owner can legitimately be a task capture that is + // released on the affinity thread. Stop the loop while the object is still + // alive, then hand joining/deletion to a helper so the domain never joins + // itself. If helper creation fails, the stopped object is intentionally + // leaked rather than terminating or accessing it after destruction. + domain->request_stop(); + try { + std::thread([domain] { + if (domain->thread_.joinable()) + domain->thread_.join(); + delete domain; + }).detach(); + } catch (...) { + } +} +void Render_Domain::request_stop() noexcept { + bool expected = false; + if (!stopping_.compare_exchange_strong( + expected, true, std::memory_order_acq_rel)) + return; slots_.acquire(); if (!tasks_.try_push(std::unique_ptr{})) std::terminate(); - if (thread_.joinable()) - thread_.join(); } void Render_Domain::update_peak(std::atomic_size_t& peak, std::size_t value) noexcept { std::size_t current = peak.load(std::memory_order_relaxed); @@ -108,7 +138,8 @@ Render_Domain::Statistics Render_Domain::statistics() const noexcept { queued_.load(std::memory_order_relaxed), peak_queued_.load(std::memory_order_relaxed), backpressure_count_.load(std::memory_order_relaxed), - backpressure_wait_ns_.load(std::memory_order_relaxed) + backpressure_wait_ns_.load(std::memory_order_relaxed), + unhandled_exception_count_.load(std::memory_order_relaxed) }; } void Render_Domain::run() { @@ -123,7 +154,11 @@ void Render_Domain::run() { } queued_.fetch_sub(1, std::memory_order_relaxed); release_admission(); - task->function(); + try { + task->function(); + } catch (...) { + unhandled_exception_count_.fetch_add(1, std::memory_order_relaxed); + } } } } diff --git a/render_3D/render_3D/detail/Render_Domain.h b/render_3D/render_3D/detail/Render_Domain.h index 0ea15cb..cda42fd 100644 --- a/render_3D/render_3D/detail/Render_Domain.h +++ b/render_3D/render_3D/detail/Render_Domain.h @@ -27,6 +27,7 @@ public: std::size_t peak_queued{}; std::uint64_t backpressure_count{}; std::uint64_t backpressure_wait_ns{}; + std::uint64_t unhandled_exception_count{}; }; class Prepared_Task final { public: @@ -73,6 +74,8 @@ public: [[nodiscard]] Statistics statistics() const noexcept; private: Render_Domain(); + static void destroy(Render_Domain* domain) noexcept; + void request_stop() noexcept; static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept; void acquire_admission(); void release_admission() noexcept; @@ -87,6 +90,7 @@ private: std::atomic_size_t peak_queued_{}; std::atomic_uint64_t backpressure_count_{}; std::atomic_uint64_t backpressure_wait_ns_{}; + std::atomic_uint64_t unhandled_exception_count_{}; std::atomic_bool stopping_{}; std::thread thread_; }; diff --git a/render_3D/tests/Render_Domain_Tests.cpp b/render_3D/tests/Render_Domain_Tests.cpp index cdd53d7..df54e3d 100644 --- a/render_3D/tests/Render_Domain_Tests.cpp +++ b/render_3D/tests/Render_Domain_Tests.cpp @@ -1,6 +1,10 @@ #include "render_3D/detail/Render_Domain.h" #include #include +#include +#include +#include +#include #include namespace renderive::render_3d::detail { namespace { @@ -43,5 +47,29 @@ TEST(RenderDomain, ReportsBoundedAdmissionStatistics) { EXPECT_TRUE(statistics.admitted <= statistics.capacity); EXPECT_TRUE(statistics.queued <= statistics.capacity); } +TEST(RenderDomain, ContainsUnhandledFireAndForgetExceptionsAndKeepsRunning) { + auto domain = Render_Domain::acquire(0x7ffffff6U); + const auto before = domain->statistics().unhandled_exception_count; + domain->post([] { throw std::runtime_error("unexpected render-domain failure"); }); + domain->invoke([] {}); + const auto after = domain->statistics().unhandled_exception_count; + EXPECT_EQ(after, before + 1U); + EXPECT_EQ(domain->invoke([] { return 17; }), 17); +} + +TEST(RenderDomain, FinalOwnerMayBeReleasedOnAffinityThread) { + auto domain = Render_Domain::acquire(0x7ffffff5U); + std::weak_ptr weak = domain; + std::promise released; + auto finished = released.get_future(); + domain->post([owned = domain, &released]() mutable { + owned.reset(); + released.set_value(); + }); + domain.reset(); + EXPECT_EQ(finished.wait_for(std::chrono::seconds(1)), + std::future_status::ready); + EXPECT_TRUE(weak.expired()); +} } } diff --git a/web_server/app/Gallery_Capture_Json.h b/web_server/app/Gallery_Capture_Json.h index ef26726..c098c89 100644 --- a/web_server/app/Gallery_Capture_Json.h +++ b/web_server/app/Gallery_Capture_Json.h @@ -48,6 +48,8 @@ inline const char* execution_status(Node_Execution_Status status) noexcept { return "complete"; case Node_Execution_Status::failed: return "failed"; + case Node_Execution_Status::cancelled: + return "cancelled"; } return "pending"; } @@ -188,7 +190,12 @@ inline Json node_analysis_json(const Node_Frame_Analysis& node) { {"node_id", node.node_id}, {"duration_ns", node.duration_ns}, {"cpu_duration_ns", node.cpu_duration_ns}, + {"render_domain_cpu_duration_ns", node.render_domain_cpu_duration_ns}, + {"total_cpu_work_duration_ns", node.total_cpu_work_duration_ns}, {"external_duration_ns", node.external_duration_ns}, + {"render_domain_queue_wait_ns", node.render_domain_queue_wait_ns}, + {"gpu_completion_wait_ns", node.gpu_completion_wait_ns}, + {"gpu_execution_duration_ns", node.gpu_execution_duration_ns}, {"start_offset_ns", node.start_offset_ns}, {"end_offset_ns", node.end_offset_ns}, {"dependency_ready_time_ns", node.dependency_ready_time_ns}, @@ -223,11 +230,25 @@ inline Json frame_json(const Captured_Frame& frame) { {"total_render_duration_ns", frame.analysis.total_render_duration_ns}, {"critical_path_duration_ns", frame.analysis.critical_path_duration_ns}, {"total_work_duration_ns", frame.analysis.total_work_duration_ns}, + {"scheduler_work_duration_ns", frame.analysis.scheduler_work_duration_ns}, + {"render_domain_work_duration_ns", + frame.analysis.render_domain_work_duration_ns}, {"total_external_duration_ns", frame.analysis.total_external_duration_ns}, - {"parallel_overlap_ns", frame.analysis.parallel_overlap_ns}, - {"peak_parallelism", frame.analysis.peak_parallelism}, - {"average_parallelism", frame.analysis.average_parallelism}, + {"total_render_domain_queue_wait_ns", + frame.analysis.total_render_domain_queue_wait_ns}, + {"total_gpu_completion_wait_ns", + frame.analysis.total_gpu_completion_wait_ns}, + {"total_gpu_execution_duration_ns", + frame.analysis.total_gpu_execution_duration_ns}, + {"scheduler_parallel_overlap_ns", + frame.analysis.scheduler_parallel_overlap_ns}, + {"scheduler_peak_parallelism", + frame.analysis.scheduler_peak_parallelism}, + {"average_cpu_concurrency", frame.analysis.average_cpu_concurrency}, + {"scheduler_average_parallelism", + frame.analysis.scheduler_average_parallelism}, + {"render_domain_utilization", frame.analysis.render_domain_utilization}, {"critical_path", frame.analysis.critical_path}, {"bottleneck_nodes", frame.analysis.bottleneck_nodes}, {"nodes", std::move(nodes)}, @@ -248,9 +269,18 @@ inline Json node_statistics_json(const Node_Statistics& statistics) { {"minimum_ns", statistics.minimum_ns}, {"maximum_ns", statistics.maximum_ns}, {"average_cpu_ns", statistics.average_cpu_ns}, + {"average_render_domain_cpu_ns", statistics.average_render_domain_cpu_ns}, + {"average_total_cpu_work_ns", statistics.average_total_cpu_work_ns}, {"average_external_ns", statistics.average_external_ns}, + {"average_render_domain_queue_wait_ns", + statistics.average_render_domain_queue_wait_ns}, + {"average_gpu_completion_wait_ns", + statistics.average_gpu_completion_wait_ns}, + {"average_gpu_execution_ns", statistics.average_gpu_execution_ns}, {"p95_cpu_ns", statistics.p95_cpu_ns}, + {"p95_render_domain_cpu_ns", statistics.p95_render_domain_cpu_ns}, {"p95_external_ns", statistics.p95_external_ns}, + {"p95_gpu_execution_ns", statistics.p95_gpu_execution_ns}, {"critical_path_frequency", statistics.critical_path_frequency}, {"average_scheduler_wait_ns", statistics.average_scheduler_wait_ns} }; @@ -267,8 +297,11 @@ inline Json plan_statistics_json(const Plan_Version_Statistics& statistics) { {"render_p50_ns", statistics.render_p50_ns}, {"render_p95_ns", statistics.render_p95_ns}, {"render_maximum_ns", statistics.render_maximum_ns}, - {"average_parallelism", statistics.average_parallelism}, - {"peak_parallelism", statistics.peak_parallelism}, + {"average_cpu_concurrency", statistics.average_cpu_concurrency}, + {"scheduler_average_parallelism", + statistics.scheduler_average_parallelism}, + {"render_domain_utilization", statistics.render_domain_utilization}, + {"scheduler_peak_parallelism", statistics.scheduler_peak_parallelism}, {"scheduler_wait_average_ns", statistics.scheduler_wait_average_ns}, {"scheduler_wait_p95_ns", statistics.scheduler_wait_p95_ns}, {"nodes", std::move(nodes)} @@ -291,12 +324,17 @@ inline Json session_summary_json(const Capture_Session& session) { std::vector durations; std::vector waits; std::map critical_frequency; - double parallelism{}; - std::size_t peak{}; + double cpu_concurrency{}; + double scheduler_parallelism{}; + double render_domain_utilization{}; + std::size_t scheduler_peak{}; for (const auto& frame : session.frames) { durations.push_back(frame.analysis.total_render_duration_ns); - parallelism += frame.analysis.average_parallelism; - peak = std::max(peak, frame.analysis.peak_parallelism); + cpu_concurrency += frame.analysis.average_cpu_concurrency; + scheduler_parallelism += frame.analysis.scheduler_average_parallelism; + render_domain_utilization += frame.analysis.render_domain_utilization; + scheduler_peak = std::max( + scheduler_peak, frame.analysis.scheduler_peak_parallelism); for (const auto& node : frame.analysis.nodes) waits.push_back(node.scheduler_wait_ns); for (Render_Node_Id node_id : frame.analysis.critical_path) @@ -313,8 +351,13 @@ inline Json session_summary_json(const Capture_Session& session) { {"render_p50_ns", percentile(durations, 0.50)}, {"render_p95_ns", percentile(durations, 0.95)}, {"render_maximum_ns", durations.empty() ? 0 : *std::max_element(durations.begin(), durations.end())}, - {"average_parallelism", session.frames.empty() ? 0.0 : parallelism / session.frames.size()}, - {"peak_parallelism", peak}, + {"average_cpu_concurrency", + session.frames.empty() ? 0.0 : cpu_concurrency / session.frames.size()}, + {"scheduler_average_parallelism", + session.frames.empty() ? 0.0 : scheduler_parallelism / session.frames.size()}, + {"render_domain_utilization", + session.frames.empty() ? 0.0 : render_domain_utilization / session.frames.size()}, + {"scheduler_peak_parallelism", scheduler_peak}, {"scheduler_wait_average_ns", waits.empty() ? 0.0 : wait_sum / waits.size()}, {"scheduler_wait_p95_ns", percentile(waits, 0.95)}, {"critical_path_frequency", std::move(critical)} diff --git a/web_server/app/render_3D/Gallery_Scene3D.cpp b/web_server/app/render_3D/Gallery_Scene3D.cpp index 077a2bd..60bbbcb 100644 --- a/web_server/app/render_3D/Gallery_Scene3D.cpp +++ b/web_server/app/render_3D/Gallery_Scene3D.cpp @@ -480,7 +480,9 @@ public: {"queued", runtime.render_domain.queued}, {"peak_queued", runtime.render_domain.peak_queued}, {"backpressure_count", runtime.render_domain.backpressure_count}, - {"backpressure_wait_ns", runtime.render_domain.backpressure_wait_ns}}}, + {"backpressure_wait_ns", runtime.render_domain.backpressure_wait_ns}, + {"unhandled_exception_count", + runtime.render_domain_unhandled_exception_count}}}, {"gpu_completion", {{"capacity", runtime.gpu_completion.capacity}, {"active", runtime.gpu_completion.active}, diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index b08d720..54d84e4 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -535,7 +535,7 @@ TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistic EXPECT_FALSE(captured.at("active")); ASSERT_EQ(captured.at("frames").size(), 2U); EXPECT_TRUE(captured.at("summary").contains("render_p95_ns")); - EXPECT_TRUE(captured.at("summary").contains("average_parallelism")); + EXPECT_TRUE(captured.at("summary").contains("average_cpu_concurrency")); EXPECT_TRUE(captured.at("summary").contains("scheduler_wait_p95_ns")); EXPECT_FALSE(captured.at("node_statistics").empty()); EXPECT_FALSE(captured.at("plan_statistics").empty()); @@ -555,7 +555,12 @@ TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistic EXPECT_TRUE(execution.contains("attachments")); const auto& analysis = frame.at("analysis"); EXPECT_TRUE(analysis.contains("critical_path")); - EXPECT_TRUE(analysis.contains("parallel_overlap_ns")); + EXPECT_TRUE(analysis.contains("scheduler_parallel_overlap_ns")); + EXPECT_TRUE(analysis.contains("scheduler_work_duration_ns")); + EXPECT_TRUE(analysis.contains("render_domain_work_duration_ns")); + EXPECT_TRUE(analysis.contains("total_gpu_execution_duration_ns")); + EXPECT_TRUE(analysis.contains("average_cpu_concurrency")); + EXPECT_TRUE(analysis.contains("render_domain_utilization")); EXPECT_TRUE(analysis.contains("workers")); const auto version = frame.at("render_plan_version"); EXPECT_TRUE(std::any_of(capture.at("plans").begin(), capture.at("plans").end(), @@ -573,7 +578,10 @@ TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistic EXPECT_TRUE(statistic.contains("p95_ns")); EXPECT_TRUE(statistic.contains("p99_ns")); EXPECT_TRUE(statistic.contains("average_cpu_ns")); + EXPECT_TRUE(statistic.contains("average_render_domain_cpu_ns")); + EXPECT_TRUE(statistic.contains("average_total_cpu_work_ns")); EXPECT_TRUE(statistic.contains("average_external_ns")); + EXPECT_TRUE(statistic.contains("average_gpu_execution_ns")); EXPECT_TRUE(statistic.contains("p95_cpu_ns")); EXPECT_TRUE(statistic.contains("p95_external_ns")); EXPECT_TRUE(statistic.contains("critical_path_frequency")); diff --git a/webapp_gallery/src/protocol/gallery_types.ts b/webapp_gallery/src/protocol/gallery_types.ts index 273da66..3f4c308 100644 --- a/webapp_gallery/src/protocol/gallery_types.ts +++ b/webapp_gallery/src/protocol/gallery_types.ts @@ -35,11 +35,13 @@ export interface Gallery_Render_Edge {from: number | string; to: number | string export interface Gallery_Renderable_Cache {owner_id: number; name: string; prepare_cache: string; paint_cache: string;} export interface Gallery_Render_Plan {version: number; nodes: Gallery_Render_Node[]; edges: Gallery_Render_Edge[]; renderables: Gallery_Renderable_Cache[];} export interface Gallery_Node_Diagnostic_Attachment {type: string; content: string;} -export interface Gallery_Node_Execution {node_id: number; worker_id: number; start_offset_ns: number; cpu_end_offset_ns: number; external_start_offset_ns: number; external_end_offset_ns: number; end_offset_ns: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; status: "pending"|"ready"|"running"|"waiting_external"|"complete"|"failed"; metrics?: Record; attachments: Gallery_Node_Diagnostic_Attachment[];} -export interface Gallery_Node_Analysis {node_id: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; scheduler_wait_ns: number; critical_path_contribution: number; on_critical_path: boolean;} -export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: {total_work_duration_ns?: number; total_external_duration_ns?: number; nodes: Gallery_Node_Analysis[]};} -export interface Gallery_Node_Statistics {node_id: number; moving_average_ns: number; p95_ns: number; p99_ns: number; average_cpu_ns: number; average_external_ns: number; p95_cpu_ns: number; p95_external_ns: number; critical_path_frequency: number;} -export interface Gallery_Plan_Statistics {render_plan_version: number; frame_count: number; render_average_ns: number; render_p95_ns: number; average_parallelism: number; peak_parallelism: number; scheduler_wait_average_ns: number; nodes: Gallery_Node_Statistics[];} +export interface Gallery_Node_Execution {node_id: number; worker_id: number; start_offset_ns: number; cpu_end_offset_ns: number; external_start_offset_ns: number; external_end_offset_ns: number; end_offset_ns: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; status: "pending"|"ready"|"running"|"waiting_external"|"complete"|"failed"|"cancelled"; metrics?: Record; attachments: Gallery_Node_Diagnostic_Attachment[];} +export interface Gallery_Node_Analysis {node_id: number; duration_ns: number; cpu_duration_ns: number; render_domain_cpu_duration_ns: number; total_cpu_work_duration_ns: number; external_duration_ns: number; render_domain_queue_wait_ns: number; gpu_completion_wait_ns: number; gpu_execution_duration_ns: number; start_offset_ns: number; end_offset_ns: number; dependency_ready_time_ns: number; scheduler_wait_ns: number; work_contribution: number; critical_path_contribution: number; on_critical_path: boolean;} +export interface Gallery_Worker_Analysis {worker_id: number; work_duration_ns: number; utilization: number;} +export interface Gallery_Frame_Analysis {total_render_duration_ns: number; critical_path_duration_ns: number; total_work_duration_ns: number; scheduler_work_duration_ns: number; render_domain_work_duration_ns: number; total_external_duration_ns: number; total_render_domain_queue_wait_ns: number; total_gpu_completion_wait_ns: number; total_gpu_execution_duration_ns: number; scheduler_parallel_overlap_ns: number; scheduler_peak_parallelism: number; average_cpu_concurrency: number; scheduler_average_parallelism: number; render_domain_utilization: number; critical_path: number[]; bottleneck_nodes: number[]; nodes: Gallery_Node_Analysis[]; workers: Gallery_Worker_Analysis[];} +export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_start_ns: number; render_end_ns: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: Gallery_Frame_Analysis;} +export interface Gallery_Node_Statistics {node_id: number; execution_count: number; average_ns: number; moving_average_ns: number; p50_ns: number; p95_ns: number; p99_ns: number; minimum_ns: number; maximum_ns: number; average_cpu_ns: number; average_render_domain_cpu_ns: number; average_total_cpu_work_ns: number; average_external_ns: number; average_render_domain_queue_wait_ns: number; average_gpu_completion_wait_ns: number; average_gpu_execution_ns: number; p95_cpu_ns: number; p95_render_domain_cpu_ns: number; p95_external_ns: number; p95_gpu_execution_ns: number; critical_path_frequency: number; average_scheduler_wait_ns: number;} +export interface Gallery_Plan_Statistics {render_plan_version: number; frame_count: number; render_average_ns: number; render_p50_ns: number; render_p95_ns: number; render_maximum_ns: number; average_cpu_concurrency: number; scheduler_average_parallelism: number; render_domain_utilization: number; scheduler_peak_parallelism: number; scheduler_wait_average_ns: number; scheduler_wait_p95_ns: number; nodes: Gallery_Node_Statistics[];} export interface Gallery_Capture_Session {session_id: number; active: boolean; requested_count: number; captured_count: number; frames: Gallery_Captured_Frame[]; node_statistics: Gallery_Node_Statistics[]; plan_statistics: Gallery_Plan_Statistics[]; summary?: Record;} export interface Gallery_Performance_Capture {controller: {enabled?: boolean; session_id?: number}; sessions: Gallery_Capture_Session[]; plans: Gallery_Render_Plan[];} diff --git a/webapp_gallery/tests/dag/dag_model.test.ts b/webapp_gallery/tests/dag/dag_model.test.ts index 2163809..f635b9f 100644 --- a/webapp_gallery/tests/dag/dag_model.test.ts +++ b/webapp_gallery/tests/dag/dag_model.test.ts @@ -1,2 +1,86 @@ -import {describe,expect,it} from "vitest";import {build_dag_model} from "../../src/dag/dag_model"; -describe("DAG view model",()=>{it("uses node_id for current plan and capture overlays",()=>{const plan={version:2,nodes:[{node_id:7,name:"paint",owner:"plot",kind:"paint"}],edges:[],renderables:[]};const frame={frame_id:1,render_plan_version:2,render_duration_ns:10,node_executions:[{node_id:7,worker_id:3,start_offset_ns:0,cpu_end_offset_ns:10,external_start_offset_ns:0,external_end_offset_ns:0,end_offset_ns:10,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,status:"complete" as const,attachments:[]}],analysis:{total_work_duration_ns:10,total_external_duration_ns:0,nodes:[{node_id:7,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,scheduler_wait_ns:1,critical_path_contribution:1,on_critical_path:true}]}};const model=build_dag_model(plan,frame,[],7);expect(model.nodes[0].id).toBe("7");expect(model.nodes[0].data).toMatchObject({duration_ns:10,worker_id:3,critical:true,selected:true});});}); +import {describe, expect, it} from "vitest"; +import {build_dag_model} from "../../src/dag/dag_model"; +import type {Gallery_Captured_Frame} from "../../src/protocol/gallery_types"; + +describe("DAG view model", () => { + it("uses node_id for current plan and capture overlays", () => { + const plan = { + version: 2, + nodes: [{node_id: 7, name: "paint", owner: "plot", kind: "paint"}], + edges: [], + renderables: [], + }; + + const frame: Gallery_Captured_Frame = { + frame_id: 1, + render_plan_version: 2, + render_start_ns: 100, + render_end_ns: 110, + render_duration_ns: 10, + node_executions: [{ + node_id: 7, + worker_id: 3, + start_offset_ns: 0, + cpu_end_offset_ns: 10, + external_start_offset_ns: 0, + external_end_offset_ns: 0, + end_offset_ns: 10, + duration_ns: 10, + cpu_duration_ns: 10, + external_duration_ns: 0, + status: "complete", + attachments: [], + }], + analysis: { + total_render_duration_ns: 10, + critical_path_duration_ns: 10, + total_work_duration_ns: 10, + scheduler_work_duration_ns: 10, + render_domain_work_duration_ns: 0, + total_external_duration_ns: 0, + total_render_domain_queue_wait_ns: 0, + total_gpu_completion_wait_ns: 0, + total_gpu_execution_duration_ns: 0, + scheduler_parallel_overlap_ns: 0, + scheduler_peak_parallelism: 1, + average_cpu_concurrency: 1, + scheduler_average_parallelism: 1, + render_domain_utilization: 0, + critical_path: [7], + bottleneck_nodes: [7], + nodes: [{ + node_id: 7, + duration_ns: 10, + cpu_duration_ns: 10, + render_domain_cpu_duration_ns: 0, + total_cpu_work_duration_ns: 10, + external_duration_ns: 0, + render_domain_queue_wait_ns: 0, + gpu_completion_wait_ns: 0, + gpu_execution_duration_ns: 0, + start_offset_ns: 0, + end_offset_ns: 10, + dependency_ready_time_ns: 0, + scheduler_wait_ns: 0, + work_contribution: 1, + critical_path_contribution: 1, + on_critical_path: true, + }], + workers: [{ + worker_id: 3, + work_duration_ns: 10, + utilization: 1, + }], + }, + }; + + const model = build_dag_model(plan, frame, [], 7); + expect(model.nodes[0].id).toBe("7"); + expect(model.nodes[0].data).toMatchObject({ + duration_ns: 10, + worker_id: 3, + critical: true, + selected: true, + }); + }); +});