diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index 11c1a23..065a873 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -58,11 +58,9 @@ if (RENDERIVE_BUILD_TESTS) continue() endif () file(RELATIVE_PATH Renderive_Kernel_test_name "${Renderive_Kernel_test_dir}" "${Renderive_Kernel_test_source}") - string(MD5 Renderive_Kernel_test_hash "${Renderive_Kernel_test_name}") - string(SUBSTRING "${Renderive_Kernel_test_hash}" 0 8 Renderive_Kernel_test_hash) - get_filename_component(Renderive_Kernel_test_name "${Renderive_Kernel_test_source}" NAME_WE) + string(REGEX REPLACE "\\.[^.]+$" "" Renderive_Kernel_test_name "${Renderive_Kernel_test_name}") string(MAKE_C_IDENTIFIER "${Renderive_Kernel_test_name}" Renderive_Kernel_test_name) - set(Renderive_Kernel_test_target "Renderive_Kernel_${Renderive_Kernel_test_name}_${Renderive_Kernel_test_hash}") + set(Renderive_Kernel_test_target "Renderive_Kernel_${Renderive_Kernel_test_name}") add_executable("${Renderive_Kernel_test_target}" "${Renderive_Kernel_test_source}") target_include_directories("${Renderive_Kernel_test_target}" PRIVATE "${Renderive_Kernel_test_dir}" diff --git a/Kernel/src/renderive/base/observer/Observer_State.hpp b/Kernel/src/renderive/base/observer/Observer_State.hpp index 8a838a2..f6b99c4 100644 --- a/Kernel/src/renderive/base/observer/Observer_State.hpp +++ b/Kernel/src/renderive/base/observer/Observer_State.hpp @@ -20,7 +20,7 @@ public: Observer_State(Observer_State&& other) noexcept(std::is_nothrow_move_constructible_v && std::is_nothrow_move_constructible_v && std::is_nothrow_default_constructible_v) requires std::move_constructible && std::move_constructible : observer_(std::move(other.observer_)), time_source_(std::move(other.time_source_)) {} Observer_State& operator=(Observer_State&&) = delete; - std::uint64_t now_ns() const noexcept { + std::uint64_t now_ns() const { std::lock_guard lock(time_source_mutex_); return static_cast(time_source_.now_ns()); } @@ -34,7 +34,7 @@ public: } template requires Struct_Observer - void observe(const Observation& observation) noexcept { + void observe(const Observation& observation) { if constexpr (Observer::enabled) { std::lock_guard lock(mutex_); observer_.observe(observation); diff --git a/Kernel/src/renderive/base/observer/concept/Observer.hpp b/Kernel/src/renderive/base/observer/concept/Observer.hpp index 312c1dc..e18814e 100644 --- a/Kernel/src/renderive/base/observer/concept/Observer.hpp +++ b/Kernel/src/renderive/base/observer/concept/Observer.hpp @@ -11,10 +11,10 @@ concept Observer_Enable_Flag = requires { }; template concept Struct_Observer = Observation_Struct && Observer_Enable_Flag && requires(That& observer, const Observation& observation) { - { observer.observe(observation) } noexcept -> std::same_as; + { observer.observe(observation) } -> std::same_as; }; template concept Timed_Struct_Observer = Struct_Observer && requires(That& observer, const That& const_observer, const Observation& observation) { - { const_observer.now_ns() } noexcept -> std::convertible_to; - { observer.observe(observation) } noexcept -> std::same_as; + { const_observer.now_ns() } -> std::convertible_to; + { observer.observe(observation) } -> std::same_as; }; diff --git a/Kernel/src/renderive/capture/Capture.cpp b/Kernel/src/renderive/capture/Capture.cpp index fb3f41d..1fa77d2 100644 --- a/Kernel/src/renderive/capture/Capture.cpp +++ b/Kernel/src/renderive/capture/Capture.cpp @@ -3,15 +3,14 @@ #include #include -Capture_Session_Id Capture_Controller::capture_next_frame() { +Capture_Request_Result Capture_Controller::capture_next_frame() { return capture_frames(1); } - -Capture_Session_Id Capture_Controller::capture_frames(std::size_t count) { +Capture_Request_Result Capture_Controller::capture_frames(std::size_t count) { if (count == 0) - throw std::invalid_argument("capture frame count must be positive"); + return {0, Capture_Error::frame_count_zero}; if (count > std::numeric_limits::max()) - throw std::invalid_argument("capture frame count is too large"); + return {0, Capture_Error::frame_count_too_large}; Capture_Session_Id id = next_session_id_.load(std::memory_order_relaxed); for (;;) { if (id == 0 || id == std::numeric_limits::max()) @@ -25,15 +24,14 @@ Capture_Session_Id Capture_Controller::capture_frames(std::size_t count) { auto current = request_.load(std::memory_order_acquire); for (;;) { if (current && current->frames.load(std::memory_order_acquire) != 0) - throw std::logic_error("a capture session is already active"); + return {0, Capture_Error::session_active}; if (request_.compare_exchange_weak(current, next, std::memory_order_acq_rel, std::memory_order_acquire)) break; } - return id; + return {id, Capture_Error::none}; } - Capture_Frame_Ticket Capture_Controller::begin_frame() noexcept { const auto request = request_.load(std::memory_order_acquire); if (!request) @@ -67,6 +65,15 @@ void Capture_Controller::finish_frame(Capture_Frame_Ticket ticket, } } +void Capture_Controller::cancel(Capture_Session_Id session_id) noexcept { + auto request = request_.load(std::memory_order_acquire); + while (request && request->session_id == session_id) { + if (request_.compare_exchange_weak(request, {}, + std::memory_order_acq_rel, + std::memory_order_acquire)) + return; + } +} Capture_Controller_State Capture_Controller::state() const noexcept { const auto request = request_.load(std::memory_order_acquire); if (!request) @@ -79,12 +86,12 @@ Capture_Controller_State Capture_Controller::state() const noexcept { void Capture_Repository::begin_session(Capture_Session_Id session_id, std::size_t requested_count) { if (session_id == 0 || requested_count == 0) - throw std::invalid_argument("invalid capture session"); + throw std::logic_error("invalid capture session invariant"); std::lock_guard lock(mutex_); if (std::any_of(sessions_.begin(), sessions_.end(), [session_id](const auto& session) { return session.session_id == session_id; })) - throw std::invalid_argument("capture session already exists"); + throw std::logic_error("capture session already exists"); while (sessions_.size() >= retained_session_count) { const auto completed = std::find_if(sessions_.begin(), sessions_.end(), [](const Capture_Session& session) { return !session.active(); }); @@ -101,16 +108,16 @@ void Capture_Repository::publish(Capture_Session_Id session_id, std::shared_ptr render_plan, Frame_Analysis analysis) { if (!snapshot || !render_plan) - throw std::invalid_argument("captured frame snapshot or render plan is null"); + throw std::logic_error("captured frame snapshot or render plan is null"); if (analysis.frame_id != snapshot->frame_id || analysis.render_plan_version != snapshot->render_plan_version || render_plan->version != snapshot->render_plan_version) - throw std::invalid_argument("captured frame analysis does not match snapshot or render plan"); + throw std::logic_error("captured frame analysis does not match snapshot or render plan"); std::lock_guard lock(mutex_); const auto iterator = std::find_if(sessions_.begin(), sessions_.end(), [session_id](const auto& session) { return session.session_id == session_id; }); if (iterator == sessions_.end()) - throw std::invalid_argument("capture session does not exist"); + throw std::logic_error("capture session does not exist"); if (!iterator->active()) throw std::logic_error("capture session is complete"); iterator->frames.push_back({std::move(snapshot), std::move(render_plan), diff --git a/Kernel/src/renderive/capture/Capture.hpp b/Kernel/src/renderive/capture/Capture.hpp index ee4eca9..e44a902 100644 --- a/Kernel/src/renderive/capture/Capture.hpp +++ b/Kernel/src/renderive/capture/Capture.hpp @@ -12,11 +12,12 @@ class Capture_Controller { public: - Capture_Session_Id capture_next_frame(); - Capture_Session_Id capture_frames(std::size_t count); + [[nodiscard]] Capture_Request_Result capture_next_frame(); + [[nodiscard]] Capture_Request_Result capture_frames(std::size_t count); [[nodiscard]] Capture_Frame_Ticket begin_frame() noexcept; void finish_frame(Capture_Frame_Ticket ticket, bool completed) noexcept; [[nodiscard]] Capture_Controller_State state() const noexcept; + void cancel(Capture_Session_Id session_id) noexcept; private: struct Request { Request(Capture_Session_Id session_id, std::size_t count) diff --git a/Kernel/src/renderive/capture/Capture_Types.hpp b/Kernel/src/renderive/capture/Capture_Types.hpp index 6e0dc58..ad9cef0 100644 --- a/Kernel/src/renderive/capture/Capture_Types.hpp +++ b/Kernel/src/renderive/capture/Capture_Types.hpp @@ -4,7 +4,19 @@ #include using Capture_Session_Id = std::uint64_t; - +enum class Capture_Error : std::uint8_t { + none, + frame_count_zero, + frame_count_too_large, + session_active +}; +struct Capture_Request_Result { + Capture_Session_Id session_id{}; + Capture_Error error{}; + [[nodiscard]] explicit operator bool() const noexcept { + return error == Capture_Error::none; + } +}; struct Capture_Frame_Ticket { Capture_Session_Id session_id{}; bool capture{}; diff --git a/Kernel/src/renderive/frame_control/base/Frame_Control_Strategy_Base.hpp b/Kernel/src/renderive/frame_control/base/Frame_Control_Strategy_Base.hpp index ddf28ca..a6e3550 100644 --- a/Kernel/src/renderive/frame_control/base/Frame_Control_Strategy_Base.hpp +++ b/Kernel/src/renderive/frame_control/base/Frame_Control_Strategy_Base.hpp @@ -1,62 +1,55 @@ #pragma once -#include #include +#include #include #include -#include struct Real_Time_Data_Observation; class Frame_Control_Strategy_Base { public: struct State { double frequency_hz{invalid_frequency_hz()}; std::uint64_t next_refresh_interval_ns{}; - std::uint64_t update_revision{}; std::uint64_t publish_revision{}; }; virtual ~Frame_Control_Strategy_Base() = default; virtual void swap() = 0; - virtual double frequency_hz() const noexcept = 0; - virtual std::uint64_t next_refresh_interval_ns() const noexcept = 0; - virtual State frame_control_state() const = 0; - virtual void on_real_time_data_update(const Real_Time_Data_Observation& observation) noexcept = 0; - virtual bool discard_stale_latest_data_frame() noexcept { + double frequency_hz() const { + std::lock_guard lock(state_mutex_); + return published_state_.frequency_hz; + } + std::uint64_t next_refresh_interval_ns() const { + std::lock_guard lock(state_mutex_); + return published_state_.next_refresh_interval_ns; + } + State frame_control_state() const { + std::lock_guard lock(state_mutex_); + return published_state_; + } + virtual void on_real_time_data_update(const Real_Time_Data_Observation& observation) = 0; + virtual bool discard_stale_latest_data_frame() { return false; } static constexpr double invalid_frequency_hz() noexcept { return std::numeric_limits::quiet_NaN(); } static std::uint64_t frequency_interval_ns(double frequency_hz) noexcept { - if (!std::isfinite(frequency_hz) || frequency_hz <= 0.0) { + if (!std::isfinite(frequency_hz) || frequency_hz <= 0.0) return 0; - } const double interval_ns = 1'000'000'000.0 / frequency_hz; - if (!std::isfinite(interval_ns) || interval_ns >= static_cast(std::numeric_limits::max())) { + if (!std::isfinite(interval_ns) || interval_ns >= static_cast(std::numeric_limits::max())) return std::numeric_limits::max(); - } return static_cast(interval_ns); } protected: explicit Frame_Control_Strategy_Base(double frequency_hz = invalid_frequency_hz(), std::uint64_t next_refresh_interval_ns = 0) - : states_{{frequency_hz, next_refresh_interval_ns, 0, 0}, {frequency_hz, next_refresh_interval_ns, 0, 0}}, render_state_(&states_[0]), cache_state_(&states_[1]) {} - void update_frame_control_state(double frequency_hz, std::uint64_t next_refresh_interval_ns) { - std::lock_guard lock(state_mutex_); - cache_state_->frequency_hz = frequency_hz; - cache_state_->next_refresh_interval_ns = next_refresh_interval_ns; - ++cache_state_->update_revision; - } - void swap_frame_control_state() { - std::lock_guard lock(state_mutex_); - std::swap(render_state_, cache_state_); - ++render_state_->publish_revision; - *cache_state_ = *render_state_; - } - State render_frame_control_state() const { - std::lock_guard lock(state_mutex_); - return *render_state_; + : published_state_{frequency_hz, next_refresh_interval_ns, 0} {} + void publish_frame_control_state(double frequency_hz, std::uint64_t next_refresh_interval_ns) { + std::lock_guard lock(state_mutex_); + published_state_.frequency_hz = frequency_hz; + published_state_.next_refresh_interval_ns = next_refresh_interval_ns; + ++published_state_.publish_revision; } private: - State states_[2]; - State* render_state_; - State* cache_state_; + State published_state_; mutable std::mutex state_mutex_; }; diff --git a/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy_Base.hpp b/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy_Base.hpp index a01ca38..6da8d76 100644 --- a/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy_Base.hpp +++ b/Kernel/src/renderive/frame_control/concept/Frame_Control_Strategy_Base.hpp @@ -5,7 +5,7 @@ template concept Runtime_Frame_Control_Strategy = std::derived_from && requires(That& strategy, const That& const_strategy) { { strategy.swap() } -> std::same_as; - { const_strategy.frequency_hz() } noexcept -> std::same_as; - { const_strategy.next_refresh_interval_ns() } noexcept -> std::same_as; + { const_strategy.frequency_hz() } -> std::same_as; + { const_strategy.next_refresh_interval_ns() } -> std::same_as; { const_strategy.frame_control_state() } -> std::same_as; }; diff --git a/Kernel/src/renderive/frame_control/concept/Frame_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/concept/Frame_Refresh_Strategy.hpp index f7449d9..e3382f7 100644 --- a/Kernel/src/renderive/frame_control/concept/Frame_Refresh_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/concept/Frame_Refresh_Strategy.hpp @@ -5,7 +5,8 @@ template concept Frame_Refresh_Strategy = Frame_Control_Strategy && requires(That& strategy, const That& const_strategy, double frequency_hz) { typename That::State; - { strategy.set_frequency_hz(frequency_hz) } -> std::same_as; + typename That::Control_Error; + { strategy.set_frequency_hz(frequency_hz) } -> std::same_as; { const_strategy.frequency_hz() } -> std::same_as; { const_strategy.state() } -> std::same_as; { const_strategy.next_refresh_interval_ns() } -> std::same_as; diff --git a/Kernel/src/renderive/frame_control/concept/Real_Time_Data_Aware_Strategy.hpp b/Kernel/src/renderive/frame_control/concept/Real_Time_Data_Aware_Strategy.hpp index 820d45f..3f21959 100644 --- a/Kernel/src/renderive/frame_control/concept/Real_Time_Data_Aware_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/concept/Real_Time_Data_Aware_Strategy.hpp @@ -4,5 +4,5 @@ #include "renderive/real_time_data/Observation.hpp" template concept Real_Time_Data_Aware_Frame_Strategy = Frame_Control_Strategy && requires(That& strategy, const Real_Time_Data_Observation& observation) { - { strategy.on_real_time_data_update(observation) } noexcept -> std::same_as; + { strategy.on_real_time_data_update(observation) } -> std::same_as; }; diff --git a/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp index 0b87789..1bdb7e7 100644 --- a/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp @@ -101,15 +101,12 @@ public: Painter_Lease acquire_painter(); Render_Lease acquire_renderer(); void swap() override; - double frequency_hz() const noexcept override; - std::uint64_t next_refresh_interval_ns() const noexcept override; - Frame_Control_Strategy_Base::State frame_control_state() const override; std::size_t pending_frame_count() const; State state() const; - void on_real_time_data_update(const Real_Time_Data_Observation& observation) noexcept override; + void on_real_time_data_update(const Real_Time_Data_Observation& observation) override; private: - std::uint64_t now_ns() const noexcept; - void observe(const Observation& observation) noexcept; + std::uint64_t now_ns() const; + void observe(const Observation& observation); inline static thread_local Flow_Refresh_Strategy* rendering_strategy_{}; std::pmr::memory_resource* const memory_resource_; Observer observer_; diff --git a/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.inl index c32cf85..c821a0a 100644 --- a/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.inl @@ -26,7 +26,6 @@ Flow_Refresh_Strategy::Painter_Lease::~Painter_Lea std::lock_guard lock(strategy_->state_mutex_); ++strategy_->state_.enqueued_frame_count; strategy_->state_.pending_frame_count = static_cast(std::max(0, strategy_->frames_.size())); - strategy_->update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::enqueued, statistics, strategy_->state_, strategy_->last_real_time_data_update_}; } strategy_->observe(observation); @@ -78,12 +77,10 @@ Flow_Refresh_Strategy::Render_Lease::Render_Lease( frame_->statistics.render_begin_time_ns = strategy.now_ns(); ++strategy.state_.dequeued_frame_count; strategy.state_.pending_frame_count = static_cast(std::max(0, strategy.frames_.size())); - strategy.update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::dequeued, frame_->statistics, strategy.state_, strategy.last_real_time_data_update_}; } else { ++strategy.state_.empty_acquire_count; strategy.state_.pending_frame_count = 0; - strategy.update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::queue_empty, {}, strategy.state_, strategy.last_real_time_data_update_}; } } @@ -108,7 +105,6 @@ Flow_Refresh_Strategy::Render_Lease::~Render_Lease std::lock_guard lock(strategy_->state_mutex_); ++strategy_->state_.rendered_frame_count; strategy_->state_.pending_frame_count = static_cast(std::max(0, strategy_->frames_.size())); - strategy_->update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::rendered, frame_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; frame_.reset(); } @@ -164,19 +160,7 @@ auto Flow_Refresh_Strategy::acquire_renderer() -> } template void Flow_Refresh_Strategy::swap() { - swap_frame_control_state(); -} -template -double Flow_Refresh_Strategy::frequency_hz() const noexcept { - return render_frame_control_state().frequency_hz; -} -template -std::uint64_t Flow_Refresh_Strategy::next_refresh_interval_ns() const noexcept { - return render_frame_control_state().next_refresh_interval_ns; -} -template -auto Flow_Refresh_Strategy::frame_control_state() const -> Frame_Control_Strategy_Base::State { - return render_frame_control_state(); + publish_frame_control_state(invalid_frequency_hz(), 0); } template std::size_t Flow_Refresh_Strategy::pending_frame_count() const { @@ -190,23 +174,22 @@ auto Flow_Refresh_Strategy::state() const -> State return result; } template -void Flow_Refresh_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) noexcept { +void Flow_Refresh_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) { Observation strategy_observation; { std::lock_guard lock(state_mutex_); ++real_time_data_update_sequence_; state_.real_time_data_update_sequence = real_time_data_update_sequence_; last_real_time_data_update_ = observation.state; - update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); strategy_observation = {Observation_Event::real_time_data_updated, {}, state_, last_real_time_data_update_}; } observe(strategy_observation); } template -std::uint64_t Flow_Refresh_Strategy::now_ns() const noexcept { +std::uint64_t Flow_Refresh_Strategy::now_ns() const { return observer_.now_ns(); } template -void Flow_Refresh_Strategy::observe(const Observation& observation) noexcept { +void Flow_Refresh_Strategy::observe(const Observation& observation) { observer_.observe(observation); } diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp index 7f74f39..289a7c1 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp @@ -13,6 +13,11 @@ template > class Low_Latency_Strategy : public Frame_Control_Strategy_Base { public: + enum class Control_Error : std::uint8_t { + none, + invalid_frequency, + invalid_consumer_feedback + }; enum class Observation_Event { published, abandoned, @@ -157,28 +162,25 @@ public: Low_Latency_Strategy& operator=(Low_Latency_Strategy&&) = delete; Painter_Lease acquire_painter(); Render_Lease acquire_renderer(); - void set_frequency_hz(double frequency_hz); + [[nodiscard]] Control_Error set_frequency_hz(double frequency_hz); void clear_frequency_limit(); - void set_consumer_feedback(Frame_Consumer_Feedback feedback); + [[nodiscard]] Control_Error set_consumer_feedback(Frame_Consumer_Feedback feedback); void clear_consumer_feedback(); void swap() override; - double frequency_hz() const noexcept override; - std::uint64_t next_refresh_interval_ns() const noexcept override; - Frame_Control_Strategy_Base::State frame_control_state() const override; State state() const; Counter_Statistics counter_statistics() const; std::size_t pending_frame_count() const; - void on_real_time_data_update(const Real_Time_Data_Observation& observation) noexcept override; + void on_real_time_data_update(const Real_Time_Data_Observation& observation) override; bool discard_pending_frame(); bool discard_pending_frame_before(std::uint64_t real_time_data_update_sequence); - bool discard_stale_latest_data_frame() noexcept override; + bool discard_stale_latest_data_frame() override; private: - static double checked_frequency_hz(double frequency_hz); - std::uint64_t now_ns() const noexcept; + static double checked_configuration_frequency(double frequency_hz); + std::uint64_t now_ns() const; void update_refresh_control(); void update_consumer_estimator(std::uint64_t sample_interval_ns); void update_state(const Frame& frame); - void observe(const Observation& observation) noexcept; + void observe(const Observation& observation); bool discard_pending_frame_locked(Observation& observation); Frame frames_[3]; Observer observer_; diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl index 65eaa98..f785ae1 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl @@ -216,7 +216,7 @@ auto Low_Latency_Strategy::Render_Lease::get() con } template Low_Latency_Strategy::Low_Latency_Strategy(Observer observer, Configuration configuration) - : Frame_Control_Strategy_Base(checked_frequency_hz(configuration.frequency_hz), frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]), replace_pending_frame_(configuration.replace_pending_frame) { + : Frame_Control_Strategy_Base(checked_configuration_frequency(configuration.frequency_hz), frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]), replace_pending_frame_(configuration.replace_pending_frame) { state_.frequency_hz = configuration.frequency_hz; state_.target_interval_ns = frequency_interval_ns(configuration.frequency_hz); update_refresh_control(); @@ -230,13 +230,15 @@ auto Low_Latency_Strategy::acquire_renderer() -> R return Render_Lease(*this); } template -void Low_Latency_Strategy::set_frequency_hz(double frequency_hz) { - frequency_hz = checked_frequency_hz(frequency_hz); +auto Low_Latency_Strategy::set_frequency_hz(double frequency_hz) -> Control_Error { + if (!std::isfinite(frequency_hz) || frequency_hz <= 0.0) + return Control_Error::invalid_frequency; std::lock_guard lock(state_mutex_); state_.frequency_limit_enabled = true; state_.frequency_hz = frequency_hz; state_.target_interval_ns = frequency_interval_ns(frequency_hz); update_refresh_control(); + return Control_Error::none; } template void Low_Latency_Strategy::clear_frequency_limit() { @@ -247,10 +249,13 @@ void Low_Latency_Strategy::clear_frequency_limit() update_refresh_control(); } template -void Low_Latency_Strategy::set_consumer_feedback(Frame_Consumer_Feedback feedback) { +auto Low_Latency_Strategy::set_consumer_feedback(Frame_Consumer_Feedback feedback) -> Control_Error { + if (feedback.observed_interval_ns == 0) + return Control_Error::invalid_consumer_feedback; std::lock_guard lock(state_mutex_); update_consumer_estimator(feedback.observed_interval_ns); update_refresh_control(); + return Control_Error::none; } template void Low_Latency_Strategy::clear_consumer_feedback() { @@ -265,19 +270,14 @@ void Low_Latency_Strategy::clear_consumer_feedback } template void Low_Latency_Strategy::swap() { - swap_frame_control_state(); -} -template -double Low_Latency_Strategy::frequency_hz() const noexcept { - return render_frame_control_state().frequency_hz; -} -template -std::uint64_t Low_Latency_Strategy::next_refresh_interval_ns() const noexcept { - return render_frame_control_state().next_refresh_interval_ns; -} -template -auto Low_Latency_Strategy::frame_control_state() const -> Frame_Control_Strategy_Base::State { - return render_frame_control_state(); + double frequency_hz; + std::uint64_t next_refresh_interval_ns; + { + std::lock_guard lock(state_mutex_); + frequency_hz = state_.frequency_hz; + next_refresh_interval_ns = state_.next_refresh_interval_ns; + } + publish_frame_control_state(frequency_hz, next_refresh_interval_ns); } template auto Low_Latency_Strategy::state() const -> State { @@ -295,7 +295,7 @@ std::size_t Low_Latency_Strategy::pending_frame_co return cache_ready_ ? 1U : 0U; } template -void Low_Latency_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) noexcept { +void Low_Latency_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) { Observation strategy_observation; { std::lock_guard lock(state_mutex_); @@ -340,7 +340,7 @@ bool Low_Latency_Strategy::discard_pending_frame_b return discarded; } template -bool Low_Latency_Strategy::discard_stale_latest_data_frame() noexcept { +bool Low_Latency_Strategy::discard_stale_latest_data_frame() { Observation observation; bool discarded{}; { @@ -356,14 +356,13 @@ bool Low_Latency_Strategy::discard_stale_latest_da return discarded; } template -double Low_Latency_Strategy::checked_frequency_hz(double frequency_hz) { - if (!std::isfinite(frequency_hz)) { - throw std::invalid_argument("frequency_hz must be finite"); - } +double Low_Latency_Strategy::checked_configuration_frequency(double frequency_hz) { + if (!std::isfinite(frequency_hz) || frequency_hz <= 0.0) + throw std::invalid_argument("low-latency frame-control configuration frequency must be positive and finite"); return frequency_hz; } template -std::uint64_t Low_Latency_Strategy::now_ns() const noexcept { +std::uint64_t Low_Latency_Strategy::now_ns() const { return observer_.now_ns(); } template @@ -384,7 +383,6 @@ void Low_Latency_Strategy::update_refresh_control( } else { state_.limit_state = Limit_State::unlimited; } - update_frame_control_state(state_.frequency_hz, state_.next_refresh_interval_ns); } template void Low_Latency_Strategy::update_consumer_estimator(std::uint64_t sample_interval_ns) { @@ -426,7 +424,7 @@ void Low_Latency_Strategy::update_state(const Fram state_.completed_frame_real_time_data_update_sequence = frame.statistics.real_time_data_update_sequence; } template -void Low_Latency_Strategy::observe(const Observation& observation) noexcept { +void Low_Latency_Strategy::observe(const Observation& observation) { observer_.observe(observation); } template diff --git a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp index a4eea44..cbda549 100644 --- a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp @@ -96,16 +96,13 @@ public: Painter_Lease acquire_painter(); Render_Lease acquire_renderer(); void swap() override; - double frequency_hz() const noexcept override; - std::uint64_t next_refresh_interval_ns() const noexcept override; - Frame_Control_Strategy_Base::State frame_control_state() const override; bool refresh(); bool discard_pending_frame(); State state() const; - void on_real_time_data_update(const Real_Time_Data_Observation& observation) noexcept override; + void on_real_time_data_update(const Real_Time_Data_Observation& observation) override; private: - std::uint64_t now_ns() const noexcept; - void observe(const Observation& observation) noexcept; + std::uint64_t now_ns() const; + void observe(const Observation& observation); Frame frames_[3]; Observer observer_; Frame* paint_; diff --git a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl index cf117b2..8b6bf2d 100644 --- a/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.inl @@ -32,7 +32,6 @@ Manual_Refresh_Strategy::Painter_Lease::~Painter_L std::swap(strategy_->paint_, strategy_->pending_); strategy_->state_.pending_frame = true; ++strategy_->state_.prepared_frame_count; - strategy_->update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); prepared_observation = {Observation_Event::prepared, frame_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; } lease_lock_.unlock(); @@ -92,7 +91,6 @@ Manual_Refresh_Strategy::Render_Lease::~Render_Lea { std::lock_guard lock(strategy_->state_mutex_); ++strategy_->state_.render_count; - strategy_->update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::rendered, frame_->statistics, strategy_->state_, strategy_->last_real_time_data_update_}; } lease_lock_.unlock(); @@ -139,19 +137,7 @@ auto Manual_Refresh_Strategy::acquire_renderer() - } template void Manual_Refresh_Strategy::swap() { - swap_frame_control_state(); -} -template -double Manual_Refresh_Strategy::frequency_hz() const noexcept { - return render_frame_control_state().frequency_hz; -} -template -std::uint64_t Manual_Refresh_Strategy::next_refresh_interval_ns() const noexcept { - return render_frame_control_state().next_refresh_interval_ns; -} -template -auto Manual_Refresh_Strategy::frame_control_state() const -> Frame_Control_Strategy_Base::State { - return render_frame_control_state(); + publish_frame_control_state(invalid_frequency_hz(), 0); } template bool Manual_Refresh_Strategy::refresh() { @@ -166,12 +152,10 @@ bool Manual_Refresh_Strategy::refresh() { state_.render_frame = true; state_.render_frame_sequence = render_->statistics.sequence; ++state_.successful_refresh_count; - update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::refresh_succeeded, render_->statistics, state_, last_real_time_data_update_}; refreshed = true; } else { ++state_.failed_refresh_count; - update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::refresh_failed, {}, state_, last_real_time_data_update_}; } } @@ -189,7 +173,6 @@ bool Manual_Refresh_Strategy::discard_pending_fram } state_.pending_frame = false; ++state_.discarded_prepared_frame_count; - update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); observation = {Observation_Event::manually_discarded, pending_->statistics, state_, last_real_time_data_update_}; } observe(observation); @@ -201,23 +184,22 @@ auto Manual_Refresh_Strategy::state() const -> Sta return state_; } template -void Manual_Refresh_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) noexcept { +void Manual_Refresh_Strategy::on_real_time_data_update(const Real_Time_Data_Observation& observation) { Observation strategy_observation; { std::lock_guard lock(state_mutex_); ++real_time_data_update_sequence_; state_.real_time_data_update_sequence = real_time_data_update_sequence_; last_real_time_data_update_ = observation.state; - update_frame_control_state(Frame_Control_Strategy_Base::invalid_frequency_hz(), 0); strategy_observation = {Observation_Event::real_time_data_updated, {}, state_, last_real_time_data_update_}; } observe(strategy_observation); } template -std::uint64_t Manual_Refresh_Strategy::now_ns() const noexcept { +std::uint64_t Manual_Refresh_Strategy::now_ns() const { return observer_.now_ns(); } template -void Manual_Refresh_Strategy::observe(const Observation& observation) noexcept { +void Manual_Refresh_Strategy::observe(const Observation& observation) { observer_.observe(observation); } diff --git a/Kernel/src/renderive/real_time_data/Double_Buffer_Frame_Observer.hpp b/Kernel/src/renderive/real_time_data/Double_Buffer_Frame_Observer.hpp index 1681f31..72b8e73 100644 --- a/Kernel/src/renderive/real_time_data/Double_Buffer_Frame_Observer.hpp +++ b/Kernel/src/renderive/real_time_data/Double_Buffer_Frame_Observer.hpp @@ -11,7 +11,7 @@ struct Double_Buffer_Frame_Observer { } template - void observe(const Observation& observation) noexcept { + void observe(const Observation& observation) { if (observation.event != Observation::Event::cache_updated) return; observer_.observe({ diff --git a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.cpp b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.cpp index 5207d02..102bb1d 100644 --- a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.cpp +++ b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.cpp @@ -101,7 +101,7 @@ std::shared_ptr Frame_Strategy_Real_Time_Data_Observer::bind( } void Frame_Strategy_Real_Time_Data_Observer::observe( - const Real_Time_Data_Observation& observation) noexcept { + const Real_Time_Data_Observation& observation) { std::shared_ptr entries; { std::lock_guard lock(state_->mutex); diff --git a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp index 8bbf40a..856f50e 100644 --- a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp +++ b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.hpp @@ -15,7 +15,7 @@ public: Renderable_Base& renderable); std::shared_ptr bind(Renderable_Base& renderable); - void observe(const Real_Time_Data_Observation& observation) noexcept; + void observe(const Real_Time_Data_Observation& observation); private: struct State; diff --git a/Kernel/src/renderive/render_graph/External_Operation.cpp b/Kernel/src/renderive/render_graph/External_Operation.cpp index 1bb7f85..229ca16 100644 --- a/Kernel/src/renderive/render_graph/External_Operation.cpp +++ b/Kernel/src/renderive/render_graph/External_Operation.cpp @@ -1,26 +1,22 @@ #include "External_Operation.hpp" - #include #include -#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{}; { @@ -32,8 +28,7 @@ public: condition_.notify_one(); return ticket; } - - void cancel(Ticket ticket) noexcept { + void cancel(Ticket ticket) { if (ticket == 0) return; bool notify{}; @@ -49,14 +44,12 @@ public: if (notify) condition_.notify_one(); } - private: struct Entry { Ticket ticket{}; Callback callback; }; using Entries = std::multimap; - Deadline_Service() : thread_([this] { run(); }) {} ~Deadline_Service() { { @@ -67,44 +60,45 @@ private: 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; + try { + 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(); } - 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(); + } catch (...) { } } - std::mutex mutex_; std::condition_variable condition_; Entries entries_; @@ -113,31 +107,17 @@ private: 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(); - } +External_Operation_Error checked_cancellation_error(External_Operation_Error error) { + if (error != External_Operation_Error::cancelled && + error != External_Operation_Error::deadline_exceeded) + throw std::invalid_argument("external operation cancellation error is invalid"); + return error; } - -std::exception_ptr deadline_error() noexcept { - try { - throw External_Operation_Deadline_Exceeded(); - } catch (...) { - return std::current_exception(); - } } - -} // namespace - struct External_Operation::State { - bool finish(External_Operation_Status terminal, - std::exception_ptr terminal_error, - std::function commit = {}) noexcept { + bool finish(External_Operation_Status terminal, External_Operation_Error terminal_error, + std::exception_ptr terminal_exception, + std::function commit = {}) { Deadline_Service::Ticket deadline_ticket{}; { std::lock_guard lock(mutex); @@ -147,134 +127,115 @@ struct External_Operation::State { 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(); + terminal_error = External_Operation_Error::none; + terminal_exception = std::current_exception(); } } - Completion callback; - std::exception_ptr callback_error; + External_Operation_Completion result; { std::lock_guard lock(mutex); status = terminal; - error = std::move(terminal_error); - callback_error = error; + error = terminal_error; + exception = std::move(terminal_exception); + result = {error, exception}; callback = std::move(completion); } + if (deadline_ticket != 0) + Deadline_Service::instance().cancel(deadline_ticket); if (callback) { try { - callback(std::move(callback_error)); + callback(std::move(result)); } catch (...) { } } return true; } - std::mutex mutex; Completion completion; - std::exception_ptr error; + External_Operation_Error error{}; + std::exception_ptr exception; 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)) {} - void External_Operation::on_complete(Completion completion) const { if (!state_) throw std::logic_error("external operation is empty"); if (!completion) throw std::invalid_argument("external operation completion is empty"); - - std::exception_ptr error; + External_Operation_Completion result; bool invoke{}; { std::lock_guard lock(state_->mutex); if (state_->subscribed) - throw std::logic_error( - "external operation already has a completion subscriber"); + throw std::logic_error("external operation already has a completion subscriber"); state_->subscribed = true; if (state_->status == External_Operation_Status::pending) { state_->completion = std::move(completion); return; } - error = state_->error; + result = {state_->error, state_->exception}; invoke = true; } if (invoke) { try { - completion(std::move(error)); + completion(std::move(result)); } catch (...) { } } } - -bool External_Operation::cancel(std::exception_ptr reason) const noexcept { +bool External_Operation::cancel(External_Operation_Error error) const { return state_ && state_->finish(External_Operation_Status::cancelled, - cancellation_error(std::move(reason))); + checked_cancellation_error(error), {}); } - -External_Operation_Status External_Operation::status() const noexcept { +External_Operation_Status External_Operation::status() const { 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_); } - External_Operation_Source::External_Operation_Source() : state_(std::make_shared()) {} - External_Operation External_Operation_Source::operation() const noexcept { return External_Operation(state_); } - -bool External_Operation_Source::complete(std::function commit) noexcept { - return state_ && state_->finish(External_Operation_Status::completed, {}, +bool External_Operation_Source::complete(std::function commit) { + return state_ && state_->finish(External_Operation_Status::completed, + External_Operation_Error::none, {}, std::move(commit)); } - -bool External_Operation_Source::fail(std::exception_ptr error) noexcept { - if (!error) { - try { - throw std::runtime_error("external operation failed"); - } catch (...) { - error = std::current_exception(); - } - } +bool External_Operation_Source::fail(External_Operation_Error error) { + if (error != External_Operation_Error::external_failure) + throw std::invalid_argument("external operation failure error is invalid"); + return state_ && state_->finish(External_Operation_Status::failed, error, {}); +} +bool External_Operation_Source::fail(std::exception_ptr exception) { + if (!exception) + throw std::invalid_argument("external operation failure exception is empty"); return state_ && state_->finish(External_Operation_Status::failed, - std::move(error)); + External_Operation_Error::none, + std::move(exception)); } - -bool External_Operation_Source::cancel(std::exception_ptr reason) noexcept { +bool External_Operation_Source::cancel(External_Operation_Error error) { return state_ && state_->finish(External_Operation_Status::cancelled, - cancellation_error(std::move(reason))); + checked_cancellation_error(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); @@ -283,7 +244,6 @@ void External_Operation_Source::set_deadline(Clock::time_point deadline) { return; if (state_->deadline && *state_->deadline <= deadline) return; - std::weak_ptr weak = state_; const auto ticket = Deadline_Service::instance().schedule( deadline, [weak, deadline] { @@ -298,7 +258,8 @@ void External_Operation_Source::set_deadline(Clock::time_point deadline) { return; } static_cast(state->finish( - External_Operation_Status::cancelled, deadline_error())); + External_Operation_Status::cancelled, + External_Operation_Error::deadline_exceeded, {})); }); previous_ticket = std::exchange(state_->deadline_ticket, ticket); state_->deadline = deadline; @@ -306,28 +267,31 @@ void External_Operation_Source::set_deadline(Clock::time_point deadline) { if (previous_ticket != 0) Deadline_Service::instance().cancel(previous_ticket); } - Node_Execution_Result::Node_Execution_Result( + External_Operation_Error error, std::optional operation) noexcept - : operation_(std::move(operation)) {} - + : error_(error), operation_(std::move(operation)) {} Node_Execution_Result Node_Execution_Result::completed() noexcept { - return Node_Execution_Result(std::nullopt); + return Node_Execution_Result(External_Operation_Error::none, std::nullopt); } - -Node_Execution_Result Node_Execution_Result::external( - External_Operation operation) { +Node_Execution_Result Node_Execution_Result::failed(External_Operation_Error error) { + if (error == External_Operation_Error::none) + throw std::invalid_argument("failed node result has no error"); + return Node_Execution_Result(error, std::nullopt); +} +Node_Execution_Result Node_Execution_Result::external(External_Operation operation) { if (!operation) throw std::invalid_argument("external node result has no operation"); - return Node_Execution_Result(std::move(operation)); + return Node_Execution_Result(External_Operation_Error::none, std::move(operation)); +} +External_Operation_Error Node_Execution_Result::error() const noexcept { + return error_; } - bool Node_Execution_Result::is_external() const noexcept { return operation_.has_value(); } - const External_Operation& Node_Execution_Result::operation() const { if (!operation_) - throw std::logic_error("completed node result has no external operation"); + throw std::logic_error("node result has no external operation"); return *operation_; } diff --git a/Kernel/src/renderive/render_graph/External_Operation.hpp b/Kernel/src/renderive/render_graph/External_Operation.hpp index a0a6605..3a0406a 100644 --- a/Kernel/src/renderive/render_graph/External_Operation.hpp +++ b/Kernel/src/renderive/render_graph/External_Operation.hpp @@ -1,98 +1,72 @@ #pragma once - #include +#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); +enum class External_Operation_Error : std::uint8_t { + none, + cancelled, + deadline_exceeded, + external_failure }; - -class External_Operation_Deadline_Exceeded final - : public External_Operation_Cancelled { -public: - External_Operation_Deadline_Exceeded(); -}; - -enum class External_Operation_Status : unsigned char { +enum class External_Operation_Status : std::uint8_t { pending, completed, failed, cancelled }; - +struct External_Operation_Completion { + External_Operation_Error error{}; + std::exception_ptr exception; + [[nodiscard]] explicit operator bool() const noexcept { + return error == External_Operation_Error::none && !exception; + } +}; class External_Operation { public: - using Completion = std::function; - + using Completion = std::function; External_Operation() = default; - - // 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]] bool cancel(External_Operation_Error error = External_Operation_Error::cancelled) const; + [[nodiscard]] External_Operation_Status status() const; [[nodiscard]] explicit operator bool() const noexcept; - private: struct State; - explicit External_Operation(std::shared_ptr state) noexcept; - std::shared_ptr state_; - friend class External_Operation_Source; }; - 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; External_Operation_Source(External_Operation_Source&&) noexcept = default; External_Operation_Source& operator=(External_Operation_Source&&) noexcept = default; - [[nodiscard]] External_Operation operation() const 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. + [[nodiscard]] bool complete(std::function commit = {}); + [[nodiscard]] bool fail(External_Operation_Error error); + [[nodiscard]] bool fail(std::exception_ptr exception); + [[nodiscard]] bool cancel(External_Operation_Error error = External_Operation_Error::cancelled); void set_deadline(Clock::time_point deadline); - private: std::shared_ptr state_; }; - class Node_Execution_Result { public: [[nodiscard]] static Node_Execution_Result completed() noexcept; - [[nodiscard]] static Node_Execution_Result external( - External_Operation operation); - + [[nodiscard]] static Node_Execution_Result failed(External_Operation_Error error); + [[nodiscard]] static Node_Execution_Result external(External_Operation operation); + [[nodiscard]] External_Operation_Error error() const noexcept; [[nodiscard]] bool is_external() const noexcept; [[nodiscard]] const External_Operation& operation() const; - private: - explicit Node_Execution_Result( - std::optional operation) noexcept; - + Node_Execution_Result(External_Operation_Error error, + std::optional operation) noexcept; + External_Operation_Error error_{}; std::optional operation_; }; 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 9fa1827..1f8cf7e 100644 --- a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp +++ b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp @@ -10,14 +10,18 @@ #include "renderive/scheduling/detail/OneTBB_Runtime.hpp" namespace renderive::render_graph::detail { 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(); +Render_Graph_Execution_Error graph_error(External_Operation_Error error) { + switch (error) { + case External_Operation_Error::none: + return Render_Graph_Execution_Error::none; + case External_Operation_Error::cancelled: + return Render_Graph_Execution_Error::cancelled; + case External_Operation_Error::deadline_exceeded: + return Render_Graph_Execution_Error::deadline_exceeded; + case External_Operation_Error::external_failure: + return Render_Graph_Execution_Error::external_failure; } + throw std::logic_error("unknown external operation error"); } } struct Render_Graph_Runtime::State @@ -62,29 +66,25 @@ struct Render_Graph_Runtime::State *nodes[indices.at(edge.from)].execute, *nodes[indices.at(edge.to)].ready); } - void execute(std::span execution_slots, - Execute_Node execute) { + Render_Graph_Execution_Error execute( + std::span execution_slots, + Execute_Node execute) { { std::lock_guard lock(lifecycle_mutex); if (running.load(std::memory_order_acquire)) throw std::logic_error("render graph runtime is already executing"); cancellation_requested.store(false, std::memory_order_release); - { - std::lock_guard cancellation_lock(cancellation_mutex); - cancellation_exception = nullptr; - } + cancellation_error = External_Operation_Error::cancelled; running.store(true, std::memory_order_release); } - - std::exception_ptr execution_error; + std::exception_ptr execution_exception; + External_Operation_Error execution_error{}; try { if (!execute) - throw std::invalid_argument( - "render graph node executor is empty"); + 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 execution slot count differs from plan"); graph.reset(); executions.assign(execution_slots.begin(), execution_slots.end()); if (executions.empty()) @@ -94,6 +94,7 @@ struct Render_Graph_Runtime::State { std::lock_guard lock(error_mutex); first_exception = nullptr; + first_error = External_Operation_Error::none; } { std::lock_guard lock(external_mutex); @@ -110,17 +111,18 @@ struct Render_Graph_Runtime::State }); { std::lock_guard lock(error_mutex); - execution_error = first_exception; + execution_exception = first_exception; + execution_error = first_error; } } catch (...) { - execution_error = std::current_exception(); + execution_exception = std::current_exception(); } - finish_execution(); - if (execution_error) - std::rethrow_exception(execution_error); + if (execution_exception) + std::rethrow_exception(execution_exception); + return graph_error(execution_error); } - void finish_execution() noexcept { + void finish_execution() { execute_node = {}; executions.clear(); { @@ -131,35 +133,26 @@ struct Render_Graph_Runtime::State { std::lock_guard lock(lifecycle_mutex); cancellation_requested.store(false, std::memory_order_release); - { - std::lock_guard cancellation_lock(cancellation_mutex); - cancellation_exception = nullptr; - } + cancellation_error = External_Operation_Error::cancelled; running.store(false, std::memory_order_release); } } - void cancel_pending(std::exception_ptr reason) noexcept { - auto error = make_cancellation_error(std::move(reason)); + void cancel_pending(External_Operation_Error error) { + if (error == External_Operation_Error::none) + throw std::invalid_argument("render graph cancellation error is none"); { std::lock_guard lifecycle_lock(lifecycle_mutex); if (!running.load(std::memory_order_acquire)) return; - { - std::lock_guard cancellation_lock(cancellation_mutex); - if (!cancellation_exception) - cancellation_exception = error; - else - error = cancellation_exception; - } + if (!cancellation_requested.load(std::memory_order_acquire)) + cancellation_error = error; cancellation_requested.store(true, std::memory_order_release); } cancel_external_operations(error); } - std::exception_ptr cancellation_error() noexcept { - std::lock_guard lock(cancellation_mutex); - return cancellation_exception - ? cancellation_exception - : make_cancellation_error({}); + External_Operation_Error cancellation_reason() { + std::lock_guard lock(lifecycle_mutex); + return cancellation_error; } void mark_ready(std::size_t index) noexcept { if (failed.load(std::memory_order_acquire)) @@ -171,82 +164,90 @@ struct Render_Graph_Runtime::State } 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(); - execution->worker_id = renderive::scheduling::detail::OneTBB_Runtime::instance().current_worker_id(); - execution->status = Node_Execution_Status::running; - metrics = &execution->metrics; - } - Node_Execution_Result result = Node_Execution_Result::completed(); try { - result = execute_node(index, metrics); - } catch (...) { - fail(index, std::current_exception(), render_clock_now_ns(), false); - return; - } - const std::uint64_t cpu_end = render_clock_now_ns(); - if (cancellation_requested.load(std::memory_order_acquire)) { - const auto error = cancellation_error(); - if (result.is_external()) - static_cast(result.operation().cancel(error)); - fail(index, error, cpu_end, true); - return; - } - if (!result.is_external()) { - complete(index, cpu_end); - gateway.try_put(Message{}); - return; - } - if (auto* execution = executions[index]) { - execution->cpu_end_time_ns = cpu_end; - execution->external_start_time_ns = cpu_end; - execution->status = Node_Execution_Status::waiting_external; - } - gateway.reserve_wait(); - try { - const External_Operation operation = result.operation(); - { - std::lock_guard lock(external_mutex); - external_operations[index] = operation; + if (failed.load(std::memory_order_acquire)) + return; + if (cancellation_requested.load(std::memory_order_acquire)) { + fail_expected(index, cancellation_reason(), render_clock_now_ns()); + return; + } + Node_Execution_Metrics* metrics{}; + if (auto* execution = executions[index]) { + execution->start_time_ns = render_clock_now_ns(); + execution->worker_id = renderive::scheduling::detail::OneTBB_Runtime::instance().current_worker_id(); + execution->status = Node_Execution_Status::running; + metrics = &execution->metrics; + } + Node_Execution_Result result = Node_Execution_Result::completed(); + try { + result = execute_node(index, metrics); + } catch (...) { + fail_exception(index, std::current_exception(), render_clock_now_ns()); + return; + } + const std::uint64_t cpu_end = render_clock_now_ns(); + if (result.error() != External_Operation_Error::none) { + fail_expected(index, result.error(), cpu_end); + return; + } + if (cancellation_requested.load(std::memory_order_acquire)) { + const auto error = cancellation_reason(); + if (result.is_external()) + static_cast(result.operation().cancel(error)); + fail_expected(index, error, cpu_end); + return; + } + if (!result.is_external()) { + complete(index, cpu_end); + gateway.try_put(Message{}); + return; + } + if (auto* execution = executions[index]) { + execution->cpu_end_time_ns = cpu_end; + execution->external_start_time_ns = cpu_end; + execution->status = Node_Execution_Status::waiting_external; + } + gateway.reserve_wait(); + try { + const External_Operation operation = result.operation(); + { + std::lock_guard lock(external_mutex); + external_operations[index] = operation; + } + if (cancellation_requested.load(std::memory_order_acquire)) + static_cast(operation.cancel(cancellation_reason())); + auto self = shared_from_this(); + auto* gateway_ptr = &gateway; + operation.on_complete( + [self = std::move(self), gateway_ptr, index]( + External_Operation_Completion completion) { + const std::uint64_t end = render_clock_now_ns(); + self->clear_external(index); + if (completion.exception) { + self->fail_exception(index, + std::move(completion.exception), end); + } else if (completion.error != External_Operation_Error::none) { + self->fail_expected(index, completion.error, end); + } else { + self->complete_external(index, end); + gateway_ptr->try_put(Message{}); + } + gateway_ptr->release_wait(); + }); + } catch (...) { + clear_external(index); + fail_exception(index, std::current_exception(), render_clock_now_ns()); + gateway.release_wait(); } - if (cancellation_requested.load(std::memory_order_acquire)) - static_cast(operation.cancel(cancellation_error())); - auto self = shared_from_this(); - auto* gateway_ptr = &gateway; - 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 (...) { - clear_external(index); - fail(index, std::current_exception(), render_clock_now_ns(), false); - gateway.release_wait(); + fail_exception(index, std::current_exception(), render_clock_now_ns()); } } - void clear_external(std::size_t index) noexcept { + void clear_external(std::size_t index) { std::lock_guard lock(external_mutex); external_operations[index] = External_Operation{}; } - void cancel_external_operations(std::exception_ptr reason) noexcept { + void cancel_external_operations(External_Operation_Error error) { std::vector active; { std::lock_guard lock(external_mutex); @@ -257,7 +258,7 @@ struct Render_Graph_Runtime::State } } for (const auto& operation : active) - static_cast(operation.cancel(reason)); + static_cast(operation.cancel(error)); } void complete(std::size_t index, std::uint64_t end) noexcept { if (auto* execution = executions[index]) { @@ -273,10 +274,8 @@ struct Render_Graph_Runtime::State execution->status = Node_Execution_Status::complete; } } - 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); + void set_failed_execution(std::size_t index, std::uint64_t end, + Node_Execution_Status status) noexcept { if (auto* execution = executions[index]) { if (execution->start_time_ns == 0) execution->start_time_ns = end; @@ -285,17 +284,44 @@ struct Render_Graph_Runtime::State if (execution->status == Node_Execution_Status::waiting_external) execution->external_end_time_ns = end; execution->end_time_ns = end; - execution->status = cancelled - ? Node_Execution_Status::cancelled - : Node_Execution_Status::failed; + execution->status = status; } - { + } + void fail_expected(std::size_t index, External_Operation_Error error, + std::uint64_t end) noexcept { + try { + const bool first_failure = !failed.exchange(true, std::memory_order_acq_rel); + const auto status = error == External_Operation_Error::external_failure + ? Node_Execution_Status::failed + : Node_Execution_Status::cancelled; + set_failed_execution(index, end, status); + { + std::lock_guard lock(error_mutex); + if (first_error == External_Operation_Error::none && !first_exception) + first_error = error; + } + if (first_failure) + cancel_pending(External_Operation_Error::cancelled); + } catch (...) { + fail_exception(index, std::current_exception(), end); + } + } + void fail_exception(std::size_t index, std::exception_ptr exception, + std::uint64_t end) noexcept { + const bool first_failure = !failed.exchange(true, std::memory_order_acq_rel); + set_failed_execution(index, end, Node_Execution_Status::failed); + try { std::lock_guard lock(error_mutex); if (!first_exception) - first_exception = error; + first_exception = std::move(exception); + } catch (...) { + } + if (first_failure) { + try { + cancel_pending(External_Operation_Error::cancelled); + } catch (...) { + } } - if (first_failure) - cancel_pending(std::move(error)); } oneapi::tbb::flow::graph graph; std::vector nodes; @@ -304,11 +330,11 @@ struct Render_Graph_Runtime::State Execute_Node execute_node; std::mutex error_mutex; std::exception_ptr first_exception; + External_Operation_Error first_error{}; std::mutex external_mutex; std::vector external_operations; - std::mutex cancellation_mutex; - std::exception_ptr cancellation_exception; std::mutex lifecycle_mutex; + External_Operation_Error cancellation_error{External_Operation_Error::cancelled}; std::atomic_bool running{}; std::atomic_bool failed{}; std::atomic_bool cancellation_requested{}; @@ -316,13 +342,16 @@ struct Render_Graph_Runtime::State Render_Graph_Runtime::Render_Graph_Runtime(const Render_Plan& plan) : state_(std::make_shared(plan)) {} Render_Graph_Runtime::~Render_Graph_Runtime() { - state_->cancel_pending({}); + try { + state_->cancel_pending(External_Operation_Error::cancelled); + } catch (...) { + } } -void Render_Graph_Runtime::execute( +Render_Graph_Execution_Error Render_Graph_Runtime::execute( std::span executions, Execute_Node execute_node) { - state_->execute(executions, std::move(execute_node)); + return state_->execute(executions, std::move(execute_node)); } -void Render_Graph_Runtime::cancel_pending(std::exception_ptr reason) noexcept { - state_->cancel_pending(std::move(reason)); +void Render_Graph_Runtime::cancel_pending(External_Operation_Error error) { + state_->cancel_pending(error); } } 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 3cba66c..7ed4672 100644 --- a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.hpp +++ b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include #include @@ -8,6 +8,12 @@ #include "renderive/render_graph/Render_Plan.hpp" #include "renderive/scene/base/Abstract_Frame.hpp" namespace renderive::render_graph::detail { +enum class Render_Graph_Execution_Error : std::uint8_t { + none, + cancelled, + deadline_exceeded, + external_failure +}; class Render_Graph_Runtime final { public: using Execute_Node = std::function executions, - Execute_Node execute_node); - void cancel_pending(std::exception_ptr reason = {}) noexcept; + [[nodiscard]] Render_Graph_Execution_Error execute( + std::span executions, Execute_Node execute_node); + void cancel_pending(External_Operation_Error error = External_Operation_Error::cancelled); private: struct State; std::shared_ptr state_; diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp index 7e8d8cc..d3bfcf6 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp @@ -73,7 +73,7 @@ void Renderable_Base::Impl::rebuild_render_graph() { scene().request_render_graph_rebuild(owner()); } -void Renderable_Base::Impl::reset_render_graph() noexcept { +void Renderable_Base::Impl::reset_render_graph() { std::lock_guard lock(render_graph_mutex); render_graph.reset(); invalidate_prepare(); diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base_p.hpp b/Kernel/src/renderive/renderable/base/Renderable_Base_p.hpp index f7e0b36..402053e 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base_p.hpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base_p.hpp @@ -15,6 +15,7 @@ #include "renderive/scene/base/Scene_Lifetime.hpp" #include "renderive/state/Render_State_View.hpp" +class State_Strategy_Base; class Renderable_Base::Impl : public renderive::inheritance::Type_Root { public: struct Real_Time_Data_State { @@ -56,7 +57,7 @@ public: void register_real_time_data(Real_Time_Data_Base& data); void unregister_real_time_data(Real_Time_Data_Base& data) noexcept; void publish_real_time_data(); - void reset_render_graph() noexcept; + void reset_render_graph(); void add_prepare_action(Prepare_Action action); void execute_prepare(const Prepare_Render_Context& context); virtual void capture_frame_data(Frame_Render_Snapshot& snapshot) const; @@ -77,6 +78,7 @@ public: std::pmr::get_default_resource()}; std::shared_ptr real_time_data_state{ std::make_shared()}; + State_Strategy_Base* state_strategy{}; const Renderable_Id renderable_id; const Render_Node_Id composite_node_id; std::atomic configuration; diff --git a/Kernel/src/renderive/scene/base/Abstract_Frame.cpp b/Kernel/src/renderive/scene/base/Abstract_Frame.cpp index 3b85274..3a929f4 100644 --- a/Kernel/src/renderive/scene/base/Abstract_Frame.cpp +++ b/Kernel/src/renderive/scene/base/Abstract_Frame.cpp @@ -39,64 +39,58 @@ std::uint64_t Frame_Snapshot::render_duration_ns() const noexcept { return render_end_ns >= render_start_ns ? render_end_ns - render_start_ns : 0; } -void Abstract_Frame::begin_render(std::uint64_t frame_id, const Render_Plan& plan, - bool capture, std::uint64_t render_start_ns) { - if (rendering_) - throw std::logic_error("frame render is already active"); - rendering_ = true; - capture_ = capture; - frame_id_ = frame_id; - plan_version_ = plan.version; - render_start_ns_ = render_start_ns; - completed_snapshot_.reset(); - executions_.clear(); +void Abstract_Frame::begin_render(Render_State& state, std::uint64_t frame_id, + const Render_Plan& plan, bool capture, + std::uint64_t render_start_ns) { + state.capture = capture; + state.frame_id = frame_id; + state.plan_version = plan.version; + state.render_start_ns = render_start_ns; + state.executions.clear(); + if (state.shared_state) + state.shared_state->completed_snapshot.store({}, std::memory_order_release); if (!capture) return; - executions_.resize(plan.graph.nodes.size()); + state.executions.resize(plan.graph.nodes.size()); for (const auto& node : plan.graph.nodes) - executions_.at(node.execution_index).node_id = node.node_id; + state.executions.at(node.execution_index).node_id = node.node_id; } -bool Abstract_Frame::capture_this_frame() const noexcept { - return rendering_ && capture_; -} - -Node_Execution* Abstract_Frame::execution_slot(std::size_t index) noexcept { - return capture_this_frame() && index < executions_.size() ? &executions_[index] : nullptr; +Node_Execution* Abstract_Frame::execution_slot(Render_State& state, + std::size_t index) noexcept { + return state.capture && index < state.executions.size() + ? &state.executions[index] + : nullptr; } std::shared_ptr Abstract_Frame::complete_render( - std::uint64_t render_end_ns) { - if (!rendering_) - throw std::logic_error("frame render is not active"); - rendering_ = false; - if (!capture_) { - capture_ = false; + Render_State& state, std::uint64_t render_end_ns) { + if (!state.capture) return nullptr; - } auto snapshot = std::make_shared(); - snapshot->frame_id = frame_id_; - snapshot->render_plan_version = plan_version_; - snapshot->render_start_ns = render_start_ns_; + snapshot->frame_id = state.frame_id; + snapshot->render_plan_version = state.plan_version; + snapshot->render_start_ns = state.render_start_ns; snapshot->render_end_ns = render_end_ns; - snapshot->node_executions = std::move(executions_); - completed_snapshot_ = snapshot; - capture_ = false; + snapshot->node_executions = std::move(state.executions); + state.capture = false; + if (state.shared_state) + state.shared_state->completed_snapshot.store(snapshot, std::memory_order_release); return snapshot; } -void Abstract_Frame::discard_render() noexcept { - rendering_ = false; - capture_ = false; - frame_id_ = 0; - plan_version_ = 0; - render_start_ns_ = 0; - executions_.clear(); - completed_snapshot_.reset(); +void Abstract_Frame::discard_render(Render_State& state) noexcept { + state.capture = false; + state.frame_id = 0; + state.plan_version = 0; + state.render_start_ns = 0; + state.executions.clear(); + if (state.shared_state) + state.shared_state->completed_snapshot.store({}, std::memory_order_release); } std::shared_ptr Abstract_Frame::completed_snapshot() const noexcept { - return completed_snapshot_; + return state_->completed_snapshot.load(std::memory_order_acquire); } std::uint64_t render_clock_now_ns() noexcept { diff --git a/Kernel/src/renderive/scene/base/Abstract_Frame.hpp b/Kernel/src/renderive/scene/base/Abstract_Frame.hpp index 7c25a63..11e2478 100644 --- a/Kernel/src/renderive/scene/base/Abstract_Frame.hpp +++ b/Kernel/src/renderive/scene/base/Abstract_Frame.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -90,22 +91,28 @@ public: virtual ~Abstract_Frame() = default; [[nodiscard]] std::shared_ptr completed_snapshot() const noexcept; private: + struct Shared_State { + std::atomic> completed_snapshot; + std::atomic> published_snapshot; + }; + struct Render_State { + std::shared_ptr shared_state; + std::uint64_t frame_id{}; + Render_Plan_Version plan_version{}; + std::uint64_t render_start_ns{}; + std::vector executions; + bool capture{}; + }; friend class Scene_Base; - void begin_render(std::uint64_t frame_id, const Render_Plan& plan, bool capture, - std::uint64_t render_start_ns); - [[nodiscard]] bool capture_this_frame() const noexcept; - [[nodiscard]] Node_Execution* execution_slot(std::size_t index) noexcept; - [[nodiscard]] std::shared_ptr complete_render( - std::uint64_t render_end_ns); - void discard_render() noexcept; - std::uint64_t frame_id_{}; - Render_Plan_Version plan_version_{}; - std::uint64_t render_start_ns_{}; - std::vector executions_; - std::shared_ptr completed_snapshot_; - bool capture_{}; - bool rendering_{}; - std::shared_ptr published_snapshot_; + static void begin_render(Render_State& state, std::uint64_t frame_id, + const Render_Plan& plan, bool capture, + std::uint64_t render_start_ns); + [[nodiscard]] static Node_Execution* execution_slot( + Render_State& state, std::size_t index) noexcept; + [[nodiscard]] static std::shared_ptr complete_render( + Render_State& state, std::uint64_t render_end_ns); + static void discard_render(Render_State& state) noexcept; + std::shared_ptr state_{std::make_shared()}; }; [[nodiscard]] std::uint64_t render_clock_now_ns() noexcept; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index 3bf38dd..d1e926a 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -2,8 +2,6 @@ #include #include -#include -#include #include #include #include @@ -26,6 +24,41 @@ #include "renderive/scheduling/detail/OneTBB_Runtime.hpp" #include "renderive/state/base/State_Strategy_Base.hpp" +namespace { +Scene_Edit_Error relationship_error( + renderive::scene::dependency::Mutation_Error error, bool display) noexcept { + using Error = renderive::scene::dependency::Mutation_Error; + switch (error) { + case Error::none: + return Scene_Edit_Error::none; + case Error::endpoint_not_attached: + return Scene_Edit_Error::renderable_not_attached; + case Error::self_reference: + return display ? Scene_Edit_Error::display_self_reference + : Scene_Edit_Error::dependency_self_reference; + case Error::cycle: + return display ? Scene_Edit_Error::display_cycle + : Scene_Edit_Error::dependency_cycle; + } + std::terminate(); +} +Scene_Render_Error scene_render_error( + renderive::render_graph::detail::Render_Graph_Execution_Error error) { + using Error = renderive::render_graph::detail::Render_Graph_Execution_Error; + switch (error) { + case Error::none: + return Scene_Render_Error::none; + case Error::cancelled: + return Scene_Render_Error::cancelled; + case Error::deadline_exceeded: + return Scene_Render_Error::deadline_exceeded; + case Error::external_failure: + return Scene_Render_Error::external_failure; + } + throw std::logic_error("unknown render graph execution error"); +} +} + class Scene_Base::Execution_Context { public: using Message = oneapi::tbb::flow::continue_msg; @@ -78,18 +111,25 @@ private: while (current < value && !peak.compare_exchange_weak( current, value, std::memory_order_relaxed)) {} } - void schedule() { + void schedule() noexcept { if (scheduled_.exchange(true, std::memory_order_acq_rel)) return; - auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena(); - arena.execute([this] { node_.try_put(Message{}); }); + try { + auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena(); + arena.execute([this] { node_.try_put(Message{}); }); + } catch (...) { + std::terminate(); + } } void execute_one() { Operation operation; if (operations_.try_pop(operation)) { queued_.fetch_sub(1, std::memory_order_relaxed); slots_.release(); - operation(); + try { + operation(); + } catch (...) { + } } scheduled_.store(false, std::memory_order_release); if (!operations_.empty()) @@ -131,6 +171,69 @@ struct Scene_Base::Compiled_Render_Plan { std::vector execution_bindings; std::unique_ptr runtime; }; +class Scene_Base::Renderable_Edit_Transaction { +public: + explicit Renderable_Edit_Transaction(Scene_Base& scene) + : scene_(scene), dependency_(scene.dependency_resolver_.resolve()), + model_dirty_(scene.model_dirty_.load(std::memory_order_acquire)) { + renderables_.reserve(scene.renderables_.size()); + color_caches_.reserve(scene.color_caches_.size()); + for (const auto& [id, renderable] : scene.renderables_) { + renderables_.emplace_back(id, renderable); + capture(renderable); + } + for (const auto& [id, cache] : scene.color_caches_) + color_caches_.emplace_back(id, cache); + if (scene.raster_capabilities_) + display_ = scene.raster_capabilities_->resolve(); + } + void capture(const Renderable& renderable) { + if (!renderable) + return; + if (std::ranges::any_of(renderable_states_, [&](const auto& state) { + return state.renderable.get() == renderable.get(); + })) + return; + auto& data = renderable->d_func(); + renderable_states_.push_back({ + renderable, + data.real_time_data_state->scene_lifetime, + data.real_time_data_state->attached.load(std::memory_order_acquire), + data.prepare_revision.load(std::memory_order_acquire)}); + } + void rollback() { + scene_.renderables_.clear(); + for (const auto& [id, renderable] : renderables_) + scene_.renderables_.emplace(id, renderable); + scene_.dependency_resolver_.assign(dependency_); + if (scene_.raster_capabilities_ && display_) + scene_.raster_capabilities_->restore(*display_); + scene_.color_caches_.clear(); + for (const auto& [id, cache] : color_caches_) + scene_.color_caches_.emplace(id, cache); + for (const auto& state : renderable_states_) { + auto& data = state.renderable->d_func(); + data.real_time_data_state->scene_lifetime = state.scene_lifetime; + data.real_time_data_state->attached.store(state.attached, std::memory_order_release); + data.prepare_revision.store(state.prepare_revision, std::memory_order_release); + } + scene_.model_dirty_.store(model_dirty_, std::memory_order_release); + } +private: + struct Renderable_State { + Renderable renderable; + std::shared_ptr scene_lifetime; + bool attached{}; + std::uint64_t prepare_revision{}; + }; + Scene_Base& scene_; + std::vector> renderables_; + renderive::scene::dependency::Resolution dependency_; + std::optional> display_; + std::vector>> color_caches_; + std::vector renderable_states_; + bool model_dirty_{}; +}; Scene_Base::Scene_Base() : Scene_Base(*std::pmr::get_default_resource()) {} Scene_Base::Scene_Base(std::pmr::memory_resource& upstream_memory_resource) : Scene_Base(upstream_memory_resource, std::make_unique()) {} @@ -154,45 +257,51 @@ Scene_Base::Attach_Builder::Attach_Builder(Scene_Base& scene) if (scene.runtime_started_) throw std::logic_error("scene attach builder is only available before runtime starts"); } -Scene_Base::Attach_Builder::~Attach_Builder() { - try { - std::lock_guard lock(scene_.model_mutex_); - scene_.cleanup_detached_topology_locked(); - scene_.validate_structure_locked(); - } catch (...) { - Scene_Base::structure_fail_fast("initial scene structure", std::current_exception()); - } -} -void Scene_Base::Attach_Builder::attach(Renderable renderable) { +Scene_Edit_Error Scene_Base::Attach_Builder::attach(Renderable renderable) { std::lock_guard lock(scene_.model_mutex_); - Renderable_Editor(scene_).attach(std::move(renderable)); + return Renderable_Editor(scene_).attach(std::move(renderable)); } -void Scene_Base::Attach_Builder::set_dependency_parent(const Renderable& child, const Renderable& parent) { +Scene_Edit_Error Scene_Base::Attach_Builder::set_dependency_parent( + const Renderable& child, const Renderable& parent) { std::lock_guard lock(scene_.model_mutex_); - Renderable_Editor(scene_).set_dependency_parent(child, parent); + return Renderable_Editor(scene_).set_dependency_parent(child, parent); } -void Scene_Base::Attach_Builder::add_dependency_parent(const Renderable& child, const Renderable& parent) { +Scene_Edit_Error Scene_Base::Attach_Builder::add_dependency_parent( + const Renderable& child, const Renderable& parent) { std::lock_guard lock(scene_.model_mutex_); - Renderable_Editor(scene_).add_dependency_parent(child, parent); + return Renderable_Editor(scene_).add_dependency_parent(child, parent); } -void Scene_Base::Attach_Builder::clear_dependency_parent(const Renderable& child) { +Scene_Edit_Error Scene_Base::Attach_Builder::clear_dependency_parent( + const Renderable& child) { std::lock_guard lock(scene_.model_mutex_); - Renderable_Editor(scene_).clear_dependency_parent(child); + return Renderable_Editor(scene_).clear_dependency_parent(child); } Scene_Base::Attach_Builder Scene_Base::attach_builder() { return Attach_Builder(*this); } -void Scene_Base::Renderable_Editor::attach(Renderable renderable) { +Scene_Edit_Error Scene_Base::Renderable_Editor::fail( + Scene_Edit_Error error) noexcept { + if (error_ == Scene_Edit_Error::none) + error_ = error; + return error_; +} +Scene_Edit_Error Scene_Base::Renderable_Editor::attach(Renderable renderable) { + if (error_ != Scene_Edit_Error::none) + return error_; if (!renderable) - throw std::invalid_argument("renderable is null"); + return fail(Scene_Edit_Error::null_renderable); + scene_.capture_edit_renderable(renderable); + const auto lifetime = renderable->d_func().real_time_data_state->scene_lifetime; + if (lifetime && lifetime.get() != scene_.scene_lifetime_.get()) + return fail(Scene_Edit_Error::foreign_renderable); renderable->d_func().bind_scene(scene_.scene_lifetime_); auto& renderable_data = renderable->d_func(); const Renderable_Id id = renderable_data.renderable_id; if (const auto existing = scene_.renderables_.find(id); existing != scene_.renderables_.end()) { if (existing->second != renderable) - throw std::logic_error("renderable id is already attached"); - return; + throw std::logic_error("renderable id collision"); + return Scene_Edit_Error::none; } if (!scene_.dependency_resolver_.contains(id)) scene_.dependency_resolver_.attach(id); @@ -202,71 +311,128 @@ void Scene_Base::Renderable_Editor::attach(Renderable renderable) { ? scene_.raster_capabilities_->make_renderable_color_cache() : std::shared_ptr{}; if (!scene_.renderables_.try_emplace(id, renderable).second) - throw std::logic_error("renderable id is already attached"); + throw std::logic_error("renderable id collision"); try { if (cache && !scene_.color_caches_.try_emplace(id, std::move(cache)).second) - throw std::logic_error("renderable color cache is already attached"); + throw std::logic_error("renderable color cache collision"); } catch (...) { scene_.renderables_.erase(id); throw; } - renderable_data.real_time_data_state->attached.store( - true, std::memory_order_release); + renderable_data.real_time_data_state->attached.store(true, std::memory_order_release); renderable_data.invalidate_prepare(); scene_.notify_model_dirty(); + return Scene_Edit_Error::none; } -void Scene_Base::Renderable_Editor::detach(const Renderable& renderable) { +Scene_Edit_Error Scene_Base::Renderable_Editor::detach( + const Renderable& renderable) { + if (error_ != Scene_Edit_Error::none) + return error_; if (!renderable) - throw std::invalid_argument("renderable is null"); - scene_.validate_renderable_scene(*renderable); + return fail(Scene_Edit_Error::null_renderable); + scene_.capture_edit_renderable(renderable); + const auto scene_error = scene_.renderable_scene_error(*renderable); + if (scene_error != Scene_Edit_Error::none) + return fail(scene_error); if (!scene_.is_renderable_attached_locked(renderable)) - return; + return fail(Scene_Edit_Error::renderable_not_attached); auto& renderable_data = renderable->d_func(); const Renderable_Id id = renderable_data.renderable_id; - renderable_data.real_time_data_state->attached.store( - false, std::memory_order_release); + renderable_data.real_time_data_state->attached.store(false, std::memory_order_release); scene_.color_caches_.erase(id); scene_.renderables_.erase(id); scene_.notify_model_dirty(); + return Scene_Edit_Error::none; } -void Scene_Base::Renderable_Editor::set_dependency_parent(const Renderable& child, const Renderable& parent) { +Scene_Edit_Error Scene_Base::Renderable_Editor::set_dependency_parent( + const Renderable& child, const Renderable& parent) { + if (error_ != Scene_Edit_Error::none) + return error_; if (!child || !parent) - throw std::invalid_argument("dependency relationship endpoint is null"); - scene_.validate_renderable_scene(*child); - scene_.validate_renderable_scene(*parent); - if (!scene_.dependency_resolver_.replace_parents( - child->d_func().renderable_id, - {parent->d_func().renderable_id})) - return; + return fail(Scene_Edit_Error::null_renderable); + scene_.capture_edit_renderable(child); + auto scene_error = scene_.renderable_scene_error(*child); + if (scene_error != Scene_Edit_Error::none) + return fail(scene_error); + scene_error = scene_.renderable_scene_error(*parent); + if (scene_error != Scene_Edit_Error::none) + return fail(scene_error); + if (!scene_.is_renderable_attached_locked(child) || + !scene_.is_renderable_attached_locked(parent)) + return fail(Scene_Edit_Error::renderable_not_attached); + const auto result = scene_.dependency_resolver_.replace_parents( + child->d_func().renderable_id, {parent->d_func().renderable_id}); + if (!result) + return fail(relationship_error(result.error, false)); + if (!result.changed) + return Scene_Edit_Error::none; child->d_func().invalidate_prepare(); scene_.notify_model_dirty(); + return Scene_Edit_Error::none; } -void Scene_Base::Renderable_Editor::add_dependency_parent(const Renderable& child, const Renderable& parent) { +Scene_Edit_Error Scene_Base::Renderable_Editor::add_dependency_parent( + const Renderable& child, const Renderable& parent) { + if (error_ != Scene_Edit_Error::none) + return error_; if (!child || !parent) - throw std::invalid_argument("dependency relationship endpoint is null"); - scene_.validate_renderable_scene(*child); - scene_.validate_renderable_scene(*parent); - if (!scene_.dependency_resolver_.add_parent(child->d_func().renderable_id, - parent->d_func().renderable_id)) - return; + return fail(Scene_Edit_Error::null_renderable); + scene_.capture_edit_renderable(child); + auto scene_error = scene_.renderable_scene_error(*child); + if (scene_error != Scene_Edit_Error::none) + return fail(scene_error); + scene_error = scene_.renderable_scene_error(*parent); + if (scene_error != Scene_Edit_Error::none) + return fail(scene_error); + if (!scene_.is_renderable_attached_locked(child) || + !scene_.is_renderable_attached_locked(parent)) + return fail(Scene_Edit_Error::renderable_not_attached); + const auto result = scene_.dependency_resolver_.add_parent( + child->d_func().renderable_id, parent->d_func().renderable_id); + if (!result) + return fail(relationship_error(result.error, false)); + if (!result.changed) + return Scene_Edit_Error::none; child->d_func().invalidate_prepare(); scene_.notify_model_dirty(); + return Scene_Edit_Error::none; } -void Scene_Base::Renderable_Editor::clear_dependency_parent(const Renderable& child) { +Scene_Edit_Error Scene_Base::Renderable_Editor::clear_dependency_parent( + const Renderable& child) { + if (error_ != Scene_Edit_Error::none) + return error_; if (!child) - throw std::invalid_argument("dependency child is null"); - scene_.validate_renderable_scene(*child); - if (!scene_.dependency_resolver_.clear_parents(child->d_func().renderable_id)) - return; + return fail(Scene_Edit_Error::null_renderable); + scene_.capture_edit_renderable(child); + const auto scene_error = scene_.renderable_scene_error(*child); + if (scene_error != Scene_Edit_Error::none) + return fail(scene_error); + if (!scene_.is_renderable_attached_locked(child)) + return fail(Scene_Edit_Error::renderable_not_attached); + const auto result = scene_.dependency_resolver_.clear_parents( + child->d_func().renderable_id); + if (!result) + return fail(relationship_error(result.error, false)); + if (!result.changed) + return Scene_Edit_Error::none; child->d_func().invalidate_prepare(); scene_.notify_model_dirty(); + return Scene_Edit_Error::none; } -void Scene_Base::edit_renderables(Renderable_Edit edit) { - if (!edit) - throw std::invalid_argument("renderable edit callback is empty"); - enqueue_renderable_edit([this, edit = std::move(edit)]() mutable { +Scene_Edit_Error Scene_Base::Edit_Operation::wait() const { + if (!result_.valid()) + throw std::logic_error("scene edit operation is invalid"); + return result_.get(); +} +Scene_Base::Edit_Operation Scene_Base::edit_renderables(Renderable_Edit edit) { + if (!edit) { + std::promise promise; + promise.set_value(Scene_Edit_Error::empty_edit); + return Edit_Operation(promise.get_future().share()); + } + return enqueue_renderable_edit([this, edit = std::move(edit)]() mutable { Renderable_Editor editor(*this); edit(editor); + return editor.error(); }); } @@ -284,81 +450,123 @@ Scene_2D_Base::Scene_2D_Base(std::pmr::memory_resource& memory_resource, Scene_2D_Base::Attach_Builder Scene_2D_Base::attach_builder() { return Attach_Builder(*this); } - -void Scene_2D_Base::edit_renderables(Renderable_Edit edit) { - if (!edit) - throw std::invalid_argument("renderable edit callback is empty"); - enqueue_renderable_edit([this, edit = std::move(edit)]() mutable { +Scene_Base::Edit_Operation Scene_2D_Base::edit_renderables(Renderable_Edit edit) { + if (!edit) { + std::promise promise; + promise.set_value(Scene_Edit_Error::empty_edit); + return Edit_Operation(promise.get_future().share()); + } + return enqueue_renderable_edit([this, edit = std::move(edit)]() mutable { Renderable_Editor editor(*this); edit(editor); + return editor.error(); }); } - void Scene_2D_Base::with_initial_display_edit(std::function edit) { std::lock_guard lock(model_mutex_); edit(); } - -void Scene_2D_Base::Attach_Builder::set_display_parent( +Scene_Edit_Error Scene_2D_Base::Attach_Builder::set_display_parent( const Renderable& child, const Renderable& parent) { + Scene_Edit_Error result{}; scene_2d_.with_initial_display_edit([&] { - Renderable_Editor(scene_2d_).set_display_parent(child, parent); + result = Renderable_Editor(scene_2d_).set_display_parent(child, parent); }); + return result; } -void Scene_2D_Base::Attach_Builder::add_display_parent( +Scene_Edit_Error Scene_2D_Base::Attach_Builder::add_display_parent( const Renderable& child, const Renderable& parent) { + Scene_Edit_Error result{}; scene_2d_.with_initial_display_edit([&] { - Renderable_Editor(scene_2d_).add_display_parent(child, parent); + result = Renderable_Editor(scene_2d_).add_display_parent(child, parent); }); + return result; } -void Scene_2D_Base::Attach_Builder::clear_display_parent( +Scene_Edit_Error Scene_2D_Base::Attach_Builder::clear_display_parent( const Renderable& child) { + Scene_Edit_Error result{}; scene_2d_.with_initial_display_edit([&] { - Renderable_Editor(scene_2d_).clear_display_parent(child); + result = Renderable_Editor(scene_2d_).clear_display_parent(child); }); + return result; } - -void Scene_2D_Base::Renderable_Editor::set_display_parent( +Scene_Edit_Error Scene_2D_Base::Renderable_Editor::set_display_parent( const Renderable& child, const Renderable& parent) { - scene_2d_.set_display_parent_locked(child, parent); + if (error() != Scene_Edit_Error::none) + return error(); + const auto result = scene_2d_.set_display_parent_locked(child, parent); + return result == Scene_Edit_Error::none ? result : fail(result); } -void Scene_2D_Base::Renderable_Editor::add_display_parent( +Scene_Edit_Error Scene_2D_Base::Renderable_Editor::add_display_parent( const Renderable& child, const Renderable& parent) { - scene_2d_.add_display_parent_locked(child, parent); + if (error() != Scene_Edit_Error::none) + return error(); + const auto result = scene_2d_.add_display_parent_locked(child, parent); + return result == Scene_Edit_Error::none ? result : fail(result); } -void Scene_2D_Base::Renderable_Editor::clear_display_parent( +Scene_Edit_Error Scene_2D_Base::Renderable_Editor::clear_display_parent( const Renderable& child) { - scene_2d_.clear_display_parent_locked(child); + if (error() != Scene_Edit_Error::none) + return error(); + const auto result = scene_2d_.clear_display_parent_locked(child); + return result == Scene_Edit_Error::none ? result : fail(result); } - -void Scene_2D_Base::set_display_parent_locked(const Renderable& child, - const Renderable& parent) { +Scene_Edit_Error Scene_2D_Base::set_display_parent_locked( + const Renderable& child, const Renderable& parent) { if (!child || !parent) - throw std::invalid_argument("display relationship endpoint is null"); - validate_renderable_scene(*child); - validate_renderable_scene(*parent); - if (display_resolver_.replace_parents( - child->renderable_id(), {parent->renderable_id()})) + return Scene_Edit_Error::null_renderable; + auto scene_error = renderable_scene_error(*child); + if (scene_error != Scene_Edit_Error::none) + return scene_error; + scene_error = renderable_scene_error(*parent); + if (scene_error != Scene_Edit_Error::none) + return scene_error; + if (!is_renderable_attached_locked(child) || !is_renderable_attached_locked(parent)) + return Scene_Edit_Error::renderable_not_attached; + const auto result = display_resolver_.replace_parents( + child->renderable_id(), {parent->renderable_id()}); + if (!result) + return relationship_error(result.error, true); + if (result.changed) notify_model_dirty(); + return Scene_Edit_Error::none; } -void Scene_2D_Base::add_display_parent_locked(const Renderable& child, - const Renderable& parent) { +Scene_Edit_Error Scene_2D_Base::add_display_parent_locked( + const Renderable& child, const Renderable& parent) { if (!child || !parent) - throw std::invalid_argument("display relationship endpoint is null"); - validate_renderable_scene(*child); - validate_renderable_scene(*parent); - if (display_resolver_.add_parent(child->renderable_id(), - parent->renderable_id())) + return Scene_Edit_Error::null_renderable; + auto scene_error = renderable_scene_error(*child); + if (scene_error != Scene_Edit_Error::none) + return scene_error; + scene_error = renderable_scene_error(*parent); + if (scene_error != Scene_Edit_Error::none) + return scene_error; + if (!is_renderable_attached_locked(child) || !is_renderable_attached_locked(parent)) + return Scene_Edit_Error::renderable_not_attached; + const auto result = display_resolver_.add_parent( + child->renderable_id(), parent->renderable_id()); + if (!result) + return relationship_error(result.error, true); + if (result.changed) notify_model_dirty(); + return Scene_Edit_Error::none; } -void Scene_2D_Base::clear_display_parent_locked(const Renderable& child) { +Scene_Edit_Error Scene_2D_Base::clear_display_parent_locked( + const Renderable& child) { if (!child) - throw std::invalid_argument("display child is null"); - validate_renderable_scene(*child); - if (display_resolver_.clear_parents(child->renderable_id())) + return Scene_Edit_Error::null_renderable; + const auto scene_error = renderable_scene_error(*child); + if (scene_error != Scene_Edit_Error::none) + return scene_error; + if (!is_renderable_attached_locked(child)) + return Scene_Edit_Error::renderable_not_attached; + const auto result = display_resolver_.clear_parents(child->renderable_id()); + if (!result) + return relationship_error(result.error, true); + if (result.changed) notify_model_dirty(); + return Scene_Edit_Error::none; } - Scene_3D_Base::Scene_3D_Base() : Scene_3D_Base(*std::pmr::get_default_resource()) {} Scene_3D_Base::Scene_3D_Base(std::pmr::memory_resource& memory_resource) @@ -369,40 +577,51 @@ Scene_3D_Base::Scene_3D_Base(std::pmr::memory_resource& memory_resource, Scene_3D_Base::Attach_Builder Scene_3D_Base::attach_builder() { return Attach_Builder(*this); } -void Scene_3D_Base::edit_renderables(Renderable_Edit edit) { - if (!edit) - throw std::invalid_argument("renderable edit callback is empty"); - enqueue_renderable_edit([this, edit = std::move(edit)]() mutable { +Scene_Base::Edit_Operation Scene_3D_Base::edit_renderables(Renderable_Edit edit) { + if (!edit) { + std::promise promise; + promise.set_value(Scene_Edit_Error::empty_edit); + return Edit_Operation(promise.get_future().share()); + } + return enqueue_renderable_edit([this, edit = std::move(edit)]() mutable { Renderable_Editor editor(*this); edit(editor); + return editor.error(); }); } -void Scene_Base::render() { - submit_render(nullptr); +Scene_Render_Error Scene_Base::render() { + return submit_render(nullptr); } - -void Scene_Base::render(Abstract_Frame& frame) { - submit_render(&frame); +Scene_Render_Error Scene_Base::render(Abstract_Frame& frame) { + return submit_render(&frame); } - -void Scene_Base::submit_render(Abstract_Frame* frame) { +Scene_Render_Error Scene_Base::submit_render(Abstract_Frame* frame) { if (active_submitted_observer_scene_ == this) { std::lock_guard lock(task_mutex_); ++deferred_render_count_; - return; + return Scene_Render_Error::none; } auto task_lock = lock_render_idle(); + if (shutting_down_) + return Scene_Render_Error::shutting_down; runtime_started_ = true; - if (pending_exception_observed_) { - pending_exception_ = nullptr; - pending_exception_observed_ = false; + std::exception_ptr previous_exception; + if (pending_exception_) + previous_exception = std::exchange(pending_exception_, {}); + else if (current_completion_ && current_completion_->completed && current_completion_->exception && !current_completion_->observed) { + current_completion_->observed = true; + previous_exception = current_completion_->exception; + } + if (previous_exception) { + task_lock.unlock(); + std::rethrow_exception(previous_exception); } - if (!pending_exception_ && current_completion_ && current_completion_->completed && current_completion_->exception && !current_completion_->observed) - pending_exception_ = current_completion_->exception; auto& strategy = frame_control_strategy(); strategy.swap(); - auto snapshot = frame ? std::exchange(frame->published_snapshot_, {}) : nullptr; + auto snapshot = frame + ? frame->state_->published_snapshot.exchange({}, std::memory_order_acq_rel) + : nullptr; if (!snapshot) snapshot = capture_live_frame(); snapshot->next_refresh_interval_ns_ = strategy.frame_control_state().next_refresh_interval_ns; @@ -410,11 +629,14 @@ void Scene_Base::submit_render(Abstract_Frame* frame) { throw std::overflow_error("render sequence exhausted"); snapshot->render_sequence = ++render_sequence_; auto task = std::make_shared(); - task->frame = frame ? Render_Task::Frame{std::ref(*frame)} : Render_Task::Frame{std::make_shared()}; + task->frame = std::make_shared(); + if (frame) + task->frame->shared_state = frame->state_; task->compiled_plan = compile_render_plan(*snapshot); task->topology = std::make_shared(topology_snapshot()); task->completion = std::make_shared(); - snapshot->capture_ticket_ = capture_controller_.begin_frame(); + const Capture_Frame_Ticket capture_ticket = capture_controller_.begin_frame(); + snapshot->capture_ticket_ = capture_ticket; task->snapshot = std::move(snapshot); current_completion_ = task->completion; const Observation submitted_observation{ @@ -424,24 +646,39 @@ void Scene_Base::submit_render(Abstract_Frame* frame) { ++pending_operations_; task_lock.unlock(); Scene_Base* previous_observer_scene = std::exchange(active_submitted_observer_scene_, this); - d_func().dispatch(submitted_observation); + try { + d_func().dispatch(submitted_observation); + } catch (...) { + active_submitted_observer_scene_ = previous_observer_scene; + const auto exception = std::current_exception(); + { + std::lock_guard lock(task_mutex_); + task->completion->exception = exception; + task->completion->completed = true; + task->completion->observed = true; + deferred_render_count_ = 0; + } + capture_controller_.finish_frame(capture_ticket, false); + complete_pending_operation(); + std::rethrow_exception(exception); + } active_submitted_observer_scene_ = previous_observer_scene; try { execution_context_->submit([this, task = std::move(task)] { execute_render_task(task); - { - std::lock_guard lock(task_mutex_); - --pending_operations_; - } - render_completed_.notify_all(); + complete_pending_operation(); }); } catch (...) { + capture_controller_.finish_frame(capture_ticket, false); + const auto exception = std::current_exception(); { std::lock_guard lock(task_mutex_); - --pending_operations_; + current_completion_->exception = exception; + current_completion_->completed = true; + current_completion_->observed = true; } - render_completed_.notify_all(); - throw; + complete_pending_operation(); + std::rethrow_exception(exception); } std::size_t deferred_render_count{}; { @@ -449,36 +686,62 @@ void Scene_Base::submit_render(Abstract_Frame* frame) { deferred_render_count = std::exchange(deferred_render_count_, 0); } for (std::size_t index = 0; index < deferred_render_count; ++index) - render(); + static_cast(render()); + return Scene_Render_Error::none; } - -void Scene_Base::wait_for_render() { +Scene_Render_Error Scene_Base::wait_for_render() { if (is_render_execution_context() || active_submitted_observer_scene_ == this) - return; + return Scene_Render_Error::none; std::unique_lock lock(task_mutex_); const auto completion = current_completion_; if (!completion) - return; + return Scene_Render_Error::none; render_completed_.wait(lock, [&completion] { return completion->completed; }); std::exception_ptr exception; - if (pending_exception_) { - exception = pending_exception_; - pending_exception_observed_ = true; - if (!completion->exception) - completion->observed = true; - } else { + if (pending_exception_) + exception = std::exchange(pending_exception_, {}); + else { exception = completion->exception; completion->observed = true; } + const auto error = completion->error; lock.unlock(); if (exception) std::rethrow_exception(exception); + return error; } -void Scene_Base::enqueue_renderable_edit(std::function edit) { - submit_operation([this, edit = std::move(edit)]() mutable { - execute_renderable_edit(std::move(edit)); - }); +Scene_Base::Edit_Operation Scene_Base::enqueue_renderable_edit( + std::function edit) { + auto promise = std::make_shared>(); + Edit_Operation operation(promise->get_future().share()); + { + std::lock_guard lock(task_mutex_); + if (shutting_down_) { + promise->set_value(Scene_Edit_Error::shutting_down); + return operation; + } + runtime_started_ = true; + ++pending_operations_; + } + try { + execution_context_->submit( + [this, edit = std::move(edit), promise = std::move(promise)]() mutable { + try { + promise->set_value(execute_renderable_edit(std::move(edit))); + } catch (...) { + const auto exception = std::current_exception(); + record_pending_exception(exception); + promise->set_exception(exception); + } + complete_pending_operation(); + }); + } catch (...) { + complete_pending_operation(); + throw; + } + return operation; } + void Scene_Base::submit_operation(std::function operation) { { std::lock_guard lock(task_mutex_); @@ -489,41 +752,53 @@ void Scene_Base::submit_operation(std::function operation) { } try { execution_context_->submit([this, operation = std::move(operation)]() mutable { - operation(); - { - std::lock_guard lock(task_mutex_); - --pending_operations_; + try { + operation(); + } catch (...) { + record_pending_exception(std::current_exception()); } - render_completed_.notify_all(); + complete_pending_operation(); }); } catch (...) { - { - std::lock_guard lock(task_mutex_); - --pending_operations_; - } - render_completed_.notify_all(); + complete_pending_operation(); throw; } } +void Scene_Base::complete_pending_operation() noexcept { + { + std::lock_guard lock(task_mutex_); + --pending_operations_; + } + render_completed_.notify_all(); +} +void Scene_Base::record_pending_exception(std::exception_ptr exception) noexcept { + std::lock_guard lock(task_mutex_); + if (!pending_exception_) + pending_exception_ = std::move(exception); +} +void Scene_Base::capture_edit_renderable(const Renderable& renderable) { + if (active_edit_transaction_) + active_edit_transaction_->capture(renderable); +} -void Scene_Base::cleanup_detached_topology_locked() { +Scene_Edit_Error Scene_Base::cleanup_detached_topology_locked() { for (const Renderable_Id id : dependency_resolver_.ids()) { if (renderables_.contains(id)) continue; if (!dependency_resolver_.isolated(id)) - throw std::logic_error( - "dependency graph still references detached renderable " + - std::to_string(id)); + return Scene_Edit_Error::dangling_dependency; dependency_resolver_.erase(id); } - if (raster_capabilities_) - raster_capabilities_->cleanup(); + if (raster_capabilities_ && !raster_capabilities_->cleanup()) + return Scene_Edit_Error::dangling_display; + return Scene_Edit_Error::none; } -void Scene_Base::validate_structure_locked() { +Scene_Edit_Error Scene_Base::validate_structure_locked() const { for (const auto& [id, renderable] : renderables_) { if (!renderable) throw std::logic_error("scene contains null renderable owner"); - validate_renderable_scene(*renderable); + if (renderable_scene_error(*renderable) != Scene_Edit_Error::none) + throw std::logic_error("scene contains renderable bound to another scene"); if (!renderable->d_func().real_time_data_state->attached.load( std::memory_order_acquire)) throw std::logic_error("scene contains renderable whose attached state is false"); @@ -532,32 +807,16 @@ void Scene_Base::validate_structure_locked() { } const auto dependency = dependency_resolver_.resolve(); if (dependency.order.size() != renderables_.size()) - throw std::logic_error( - "dependency graph does not match attached renderables"); + throw std::logic_error("dependency graph does not match attached renderables"); for (const Renderable_Id id : dependency.order) { if (!renderables_.contains(id)) - throw std::logic_error( - "dependency graph references detached renderable " + - std::to_string(id)); + throw std::logic_error("dependency graph references detached renderable " + std::to_string(id)); } - if (raster_capabilities_) - raster_capabilities_->validate(); -} -[[noreturn]] void Scene_Base::structure_fail_fast(const char* stage, std::exception_ptr exception) noexcept { - std::fprintf(stderr, "Renderive scene structure failure during %s", stage ? stage : "unknown stage"); - if (exception) { - try { - std::rethrow_exception(exception); - } catch (const std::exception& value) { - std::fprintf(stderr, ": %s", value.what()); - } catch (...) { - std::fprintf(stderr, ": unknown exception"); - } - } - std::fprintf(stderr, "\n"); - std::fflush(stderr); - std::abort(); + if (raster_capabilities_ && !raster_capabilities_->valid()) + throw std::logic_error("display graph does not match attached renderables"); + return Scene_Edit_Error::none; } + void Scene_Base::request_render_graph_rebuild(Renderable_Base& renderable) { auto& renderable_data = renderable.d_func(); if (!renderable_data.real_time_data_state->attached.load( @@ -565,7 +824,8 @@ void Scene_Base::request_render_graph_rebuild(Renderable_Base& renderable) { renderable_data.reset_render_graph(); return; } - validate_renderable_scene(renderable); + if (renderable_scene_error(renderable) != Scene_Edit_Error::none) + throw std::logic_error("attached renderable scene invariant violated"); if (active_renderable_edit_scene_ == this) { renderable_data.reset_render_graph(); notify_model_dirty(); @@ -587,10 +847,11 @@ void Scene_Base::request_render_graph_rebuild(Renderable_Base& renderable) { return; } } - enqueue_renderable_edit([owner = std::move(owner)] { + static_cast(enqueue_renderable_edit([owner = std::move(owner)] { owner->d_func().reset_render_graph(); owner->d_func().notify_scene_model_dirty(); - }); + return Scene_Edit_Error::none; + })); } void Scene_Base::publish_frame_state() { auto task_lock = lock_render_idle(); @@ -600,12 +861,12 @@ void Scene_Base::publish_frame_state() { void Scene_Base::publish_frame_state(Abstract_Frame& frame) { auto task_lock = lock_render_idle(); publish_frame_state_locked(); - frame.published_snapshot_ = capture_live_frame(); + frame.state_->published_snapshot.store(capture_live_frame(), + std::memory_order_release); } void Scene_Base::publish_frame_state_locked() { - if (auto* state = dynamic_cast(this)) - state->publish(); + publish_scene_state(); std::vector renderables; { std::lock_guard lock(model_mutex_); @@ -616,8 +877,7 @@ void Scene_Base::publish_frame_state_locked() { } } for (const Renderable& renderable : renderables) { - if (auto* state = - dynamic_cast(renderable->d_ptr.get())) + if (auto* state = renderable->d_func().state_strategy) state->publish(); renderable->d_func().publish_real_time_data(); } @@ -657,12 +917,12 @@ Scene_Base::Topology_Snapshot Scene_Base::topology_snapshot() const { snapshot.renderables.push_back(renderables_.at(id)); const auto append_relationships = [&](const auto& resolution, auto& output) { + std::unordered_set children; + children.reserve(resolution.relationships.size()); + for (const auto& relationship : resolution.relationships) + children.insert(relationship.child); for (const Renderable_Id id : resolution.order) { - if (std::ranges::none_of( - resolution.relationships, - [id](const auto& relationship) { - return relationship.child == id; - })) + if (!children.contains(id)) output.push_back({renderables_.at(id), {}}); } for (const auto& relationship : resolution.relationships) @@ -789,15 +1049,21 @@ std::shared_ptr Scene_Base::find_render_plan( return render_plan_history_.find(version); } -Capture_Session_Id Scene_Base::capture_next_frame() { +Capture_Request_Result Scene_Base::capture_next_frame() { return capture_frames(1); } - -Capture_Session_Id Scene_Base::capture_frames(std::size_t count) { +Capture_Request_Result Scene_Base::capture_frames(std::size_t count) { std::lock_guard lock(task_mutex_); - const Capture_Session_Id session_id = capture_controller_.capture_frames(count); - capture_repository_.begin_session(session_id, count); - return session_id; + const auto request = capture_controller_.capture_frames(count); + if (!request) + return request; + try { + capture_repository_.begin_session(request.session_id, count); + } catch (...) { + capture_controller_.cancel(request.session_id); + throw; + } + return request; } Capture_Controller_State Scene_Base::capture_state() const noexcept { @@ -868,36 +1134,36 @@ void Scene_2D_Base::attach(Renderable_Id id) { if (!display_resolver_.contains(id)) display_resolver_.attach(id); } -void Scene_2D_Base::cleanup() { +bool Scene_2D_Base::cleanup() { for (const Renderable_Id id : display_resolver_.ids()) { if (renderables_.contains(id)) continue; if (!display_resolver_.isolated(id)) - throw std::logic_error( - "display graph still references detached renderable " + - std::to_string(id)); + return false; display_resolver_.erase(id); } + return true; } -void Scene_2D_Base::validate() const { +bool Scene_2D_Base::valid() const { const auto display = display_resolver_.resolve(); if (display.order.size() != renderables_.size()) - throw std::logic_error( - "display graph does not match attached renderables"); - for (const Renderable_Id id : display.order) { - if (!renderables_.contains(id)) - throw std::logic_error( - "display graph references detached renderable " + - std::to_string(id)); - } + return false; + return std::ranges::all_of(display.order, [this](Renderable_Id id) { + return renderables_.contains(id); + }); } + renderive::scene::dependency::Resolution Scene_2D_Base::resolve() const { return display_resolver_.resolve(); } +void Scene_2D_Base::restore( + const renderive::scene::dependency::Resolution& resolution) { + display_resolver_.assign(resolution); +} std::uint64_t Scene_Base::Impl::now_ns() const noexcept { - return 0; + return render_clock_now_ns(); } std::unique_lock Scene_Base::lock_render_idle() { @@ -940,39 +1206,64 @@ void Scene_Base::shutdown() noexcept { } scene_lifetime_->invalidate(); } -void Scene_Base::execute_renderable_edit(std::function edit) { +Scene_Edit_Error Scene_Base::execute_renderable_edit( + std::function edit) { std::lock_guard lock(model_mutex_); + Renderable_Edit_Transaction transaction(*this); + active_edit_transaction_ = &transaction; Scene_Base* previous = std::exchange(active_renderable_edit_scene_, this); try { - edit(); - cleanup_detached_topology_locked(); - validate_structure_locked(); - } catch (...) { + Scene_Edit_Error error = edit(); + if (error == Scene_Edit_Error::none) + error = cleanup_detached_topology_locked(); + if (error == Scene_Edit_Error::none) + error = validate_structure_locked(); + if (error != Scene_Edit_Error::none) + transaction.rollback(); active_renderable_edit_scene_ = previous; - structure_fail_fast("runtime renderable edit", std::current_exception()); + active_edit_transaction_ = nullptr; + return error; + } catch (...) { + const auto exception = std::current_exception(); + transaction.rollback(); + active_renderable_edit_scene_ = previous; + active_edit_transaction_ = nullptr; + std::rethrow_exception(exception); } - active_renderable_edit_scene_ = previous; } + void Scene_Base::execute_render_task(std::shared_ptr task) { Render_Execution_Scope scope(*this); const auto& snapshot = *task->snapshot; - d_func().dispatch({Observation_Event::render_started, d_func().now_ns(), snapshot.render_sequence, - snapshot.scene_state_revision_, snapshot.renderables.size(), task->topology, - task->snapshot, task->compiled_plan->plan}); + Scene_Render_Error error{}; std::exception_ptr exception; + bool render_graph_entered{}; try { - execute_render_graph(*task); - d_func().dispatch({Observation_Event::render_completed, d_func().now_ns(), snapshot.render_sequence, + d_func().dispatch({Observation_Event::render_started, d_func().now_ns(), snapshot.render_sequence, + snapshot.scene_state_revision_, snapshot.renderables.size(), task->topology, + task->snapshot, task->compiled_plan->plan}); + render_graph_entered = true; + error = execute_render_graph(*task); + const auto event = error == Scene_Render_Error::none + ? Observation_Event::render_completed + : Observation_Event::render_cancelled; + d_func().dispatch({event, d_func().now_ns(), snapshot.render_sequence, snapshot.scene_state_revision_, snapshot.renderables.size(), task->topology, task->snapshot, task->compiled_plan->plan}); } catch (...) { exception = std::current_exception(); - d_func().dispatch({Observation_Event::render_failed, d_func().now_ns(), snapshot.render_sequence, - snapshot.scene_state_revision_, snapshot.renderables.size(), task->topology, - task->snapshot, task->compiled_plan->plan}); + if (!render_graph_entered) + capture_controller_.finish_frame(snapshot.capture_ticket_, false); + try { + d_func().dispatch({Observation_Event::render_failed, d_func().now_ns(), snapshot.render_sequence, + snapshot.scene_state_revision_, snapshot.renderables.size(), task->topology, + task->snapshot, task->compiled_plan->plan}); + } catch (...) { + } } { std::lock_guard lock(task_mutex_); + task->completion->error = error; task->completion->exception = exception; task->completion->completed = true; } @@ -1176,17 +1467,10 @@ std::shared_ptr Scene_Base::compile_render_pla return compiled; } -void Scene_Base::execute_render_graph(Render_Task& task) { - Abstract_Frame* const frame = std::visit( - [](Frame& value) -> Abstract_Frame* { - if constexpr (std::same_as>) - return value.get(); - else - return &value.get(); - }, - task.frame); - if (!task.snapshot || !task.compiled_plan || !frame) +Scene_Render_Error Scene_Base::execute_render_graph(Render_Task& task) { + if (!task.snapshot || !task.compiled_plan || !task.frame) throw std::logic_error("render task is incomplete"); + auto& frame = *task.frame; const auto& snapshot = *task.snapshot; const Capture_Frame_Ticket capture_ticket = snapshot.capture_ticket_; bool frame_active{}; @@ -1198,16 +1482,17 @@ void Scene_Base::execute_render_graph(Render_Task& task) { const bool capture = capture_ticket.capture; const auto& compiled = *task.compiled_plan; - frame->begin_render(snapshot.render_sequence, *compiled.plan, capture, - capture ? render_clock_now_ns() : 0); + Abstract_Frame::begin_render(frame, snapshot.render_sequence, + *compiled.plan, capture, + capture ? render_clock_now_ns() : 0); frame_active = true; std::vector execution_slots( compiled.plan->graph.nodes.size()); for (const auto& node : compiled.plan->graph.nodes) execution_slots[node.execution_index] = - frame->execution_slot(node.execution_index); + Abstract_Frame::execution_slot(frame, node.execution_index); - compiled.runtime->execute( + const auto graph_error = compiled.runtime->execute( execution_slots, [this, &compiled, &snapshot, &execution_slots]( std::size_t execution_index, @@ -1263,7 +1548,12 @@ void Scene_Base::execute_render_graph(Render_Task& task) { } throw std::logic_error("unknown render node kind"); }); - + if (graph_error != renderive::render_graph::detail::Render_Graph_Execution_Error::none) { + Abstract_Frame::discard_render(frame); + frame_active = false; + capture_controller_.finish_frame(capture_ticket, false); + return scene_render_error(graph_error); + } for (const auto& state : snapshot.renderables) { if (state.prepare_required) state.owner_->d_func().mark_prepared(state.prepare_revision); @@ -1271,8 +1561,8 @@ void Scene_Base::execute_render_graph(Render_Task& task) { state.owner_->d_func().mark_painted(state.paint_revision, state.prepare_revision); } - const auto completed = frame->complete_render( - capture ? render_clock_now_ns() : 0); + const auto completed = Abstract_Frame::complete_render( + frame, capture ? render_clock_now_ns() : 0); frame_active = false; for (const auto& state : snapshot.renderables) { if (!state.paint_required) @@ -1289,19 +1579,23 @@ void Scene_Base::execute_render_graph(Render_Task& task) { analyze_frame(*compiled.plan, *completed)); } capture_controller_.finish_frame(capture_ticket, true); + return Scene_Render_Error::none; } catch (...) { if (frame_active) - frame->discard_render(); + Abstract_Frame::discard_render(frame); capture_controller_.finish_frame(capture_ticket, false); throw; } } -void Scene_Base::validate_renderable_scene( - const Renderable_Base& renderable) const { - if (renderable.d_func().real_time_data_state->scene_lifetime.get() != - scene_lifetime_.get()) - throw std::invalid_argument("renderable belongs to another scene"); +Scene_Edit_Error Scene_Base::renderable_scene_error( + const Renderable_Base& renderable) const noexcept { + const auto& lifetime = renderable.d_func().real_time_data_state->scene_lifetime; + if (!lifetime) + return Scene_Edit_Error::renderable_not_attached; + return lifetime.get() == scene_lifetime_.get() + ? Scene_Edit_Error::none + : Scene_Edit_Error::foreign_renderable; } bool Scene_Base::is_renderable_attached_locked( diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index 2dda39b..cfd274c 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -34,14 +35,37 @@ class Color_Cache; +enum class Scene_Render_Error : std::uint8_t { + none, + cancelled, + deadline_exceeded, + external_failure, + shutting_down +}; +enum class Scene_Edit_Error : std::uint8_t { + none, + empty_edit, + null_renderable, + foreign_renderable, + renderable_not_attached, + dependency_self_reference, + dependency_cycle, + display_self_reference, + display_cycle, + dangling_dependency, + dangling_display, + shutting_down +}; + namespace renderive::scene::detail { struct Composition_Relationships { virtual ~Composition_Relationships() = default; virtual void attach(Renderable_Id id) = 0; - virtual void cleanup() = 0; - virtual void validate() const = 0; + [[nodiscard]] virtual bool cleanup() = 0; + [[nodiscard]] virtual bool valid() const = 0; virtual dependency::Resolution resolve() const = 0; + virtual void restore(const dependency::Resolution& resolution) = 0; }; struct Raster_Capabilities : Composition_Relationships { @@ -86,15 +110,18 @@ public: class Renderable_Editor : public renderive::scene_inheritance::Editor_Root { public: - void attach(Renderable renderable); - void detach(const Renderable& renderable); - void set_dependency_parent(const Renderable& child, const Renderable& parent); - void add_dependency_parent(const Renderable& child, const Renderable& parent); - void clear_dependency_parent(const Renderable& child); + [[nodiscard]] Scene_Edit_Error attach(Renderable renderable); + [[nodiscard]] Scene_Edit_Error detach(const Renderable& renderable); + [[nodiscard]] Scene_Edit_Error set_dependency_parent(const Renderable& child, const Renderable& parent); + [[nodiscard]] Scene_Edit_Error add_dependency_parent(const Renderable& child, const Renderable& parent); + [[nodiscard]] Scene_Edit_Error clear_dependency_parent(const Renderable& child); + [[nodiscard]] Scene_Edit_Error error() const noexcept { return error_; } protected: explicit Renderable_Editor(Scene_Base& scene) : scene_(scene) {} Scene_Base& scene_; + Scene_Edit_Error fail(Scene_Edit_Error error) noexcept; private: + Scene_Edit_Error error_{}; friend class Scene_Base; friend class Attach_Builder; }; @@ -106,11 +133,10 @@ public: Attach_Builder& operator=(const Attach_Builder&) = delete; Attach_Builder(Attach_Builder&&) = delete; Attach_Builder& operator=(Attach_Builder&&) = delete; - ~Attach_Builder(); - void attach(Renderable renderable); - void set_dependency_parent(const Renderable& child, const Renderable& parent); - void add_dependency_parent(const Renderable& child, const Renderable& parent); - void clear_dependency_parent(const Renderable& child); + [[nodiscard]] Scene_Edit_Error attach(Renderable renderable); + [[nodiscard]] Scene_Edit_Error set_dependency_parent(const Renderable& child, const Renderable& parent); + [[nodiscard]] Scene_Edit_Error add_dependency_parent(const Renderable& child, const Renderable& parent); + [[nodiscard]] Scene_Edit_Error clear_dependency_parent(const Renderable& child); protected: explicit Attach_Builder(Scene_Base& scene); Scene_Base& scene_; @@ -121,6 +147,19 @@ public: using Renderable_Edit = std::function; + class Edit_Operation { + public: + Edit_Operation() = default; + [[nodiscard]] Scene_Edit_Error wait() const; + private: + explicit Edit_Operation(std::shared_future result) + : result_(std::move(result)) {} + std::shared_future result_; + friend class Scene_Base; + friend class Scene_2D_Base; + friend class Scene_3D_Base; + }; + struct Topology_Relationship { Const_Renderable child; Const_Renderable parent; @@ -136,6 +175,7 @@ public: render_submitted, render_started, render_completed, + render_cancelled, render_failed }; @@ -158,15 +198,15 @@ public: Scene_Base& operator=(Scene_Base&&) = delete; virtual ~Scene_Base(); - void render(); - void render(Abstract_Frame& frame); - void wait_for_render(); + Scene_Render_Error render(); + Scene_Render_Error render(Abstract_Frame& frame); + Scene_Render_Error wait_for_render(); void publish_frame_state(); void publish_frame_state(Abstract_Frame& frame); void notify_model_dirty() noexcept; Attach_Builder attach_builder(); - void edit_renderables(Renderable_Edit edit); + [[nodiscard]] Edit_Operation edit_renderables(Renderable_Edit edit); [[nodiscard]] std::size_t renderable_count() const; [[nodiscard]] Topology_Snapshot topology_snapshot() const; @@ -175,8 +215,8 @@ public: [[nodiscard]] std::shared_ptr find_render_plan( Render_Plan_Version version) const; - Capture_Session_Id capture_next_frame(); - Capture_Session_Id capture_frames(std::size_t count); + [[nodiscard]] Capture_Request_Result capture_next_frame(); + [[nodiscard]] Capture_Request_Result capture_frames(std::size_t count); [[nodiscard]] Capture_Controller_State capture_state() const noexcept; [[nodiscard]] std::optional capture_session( Capture_Session_Id session_id) const; @@ -211,6 +251,7 @@ protected: virtual Frame_Control_Strategy_Base& frame_control_strategy_impl() = 0; virtual const Frame_Control_Strategy_Base& frame_control_strategy_impl() const = 0; + virtual void publish_scene_state() = 0; virtual std::uint64_t acquire_scene_state() = 0; virtual void capture_scene_state(Frame_Render_Snapshot& snapshot) const = 0; std::unique_lock lock_render_idle(); @@ -218,7 +259,7 @@ protected: bool consume_model_dirty() noexcept; void invalidate_renderables(); void shutdown() noexcept; - void enqueue_renderable_edit(std::function edit); + [[nodiscard]] Edit_Operation enqueue_renderable_edit(std::function edit); private: friend class Renderable_Base; @@ -240,25 +281,29 @@ private: struct Compiled_Render_Plan; struct Render_Task { - using Frame = std::variant, - std::reference_wrapper>; std::shared_ptr snapshot; std::shared_ptr compiled_plan; std::shared_ptr topology; - Frame frame; + std::shared_ptr frame; std::shared_ptr completion; }; class Execution_Context; class Render_Execution_Scope; + class Renderable_Edit_Transaction; + struct Render_Completion { + Scene_Render_Error error{}; std::exception_ptr exception; bool completed{}; bool observed{}; }; - void submit_render(Abstract_Frame* frame); + [[nodiscard]] Scene_Render_Error submit_render(Abstract_Frame* frame); void submit_operation(std::function operation); + void complete_pending_operation() noexcept; + void record_pending_exception(std::exception_ptr exception) noexcept; + void capture_edit_renderable(const Renderable& renderable); void request_render_graph_rebuild(Renderable_Base& renderable); [[nodiscard]] std::shared_ptr snapshot_live_model(); [[nodiscard]] std::shared_ptr @@ -267,16 +312,15 @@ private: std::shared_ptr compile_render_plan( const Frame_Render_Snapshot& snapshot); void execute_render_task(std::shared_ptr task); - void execute_renderable_edit(std::function edit); - void execute_render_graph(Render_Task& task); - void validate_renderable_scene(const Renderable_Base& renderable) const; + [[nodiscard]] Scene_Edit_Error execute_renderable_edit(std::function edit); + [[nodiscard]] Scene_Render_Error execute_render_graph(Render_Task& task); + [[nodiscard]] Scene_Edit_Error renderable_scene_error(const Renderable_Base& renderable) const noexcept; [[nodiscard]] bool is_renderable_attached_locked( const Renderable& renderable) const; - void validate_structure_locked(); - void cleanup_detached_topology_locked(); + [[nodiscard]] Scene_Edit_Error validate_structure_locked() const; + [[nodiscard]] Scene_Edit_Error cleanup_detached_topology_locked(); void bind_raster_capabilities( renderive::scene::detail::Raster_Capabilities& capabilities) noexcept; - [[noreturn]] static void structure_fail_fast(const char* stage, std::exception_ptr exception = {}) noexcept; inline static thread_local Scene_Base* active_execution_scene_{}; inline static thread_local Scene_Base* active_renderable_edit_scene_{}; @@ -298,12 +342,12 @@ private: std::condition_variable_any render_completed_; std::shared_ptr current_completion_; std::exception_ptr pending_exception_; - bool pending_exception_observed_{}; std::size_t deferred_render_count_{}; std::uint64_t render_sequence_{}; std::size_t pending_operations_{}; bool runtime_started_{}; bool shutting_down_{}; + Renderable_Edit_Transaction* active_edit_transaction_{}; const Render_Node_Id composite_begin_node_id_; const Render_Node_Id scene_render_node_id_; @@ -321,9 +365,9 @@ public: : public renderive::scene_inheritance::Editor_Node< Renderable_Editor, Scene_Base::Renderable_Editor> { public: - void set_display_parent(const Renderable& child, const Renderable& parent); - void add_display_parent(const Renderable& child, const Renderable& parent); - void clear_display_parent(const Renderable& child); + [[nodiscard]] Scene_Edit_Error set_display_parent(const Renderable& child, const Renderable& parent); + [[nodiscard]] Scene_Edit_Error add_display_parent(const Renderable& child, const Renderable& parent); + [[nodiscard]] Scene_Edit_Error clear_display_parent(const Renderable& child); private: explicit Renderable_Editor(Scene_2D_Base& scene) : renderive::scene_inheritance::Editor_Node< @@ -338,9 +382,9 @@ public: : public renderive::scene_inheritance::Builder_Node< Attach_Builder, Scene_Base::Attach_Builder> { public: - void set_display_parent(const Renderable& child, const Renderable& parent); - void add_display_parent(const Renderable& child, const Renderable& parent); - void clear_display_parent(const Renderable& child); + [[nodiscard]] Scene_Edit_Error set_display_parent(const Renderable& child, const Renderable& parent); + [[nodiscard]] Scene_Edit_Error add_display_parent(const Renderable& child, const Renderable& parent); + [[nodiscard]] Scene_Edit_Error clear_display_parent(const Renderable& child); private: explicit Attach_Builder(Scene_2D_Base& scene) : renderive::scene_inheritance::Builder_Node< @@ -355,7 +399,7 @@ public: Scene_2D_Base(); explicit Scene_2D_Base(std::pmr::memory_resource& memory_resource); Attach_Builder attach_builder(); - void edit_renderables(Renderable_Edit edit); + [[nodiscard]] Edit_Operation edit_renderables(Renderable_Edit edit); protected: struct State_Layer @@ -374,16 +418,17 @@ protected: private: friend class Scene_Base; void with_initial_display_edit(std::function edit); - void set_display_parent_locked(const Renderable& child, + [[nodiscard]] Scene_Edit_Error set_display_parent_locked(const Renderable& child, const Renderable& parent); - void add_display_parent_locked(const Renderable& child, + [[nodiscard]] Scene_Edit_Error add_display_parent_locked(const Renderable& child, const Renderable& parent); - void clear_display_parent_locked(const Renderable& child); + [[nodiscard]] Scene_Edit_Error clear_display_parent_locked(const Renderable& child); void attach(Renderable_Id id) override; - void cleanup() override; - void validate() const override; + [[nodiscard]] bool cleanup() override; + [[nodiscard]] bool valid() const override; renderive::scene::dependency::Resolution resolve() const override; + void restore(const renderive::scene::dependency::Resolution& resolution) override; renderive::scene::dependency::Dependency_Resolver display_resolver_; @@ -415,7 +460,7 @@ public: Scene_3D_Base(); explicit Scene_3D_Base(std::pmr::memory_resource& memory_resource); Attach_Builder attach_builder(); - void edit_renderables(Renderable_Edit edit); + [[nodiscard]] Edit_Operation edit_renderables(Renderable_Edit edit); protected: struct State_Layer diff --git a/Kernel/src/renderive/scene/concept/Scene.hpp b/Kernel/src/renderive/scene/concept/Scene.hpp index c781588..8a0a0c6 100644 --- a/Kernel/src/renderive/scene/concept/Scene.hpp +++ b/Kernel/src/renderive/scene/concept/Scene.hpp @@ -5,8 +5,8 @@ template concept Scene = std::derived_from && requires(That& scene, const That& const_scene) { { scene.frame_control_strategy() } -> std::same_as; { const_scene.frame_control_strategy() } -> std::same_as; - { scene.render() } -> std::same_as; - { scene.wait_for_render() } -> std::same_as; + { scene.render() } -> std::same_as; + { scene.wait_for_render() } -> std::same_as; }; template concept Scene_2D = Scene && std::derived_from; diff --git a/Kernel/src/renderive/scene/dependency/Dependency_Resolver.hpp b/Kernel/src/renderive/scene/dependency/Dependency_Resolver.hpp index 05c0ac8..85170d4 100644 --- a/Kernel/src/renderive/scene/dependency/Dependency_Resolver.hpp +++ b/Kernel/src/renderive/scene/dependency/Dependency_Resolver.hpp @@ -1,5 +1,4 @@ #pragma once - #include #include #include @@ -9,35 +8,50 @@ #include #include #include - namespace renderive::scene::dependency { - template struct Relationship { Id child{}; Id parent{}; bool operator==(const Relationship&) const = default; }; - template struct Resolution { std::vector order; std::vector> relationships; }; - -// Stores only declared relationships. Order and relationship snapshots are -// derived on demand, so mutations have one authoritative source. +enum class Mutation_Error : unsigned char { + none, + endpoint_not_attached, + self_reference, + cycle +}; +struct Mutation_Result { + Mutation_Error error{}; + bool changed{}; + [[nodiscard]] explicit operator bool() const noexcept { + return error == Mutation_Error::none; + } +}; template > class Dependency_Resolver { public: explicit Dependency_Resolver(std::pmr::memory_resource& resource) : nodes_(&resource) {} - + void assign(const Resolution& resolution) { + nodes_.clear(); + for (const Id id : resolution.order) + attach(id); + for (const auto& relationship : resolution.relationships) { + const auto result = add_parent(relationship.child, relationship.parent); + if (!result) + throw std::logic_error("invalid dependency resolution"); + } + } void attach(Id id) { if (!nodes_.try_emplace(id, resource()).second) throw std::logic_error("dependency id is already attached"); } - void erase(Id id) { validate_endpoint(id); for (auto& [node_id, node] : nodes_) { @@ -47,59 +61,58 @@ public: } nodes_.erase(id); } - - bool replace_parents(Id child, const std::vector& parents) { - validate_endpoint(child); + [[nodiscard]] Mutation_Result replace_parents(Id child, const std::vector& parents) { + if (!nodes_.contains(child)) + return {Mutation_Error::endpoint_not_attached, false}; std::vector unique; unique.reserve(parents.size()); for (const Id parent : parents) { - validate_relationship(child, parent); + if (!nodes_.contains(parent)) + return {Mutation_Error::endpoint_not_attached, false}; + if (child == parent) + return {Mutation_Error::self_reference, false}; if (std::find(unique.begin(), unique.end(), parent) == unique.end()) unique.push_back(parent); } std::sort(unique.begin(), unique.end()); auto& current = nodes_.at(child).parents; - if (std::equal(current.begin(), current.end(), unique.begin(), - unique.end())) - return false; + if (std::equal(current.begin(), current.end(), unique.begin(), unique.end())) + return {Mutation_Error::none, false}; const std::vector previous(current.begin(), current.end()); assign_parents(child, unique); - try { - validate_acyclic(); - } catch (...) { + if (!acyclic()) { assign_parents(child, previous); - throw; + return {Mutation_Error::cycle, false}; } - return true; + return {Mutation_Error::none, true}; } - - bool add_parent(Id child, Id parent) { - validate_relationship(child, parent); - auto parents = std::vector(nodes_.at(child).parents.begin(), - nodes_.at(child).parents.end()); - if (std::find(parents.begin(), parents.end(), parent) != parents.end()) - return false; + [[nodiscard]] Mutation_Result add_parent(Id child, Id parent) { + if (!nodes_.contains(child) || !nodes_.contains(parent)) + return {Mutation_Error::endpoint_not_attached, false}; + if (child == parent) + return {Mutation_Error::self_reference, false}; + const auto& current = nodes_.at(child).parents; + if (std::find(current.begin(), current.end(), parent) != current.end()) + return {Mutation_Error::none, false}; + auto parents = std::vector(current.begin(), current.end()); parents.push_back(parent); return replace_parents(child, parents); } - - bool clear_parents(Id child) { - validate_endpoint(child); + [[nodiscard]] Mutation_Result clear_parents(Id child) { + if (!nodes_.contains(child)) + return {Mutation_Error::endpoint_not_attached, false}; if (nodes_.at(child).parents.empty()) - return false; + return {Mutation_Error::none, false}; return replace_parents(child, {}); } - [[nodiscard]] bool contains(Id id) const noexcept { return nodes_.contains(id); } - [[nodiscard]] bool isolated(Id id) const { validate_endpoint(id); const auto& node = nodes_.at(id); return node.parents.empty() && node.children.empty(); } - [[nodiscard]] std::vector ids() const { std::vector result; result.reserve(nodes_.size()); @@ -110,13 +123,11 @@ public: std::sort(result.begin(), result.end()); return result; } - [[nodiscard]] std::vector parents(Id id) const { validate_endpoint(id); const auto& values = nodes_.at(id).parents; return {values.begin(), values.end()}; } - [[nodiscard]] Resolution resolve() const { Resolution result; result.relationships.reserve(edge_count()); @@ -124,7 +135,6 @@ public: for (const Id parent : nodes_.at(child).parents) result.relationships.push_back({child, parent}); } - std::unordered_map indegree; indegree.reserve(nodes_.size()); std::priority_queue, std::greater> ready; @@ -148,7 +158,6 @@ public: throw std::logic_error("dependency graph contains a cycle"); return result; } - private: struct Node { explicit Node(std::pmr::memory_resource* resource) @@ -156,25 +165,35 @@ private: std::pmr::vector parents; std::pmr::vector children; }; - [[nodiscard]] std::pmr::memory_resource* resource() const noexcept { return nodes_.get_allocator().resource(); } - void validate_endpoint(Id id) const { if (!nodes_.contains(id)) - throw std::invalid_argument("dependency endpoint is not attached"); + throw std::logic_error("dependency endpoint invariant violated"); } - - void validate_relationship(Id child, Id parent) const { - validate_endpoint(child); - validate_endpoint(parent); - if (child == parent) - throw std::invalid_argument("dependency cannot reference itself"); + [[nodiscard]] bool acyclic() const { + std::unordered_map indegree; + indegree.reserve(nodes_.size()); + std::queue ready; + for (const auto& [id, node] : nodes_) { + indegree.emplace(id, node.parents.size()); + if (node.parents.empty()) + ready.push(id); + } + std::size_t visited{}; + while (!ready.empty()) { + const Id id = ready.front(); + ready.pop(); + ++visited; + for (const Id child : nodes_.at(id).children) { + auto& degree = indegree.at(child); + if (--degree == 0) + ready.push(child); + } + } + return visited == nodes_.size(); } - - void validate_acyclic() const { static_cast(resolve()); } - void assign_parents(Id child, const std::vector& parents) { auto& current = nodes_.at(child).parents; for (const Id parent : current) @@ -182,12 +201,10 @@ private: current.assign(parents.begin(), parents.end()); for (const Id parent : current) { auto& children = nodes_.at(parent).children; - if (std::find(children.begin(), children.end(), child) == - children.end()) + if (std::find(children.begin(), children.end(), child) == children.end()) children.push_back(child); } } - [[nodiscard]] std::size_t edge_count() const noexcept { std::size_t result{}; for (const auto& [id, node] : nodes_) { @@ -196,13 +213,9 @@ private: } return result; } - static void erase_id(std::pmr::vector& values, Id id) noexcept { - values.erase(std::remove(values.begin(), values.end(), id), - values.end()); + values.erase(std::remove(values.begin(), values.end(), id), values.end()); } - std::pmr::unordered_map nodes_; }; - } // namespace renderive::scene::dependency diff --git a/Kernel/src/renderive/state/Concepts.hpp b/Kernel/src/renderive/state/Concepts.hpp index 1e11767..bbd6550 100644 --- a/Kernel/src/renderive/state/Concepts.hpp +++ b/Kernel/src/renderive/state/Concepts.hpp @@ -9,13 +9,13 @@ concept Has_State_Validator = requires(const State& state) { { Product::validate_state(state) } -> std::same_as; }; template -concept Double_State_Storage_Type = std::derived_from && requires(That& strategy, const That& const_strategy) { +concept Published_State_Storage_Type = std::derived_from && requires(That& strategy, const That& const_strategy) { typename That::State; typename That::Observation; { strategy.publish() } -> std::same_as; { const_strategy.state_revision() } -> std::same_as; }; template -concept Triple_State_Storage_Type = Double_State_Storage_Type && requires(That& strategy) { +concept Render_Acquired_State_Storage_Type = Published_State_Storage_Type && requires(That& strategy) { { strategy.acquire_render_state() } -> std::same_as; }; diff --git a/Kernel/src/renderive/state/Double_State_Storage.hpp b/Kernel/src/renderive/state/Published_State_Storage.hpp similarity index 92% rename from Kernel/src/renderive/state/Double_State_Storage.hpp rename to Kernel/src/renderive/state/Published_State_Storage.hpp index 664ad3e..a3fa6bc 100644 --- a/Kernel/src/renderive/state/Double_State_Storage.hpp +++ b/Kernel/src/renderive/state/Published_State_Storage.hpp @@ -17,8 +17,8 @@ template > -struct Double_State_Storage : State_Strategy_Base { - using Self = Double_State_Storage; +struct Published_State_Storage : State_Strategy_Base { + using Self = Published_State_Storage; using State = State_Type; enum class Observation_Event { @@ -36,21 +36,21 @@ struct Double_State_Storage : State_Strategy_Base { static_assert(Timed_Struct_Observer); - Double_State_Storage() requires std::default_initializable + Published_State_Storage() requires std::default_initializable : states{}, render_state(&states[0]), cache_state(&states[1]), scratch_state(&states[2]) {} - explicit Double_State_Storage(With_Observer option) + explicit Published_State_Storage(With_Observer option) requires std::default_initializable : observer(std::move(option.observer)), states{}, render_state(&states[0]), cache_state(&states[1]), scratch_state(&states[2]) {} - explicit Double_State_Storage(const State& state) + explicit Published_State_Storage(const State& state) : states{state, state, state}, render_state(&states[0]), cache_state(&states[1]), scratch_state(&states[2]) {} - Double_State_Storage(const State& state, With_Observer option) + Published_State_Storage(const State& state, With_Observer option) : observer(std::move(option.observer)), states{state, state, state}, render_state(&states[0]), cache_state(&states[1]), scratch_state(&states[2]) {} diff --git a/Kernel/src/renderive/state/Triple_State_Storage.hpp b/Kernel/src/renderive/state/Render_Acquired_State_Storage.hpp similarity index 91% rename from Kernel/src/renderive/state/Triple_State_Storage.hpp rename to Kernel/src/renderive/state/Render_Acquired_State_Storage.hpp index 8b1a851..06462ed 100644 --- a/Kernel/src/renderive/state/Triple_State_Storage.hpp +++ b/Kernel/src/renderive/state/Render_Acquired_State_Storage.hpp @@ -16,8 +16,8 @@ template > -struct Triple_State_Storage : State_Strategy_Base { - using Self = Triple_State_Storage; +struct Render_Acquired_State_Storage : State_Strategy_Base { + using Self = Render_Acquired_State_Storage; using State = State_Type; enum class Observation_Event { @@ -37,22 +37,22 @@ struct Triple_State_Storage : State_Strategy_Base { static_assert(Timed_Struct_Observer); - Triple_State_Storage() requires std::default_initializable + Render_Acquired_State_Storage() requires std::default_initializable : states{}, render_state(&states[0]), published_state(&states[1]), cache_state(&states[2]), scratch_state(&states[3]) {} - explicit Triple_State_Storage(With_Observer option) + explicit Render_Acquired_State_Storage(With_Observer option) requires std::default_initializable : observer(std::move(option.observer)), states{}, render_state(&states[0]), published_state(&states[1]), cache_state(&states[2]), scratch_state(&states[3]) {} - explicit Triple_State_Storage(const State& state) + explicit Render_Acquired_State_Storage(const State& state) : states{state, state, state, state}, render_state(&states[0]), published_state(&states[1]), cache_state(&states[2]), scratch_state(&states[3]) {} - Triple_State_Storage(const State& state, With_Observer option) + Render_Acquired_State_Storage(const State& state, With_Observer option) : observer(std::move(option.observer)), states{state, state, state, state}, render_state(&states[0]), published_state(&states[1]), cache_state(&states[2]), diff --git a/Kernel/src/renderive/state/State_Strategy.hpp b/Kernel/src/renderive/state/State_Strategy.hpp index ce1905d..154d8e5 100644 --- a/Kernel/src/renderive/state/State_Strategy.hpp +++ b/Kernel/src/renderive/state/State_Strategy.hpp @@ -1,7 +1,7 @@ #pragma once #include "Concepts.hpp" -#include "Double_State_Storage.hpp" +#include "Published_State_Storage.hpp" #include "Stateful_Impl.hpp" #include "Optional_State_Validator.hpp" -#include "Triple_State_Storage.hpp" +#include "Render_Acquired_State_Storage.hpp" #include "base/State_Strategy_Base.hpp" diff --git a/Kernel/src/renderive/state/Stateful_Impl.hpp b/Kernel/src/renderive/state/Stateful_Impl.hpp index a2d458e..18b06ad 100644 --- a/Kernel/src/renderive/state/Stateful_Impl.hpp +++ b/Kernel/src/renderive/state/Stateful_Impl.hpp @@ -1,6 +1,6 @@ #pragma once -#include "Double_State_Storage.hpp" +#include "Published_State_Storage.hpp" #include "renderive/inheritance/Inheritance.hpp" #include @@ -9,15 +9,17 @@ template > struct Stateful_Impl : Implementation, - Double_State_Storage { - using State_Storage = Double_State_Storage; + Published_State_Storage { + using State_Storage = Published_State_Storage; template Stateful_Impl(const State& state, With_Observer observer, Implementation_Args&&... implementation_args) : Implementation( std::forward(implementation_args)...), - State_Storage(state, std::move(observer)) {} + State_Storage(state, std::move(observer)) { + this->state_strategy = static_cast(this); + } void publish() override { State_Storage::publish(); diff --git a/Kernel/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp b/Kernel/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp index 649999a..53f8791 100644 --- a/Kernel/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp +++ b/Kernel/tests/renderive/frame_control/base/Frame_Control_Strategy_Base_Test.cpp @@ -5,7 +5,7 @@ struct Frame_Control_Base_Test_Frame {}; using Frame_Control_Base_Test_Strategy = Low_Latency_Strategy; TEST(frame_control_strategy_base_test, publishes_cached_state_only_on_swap) { Frame_Control_Base_Test_Strategy strategy; - strategy.set_frequency_hz(120.0); + EXPECT_EQ(strategy.set_frequency_hz(120.0), Frame_Control_Base_Test_Strategy::Control_Error::none); { auto frame = strategy.acquire_painter(); ASSERT_TRUE(frame); diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp index e1be105..a5676bc 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Pipeline_Test.cpp @@ -35,7 +35,7 @@ TEST(low_latency_pipeline_test, classifies_frequency_paint_and_render_limits_acr Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(100.0); + EXPECT_EQ(strategy.set_frequency_hz(100.0), Low_Latency_Test_Strategy::Control_Error::none); complete_pipeline(strategy, time_source, 1, 2'000'000, 500'000, 3'000'000); auto state = strategy.state(); EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited); @@ -43,7 +43,7 @@ TEST(low_latency_pipeline_test, classifies_frequency_paint_and_render_limits_acr EXPECT_EQ(state.bottleneck_duration_ns, 3'000'000u); EXPECT_EQ(state.next_refresh_interval_ns, 10'000'000u); - strategy.set_frequency_hz(1'000.0); + EXPECT_EQ(strategy.set_frequency_hz(1'000.0), Low_Latency_Test_Strategy::Control_Error::none); complete_pipeline(strategy, time_source, 2, 4'000'000, 250'000, 2'000'000); state = strategy.state(); EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::paint_limited); @@ -65,8 +65,8 @@ TEST(low_latency_pipeline_test, consumer_feedback_limits_refresh_without_knowing Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(100.0); - strategy.set_consumer_feedback({40'000'000}); + EXPECT_EQ(strategy.set_frequency_hz(100.0), Low_Latency_Test_Strategy::Control_Error::none); + EXPECT_EQ(strategy.set_consumer_feedback({40'000'000}), Low_Latency_Test_Strategy::Control_Error::none); complete_pipeline(strategy, time_source, 1, 2'000'000, 0, 3'000'000); auto state = strategy.state(); EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::consumer_limited); @@ -84,7 +84,7 @@ TEST(low_latency_pipeline_test, applies_frequency_and_equal_bottleneck_boundarie Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(1'000.0); + EXPECT_EQ(strategy.set_frequency_hz(1'000.0), Low_Latency_Test_Strategy::Control_Error::none); complete_pipeline(strategy, time_source, 1, 1'000'000, 0, 999'999); auto state = strategy.state(); @@ -102,7 +102,7 @@ TEST(low_latency_pipeline_test, accepts_one_billion_hz_and_uses_the_measured_bot Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(1'000'000'000.0); + EXPECT_EQ(strategy.set_frequency_hz(1'000'000'000.0), Low_Latency_Test_Strategy::Control_Error::none); complete_pipeline(strategy, time_source, 7, 20, 10, 30); const auto state = strategy.state(); @@ -117,7 +117,7 @@ TEST(low_latency_pipeline_test, reports_exact_pipeline_timing_and_state_to_obser Low_Latency_Test_Observer observer; auto data = observer.data; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(500.0); + EXPECT_EQ(strategy.set_frequency_hz(500.0), Low_Latency_Test_Strategy::Control_Error::none); complete_pipeline(strategy, time_source, 42, 3'000'000, 700'000, 4'000'000); const auto observations = low_latency_observations(data); diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp index a8a78d8..56eab3a 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp @@ -135,7 +135,7 @@ TEST(low_latency_strategy_test, updates_frequency_after_completed_lifecycle) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(100.0); + EXPECT_EQ(strategy.set_frequency_hz(100.0), Low_Latency_Test_Strategy::Control_Error::none); EXPECT_DOUBLE_EQ(strategy.state().frequency_hz, 100.0); { auto frame = strategy.acquire_painter(); @@ -155,7 +155,7 @@ TEST(low_latency_strategy_test, detects_render_limited_state) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(1'000'000.0); + EXPECT_EQ(strategy.set_frequency_hz(1'000'000.0), Low_Latency_Test_Strategy::Control_Error::none); { auto frame = strategy.acquire_painter(); frame->value = 5; @@ -173,14 +173,16 @@ TEST(low_latency_strategy_test, rejects_non_finite_frequency_configuration) { EXPECT_THROW((Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source), {std::numeric_limits::quiet_NaN()})), std::invalid_argument); EXPECT_THROW((Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source), {std::numeric_limits::infinity()})), std::invalid_argument); auto strategy = make_low_latency_test_strategy(time_source, observer); - EXPECT_THROW(strategy.set_frequency_hz(std::numeric_limits::quiet_NaN()), std::invalid_argument); - EXPECT_THROW(strategy.set_frequency_hz(std::numeric_limits::infinity()), std::invalid_argument); + EXPECT_EQ(strategy.set_frequency_hz(std::numeric_limits::quiet_NaN()), + Low_Latency_Test_Strategy::Control_Error::invalid_frequency); + EXPECT_EQ(strategy.set_frequency_hz(std::numeric_limits::infinity()), + Low_Latency_Test_Strategy::Control_Error::invalid_frequency); } TEST(low_latency_strategy_test, clamps_extremely_small_frequency_interval_without_overflow) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(1e-300); + EXPECT_EQ(strategy.set_frequency_hz(1e-300), Low_Latency_Test_Strategy::Control_Error::none); { auto frame = strategy.acquire_painter(); frame->value = 1; @@ -195,8 +197,8 @@ TEST(low_latency_strategy_test, separates_consumer_rate_from_jitter_safety_margi Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(1000.0); - strategy.set_consumer_feedback({10'000'000}); + EXPECT_EQ(strategy.set_frequency_hz(1000.0), Low_Latency_Test_Strategy::Control_Error::none); + EXPECT_EQ(strategy.set_consumer_feedback({10'000'000}), Low_Latency_Test_Strategy::Control_Error::none); auto state = strategy.state(); EXPECT_TRUE(state.consumer_feedback_enabled); EXPECT_EQ(state.consumer_sample_interval_ns, 10'000'000u); @@ -204,7 +206,7 @@ TEST(low_latency_strategy_test, separates_consumer_rate_from_jitter_safety_margi EXPECT_EQ(state.consumer_variation_ns, 0u); EXPECT_EQ(state.consumer_safety_interval_ns, 10'000'000u); EXPECT_EQ(state.consumer_interval_ns, 10'000'000u); - strategy.set_consumer_feedback({14'000'000}); + EXPECT_EQ(strategy.set_consumer_feedback({14'000'000}), Low_Latency_Test_Strategy::Control_Error::none); state = strategy.state(); EXPECT_EQ(state.consumer_sample_interval_ns, 14'000'000u); EXPECT_EQ(state.consumer_smoothed_interval_ns, 12'000'000u); @@ -218,10 +220,10 @@ TEST(low_latency_strategy_test, consumer_jitter_changes_safety_deadline_without_ Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(1000.0); - strategy.set_consumer_feedback({20'000'000}); - strategy.set_consumer_feedback({22'000'000}); - strategy.set_consumer_feedback({20'000'000}); + EXPECT_EQ(strategy.set_frequency_hz(1000.0), Low_Latency_Test_Strategy::Control_Error::none); + EXPECT_EQ(strategy.set_consumer_feedback({20'000'000}), Low_Latency_Test_Strategy::Control_Error::none); + EXPECT_EQ(strategy.set_consumer_feedback({22'000'000}), Low_Latency_Test_Strategy::Control_Error::none); + EXPECT_EQ(strategy.set_consumer_feedback({20'000'000}), Low_Latency_Test_Strategy::Control_Error::none); const auto state = strategy.state(); EXPECT_EQ(state.consumer_sample_interval_ns, 20'000'000u); EXPECT_EQ(state.consumer_smoothed_interval_ns, 20'875'000u); @@ -234,8 +236,8 @@ TEST(low_latency_strategy_test, allows_frequency_and_consumer_limits_to_be_clear Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); - strategy.set_frequency_hz(1000.0); - strategy.set_consumer_feedback({20'000'000}); + EXPECT_EQ(strategy.set_frequency_hz(1000.0), Low_Latency_Test_Strategy::Control_Error::none); + EXPECT_EQ(strategy.set_consumer_feedback({20'000'000}), Low_Latency_Test_Strategy::Control_Error::none); strategy.clear_consumer_feedback(); auto state = strategy.state(); EXPECT_FALSE(state.consumer_feedback_enabled); diff --git a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp index e9a91fb..29dffc2 100644 --- a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp @@ -105,7 +105,9 @@ TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_f TEST(render_dag_test, capture_controller_and_repository_capture_exact_requested_frames) { Capture_Controller controller; Capture_Repository repository; - const auto session_id = controller.capture_frames(2); + const auto request = controller.capture_frames(2); + ASSERT_TRUE(request); + const auto session_id = request.session_id; repository.begin_session(session_id, 2); const auto first = controller.begin_frame(); const auto second = controller.begin_frame(); @@ -196,7 +198,9 @@ TEST(render_dag_test, TEST(render_dag_test, failed_capture_ticket_is_retried_until_the_requested_frame_completes) { Capture_Controller controller; - const auto session_id = controller.capture_next_frame(); + const auto request = controller.capture_next_frame(); + ASSERT_TRUE(request); + const auto session_id = request.session_id; const auto failed = controller.begin_frame(); ASSERT_TRUE(failed.capture); controller.finish_frame(failed, false); @@ -214,7 +218,8 @@ TEST(render_dag_test, failed_capture_ticket_is_retried_until_the_requested_frame TEST(render_dag_test, concurrent_workers_reserve_exactly_one_capture_slot_each) { Capture_Controller controller; constexpr std::size_t requested = 64; - controller.capture_frames(requested); + const auto request = controller.capture_frames(requested); + ASSERT_TRUE(request); std::atomic captured{}; std::vector workers; workers.reserve(256); 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 cc25168..92f94c5 100644 --- a/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp @@ -51,8 +51,9 @@ TEST(external_operation_test, completion_before_subscription_is_delivered_once) EXPECT_FALSE(source.complete()); int completion_count{}; - operation.on_complete([&](std::exception_ptr error) { - EXPECT_FALSE(error); + operation.on_complete([&](External_Operation_Completion completion) { + EXPECT_EQ(completion.error, External_Operation_Error::none); + EXPECT_FALSE(completion.exception); ++completion_count; }); EXPECT_EQ(completion_count, 1); @@ -64,12 +65,12 @@ TEST(external_operation_test, const auto completed_operation = completed_first.operation(); ASSERT_TRUE(completed_first.complete()); EXPECT_NO_THROW(completed_operation.on_complete( - [](std::exception_ptr) { throw std::runtime_error("late callback"); })); + [](External_Operation_Completion) { throw std::runtime_error("late callback"); })); External_Operation_Source subscribed_first; const auto subscribed_operation = subscribed_first.operation(); subscribed_operation.on_complete( - [](std::exception_ptr) { throw std::runtime_error("early callback"); }); + [](External_Operation_Completion) { throw std::runtime_error("early callback"); }); EXPECT_TRUE(subscribed_first.complete()); } @@ -81,8 +82,8 @@ TEST(render_graph_runtime_test, reusable_topology_executes_multiple_frames) { ++execution_count.at(index); return Node_Execution_Result::completed(); }; - runtime.execute({}, execute_node); - runtime.execute({}, execute_node); + EXPECT_EQ(runtime.execute({}, execute_node), renderive::render_graph::detail::Render_Graph_Execution_Error::none); + EXPECT_EQ(runtime.execute({}, execute_node), renderive::render_graph::detail::Render_Graph_Execution_Error::none); EXPECT_EQ(execution_count[0], 2U); EXPECT_EQ(execution_count[1], 2U); } @@ -107,12 +108,14 @@ TEST(render_graph_runtime_test, return Node_Execution_Result::completed(); }; - std::thread execution([&] { runtime.execute(slots, execute_node); }); + auto graph_error = renderive::render_graph::detail::Render_Graph_Execution_Error::none; + std::thread execution([&] { graph_error = runtime.execute(slots, execute_node); }); submit_started.wait(false, std::memory_order_acquire); EXPECT_FALSE(publish_executed.load(std::memory_order_acquire)); EXPECT_TRUE(source.complete()); execution.join(); + EXPECT_EQ(graph_error, renderive::render_graph::detail::Render_Graph_Execution_Error::none); EXPECT_TRUE(publish_executed.load(std::memory_order_acquire)); EXPECT_EQ(execution_storage[0].status, Node_Execution_Status::complete); EXPECT_EQ(execution_storage[1].status, Node_Execution_Status::complete); @@ -142,7 +145,7 @@ TEST(render_graph_runtime_test, std::thread execution([&] { try { - runtime.execute({}, execute_node); + static_cast(runtime.execute({}, execute_node)); } catch (...) { graph_error = std::current_exception(); } @@ -176,7 +179,8 @@ TEST(render_graph_runtime_test, return Node_Execution_Result::external(source.operation()); }; - std::thread execution([&] { runtime.execute({}, execute_node); }); + auto graph_error = renderive::render_graph::detail::Render_Graph_Execution_Error::none; + std::thread execution([&] { graph_error = runtime.execute({}, execute_node); }); external_started.wait(false, std::memory_order_acquire); renderive::scheduling::detail::OneTBB_Runtime::instance().enqueue([&] { { @@ -192,6 +196,7 @@ TEST(render_graph_runtime_test, } EXPECT_TRUE(source.complete()); execution.join(); + EXPECT_EQ(graph_error, renderive::render_graph::detail::Render_Graph_Execution_Error::none); } TEST(external_operation_test, cancellation_is_terminal_and_delivered_once) { @@ -202,15 +207,14 @@ TEST(external_operation_test, cancellation_is_terminal_and_delivered_once) { 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); + External_Operation_Completion completion; + operation.on_complete([&](External_Operation_Completion result) { + completion = std::move(result); ++completion_count; }); EXPECT_EQ(completion_count, 1); - ASSERT_TRUE(completion_error); - EXPECT_THROW(std::rethrow_exception(completion_error), - External_Operation_Cancelled); + EXPECT_EQ(completion.error, External_Operation_Error::cancelled); + EXPECT_FALSE(completion.exception); } TEST(external_operation_test, deadline_cancels_pending_operation) { @@ -219,11 +223,11 @@ TEST(external_operation_test, deadline_cancels_pending_operation) { std::mutex mutex; std::condition_variable condition; bool completed{}; - std::exception_ptr completion_error; - operation.on_complete([&](std::exception_ptr error) { + External_Operation_Completion completion; + operation.on_complete([&](External_Operation_Completion result) { { std::lock_guard lock(mutex); - completion_error = std::move(error); + completion = std::move(result); completed = true; } condition.notify_one(); @@ -234,9 +238,8 @@ TEST(external_operation_test, deadline_cancels_pending_operation) { 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(completion.error, External_Operation_Error::deadline_exceeded); + EXPECT_FALSE(completion.exception); EXPECT_EQ(operation.status(), External_Operation_Status::cancelled); } @@ -248,7 +251,7 @@ TEST(render_graph_runtime_test, auto slots = execution_slots(*plan, execution_storage); std::atomic submit_started{}; std::atomic publish_executed{}; - std::exception_ptr graph_error; + auto graph_error = renderive::render_graph::detail::Render_Graph_Execution_Error::none; renderive::render_graph::detail::Render_Graph_Runtime runtime(*plan); const auto execute_node = [&](std::size_t index, Node_Execution_Metrics*) { @@ -262,19 +265,13 @@ TEST(render_graph_runtime_test, }; std::thread execution([&] { - try { - runtime.execute(slots, execute_node); - } catch (...) { - graph_error = std::current_exception(); - } + graph_error = runtime.execute(slots, execute_node); }); 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_EQ(graph_error, renderive::render_graph::detail::Render_Graph_Execution_Error::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); @@ -288,11 +285,12 @@ TEST(render_graph_runtime_test, idle_cancellation_does_not_arm_next_execution) { runtime.cancel_pending(); std::atomic executed{}; - EXPECT_NO_THROW(runtime.execute( - slots, [&](std::size_t, Node_Execution_Metrics*) { - executed.fetch_add(1, std::memory_order_relaxed); - return Node_Execution_Result::completed(); - })); + EXPECT_EQ(runtime.execute( + slots, [&](std::size_t, Node_Execution_Metrics*) { + executed.fetch_add(1, std::memory_order_relaxed); + return Node_Execution_Result::completed(); + }), + renderive::render_graph::detail::Render_Graph_Execution_Error::none); EXPECT_EQ(executed.load(std::memory_order_relaxed), 2); EXPECT_EQ(execution_storage[0].status, Node_Execution_Status::complete); @@ -315,8 +313,9 @@ TEST(external_operation_test, completion_commit_precedes_completion_notification const auto operation = source.operation(); std::atomic published{}; int observed{}; - operation.on_complete([&](std::exception_ptr error) { - EXPECT_FALSE(error); + operation.on_complete([&](External_Operation_Completion completion) { + EXPECT_EQ(completion.error, External_Operation_Error::none); + EXPECT_FALSE(completion.exception); observed = published.load(std::memory_order_acquire); }); @@ -331,13 +330,14 @@ TEST(external_operation_test, completion_commit_precedes_completion_notification 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); + External_Operation_Completion completion; + operation.on_complete([&](External_Operation_Completion result) { + completion = std::move(result); }); 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(completion.error, External_Operation_Error::none); + ASSERT_TRUE(completion.exception); + EXPECT_THROW(std::rethrow_exception(completion.exception), std::runtime_error); EXPECT_EQ(operation.status(), External_Operation_Status::failed); } diff --git a/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp b/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp index 7d33050..87c7abe 100644 --- a/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp +++ b/Kernel/tests/renderive/scene/Dynamic_Renderable_Lifecycle_Test.cpp @@ -274,7 +274,9 @@ TEST(dynamic_renderable_lifecycle_test, captures_before_and_after_runtime_attach auto first = renderive_Owner::make(); auto second = renderive_Owner::make(); attach_initial(scene, first); - const Capture_Session_Id session_id = scene.capture_frames(2); + const auto request = scene.capture_frames(2); + ASSERT_TRUE(request); + const Capture_Session_Id session_id = request.session_id; scene.render(); scene.wait_for_render(); wait_renderable_edit(scene, [second](auto& editor) { diff --git a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp index 626e934..75da6c6 100644 --- a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp +++ b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp @@ -294,6 +294,34 @@ TEST(render_plan_execution_test, capture_off_keeps_abstract_frame_without_execut EXPECT_FALSE(frame.completed_snapshot()); } +TEST(render_plan_execution_test, abstract_frame_lifetime_is_not_borrowed_by_async_render) { + Scene2D_Context<> scene; + auto renderable = renderive_Owner::make(); + std::mutex mutex; + std::condition_variable condition; + bool paint_started{}; + bool release_paint{}; + renderable->paint_action = [&] { + std::unique_lock lock(mutex); + paint_started = true; + condition.notify_all(); + condition.wait(lock, [&] { return release_paint; }); + }; + attach_initial(scene, renderable); + { + Abstract_Frame frame; + scene.render(frame); + std::unique_lock lock(mutex); + condition.wait(lock, [&] { return paint_started; }); + } + { + std::lock_guard lock(mutex); + release_paint = true; + } + condition.notify_all(); + EXPECT_NO_THROW(scene.wait_for_render()); +} + TEST(render_plan_execution_test, measures_capture_off_and_on_cost_separately) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(); @@ -311,7 +339,9 @@ TEST(render_plan_execution_test, measures_capture_off_and_on_cost_separately) { }; render_frames(warmup_count); const auto capture_off_ns = render_frames(frame_count); - const Capture_Session_Id session_id = scene.capture_frames(frame_count); + const auto request = scene.capture_frames(frame_count); + ASSERT_TRUE(request); + const Capture_Session_Id session_id = request.session_id; const auto capture_on_ns = render_frames(frame_count); const auto session = scene.capture_session(session_id); ASSERT_TRUE(session); @@ -329,7 +359,9 @@ TEST(render_plan_execution_test, capture_requests_publish_exact_immutable_abstra auto renderable = renderive_Owner::make(true); attach_initial(scene, renderable); constexpr std::size_t capture_count = 20; - const Capture_Session_Id session_id = scene.capture_frames(capture_count); + const auto request = scene.capture_frames(capture_count); + ASSERT_TRUE(request); + const Capture_Session_Id session_id = request.session_id; Alternate_Frame alternate_frame; scene.render(alternate_frame); scene.wait_for_render(); @@ -365,7 +397,9 @@ TEST(render_plan_execution_test, failed_render_discards_abstract_frame_capture_a throw std::runtime_error("paint failed"); }; attach_initial(scene, renderable); - const Capture_Session_Id session_id = scene.capture_next_frame(); + const auto request = scene.capture_next_frame(); + ASSERT_TRUE(request); + const Capture_Session_Id session_id = request.session_id; Alternate_Frame failed_frame; scene.render(failed_frame); EXPECT_THROW(scene.wait_for_render(), std::runtime_error); diff --git a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp index bdcc4c7..c594f81 100644 --- a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp @@ -96,7 +96,6 @@ struct Scene3D_Playback_Snapshot_Test_Scene final auto frame = frame_control.acquire_renderer(); ASSERT_TRUE(frame); render(*frame); - wait_for_render(); } Node_Execution_Result render_scene( diff --git a/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp index 4f551d6..b260bde 100644 --- a/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp @@ -32,7 +32,7 @@ struct Scene_State_Observer_Test_Recorder { }; using Scene_State_Observer_Test_Scene_Observer = Observer_State; using Scene_State_Observer_Test_State_Observer = Observer_State<>; -TEST(scene_state_observer_test, scene_uses_double_state_and_observes_lifecycle) { +TEST(scene_state_observer_test, scene_uses_published_state_and_observes_lifecycle) { State_Render_State_Reader render; Scene_State_Observer_Test_Recorder recorder; using Scene = Scene2D_Context, Recording_Color_Cache, Scene_State_Observer_Test_State, Scene_State_Observer_Test_State_Observer, Scene_State_Observer_Test_Scene_Observer>; diff --git a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp index 252ed2a..f174e16 100644 --- a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp +++ b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp @@ -458,23 +458,31 @@ TEST(scene_base_test, initialization_and_runtime_edit_build_equivalent_plan_shap std::ranges::sort(edited_edges); EXPECT_EQ(initialized_edges, edited_edges); } -TEST(scene_base_test, detached_topology_reference_fails_fast_after_callback) { - EXPECT_DEATH( - { - Scene2D_Context<> scene; - auto parent = renderive_Owner::make(); - auto child = renderive_Owner::make(); - with_attach_builder(scene, [&](auto& builder) { - builder.attach(parent); - builder.attach(child); - builder.set_dependency_parent(child, parent); - }); - scene.edit_renderables([parent](auto& editor) { - editor.detach(parent); - }); - scene.render(); - }, - "dependency graph still references detached renderable"); +TEST(scene_base_test, invalid_runtime_edit_rolls_back_complete_topology) { + Scene2D_Context<> scene; + auto parent = renderive_Owner::make(); + auto child = renderive_Owner::make(); + with_attach_builder(scene, [&](auto& builder) { + builder.attach(parent); + builder.attach(child); + builder.set_dependency_parent(child, parent); + }); + const auto topology = scene.topology_snapshot(); + auto operation = scene.edit_renderables([parent](auto& editor) { + editor.detach(parent); + }); + EXPECT_EQ(operation.wait(), Scene_Edit_Error::dangling_dependency); + const auto restored = scene.topology_snapshot(); + EXPECT_EQ(restored.renderables, topology.renderables); + EXPECT_EQ(restored.display.size(), topology.display.size()); + ASSERT_EQ(restored.dependency.size(), topology.dependency.size()); + ASSERT_EQ(restored.dependency.size(), 1u); + EXPECT_EQ(restored.dependency[0].child, topology.dependency[0].child); + EXPECT_EQ(restored.dependency[0].parent, topology.dependency[0].parent); + scene.render(); + EXPECT_NO_THROW(scene.wait_for_render()); + EXPECT_EQ(parent->render_count, 1); + EXPECT_EQ(child->render_count, 1); } TEST(scene_base_test, wait_for_render_propagates_background_render_failure) { Scene2D_Context<> scene; diff --git a/Kernel/tests/renderive/state/Concepts_Test.cpp b/Kernel/tests/renderive/state/Concepts_Test.cpp index 61cbd58..f9dd4b9 100644 --- a/Kernel/tests/renderive/state/Concepts_Test.cpp +++ b/Kernel/tests/renderive/state/Concepts_Test.cpp @@ -2,7 +2,7 @@ #include "State_Test_Types.hpp" static_assert(State_Value); static_assert(std::derived_from); -static_assert(Double_State_Storage_Type); +static_assert(Published_State_Storage_Type); static_assert(!Has_State_Validator); static_assert(Has_State_Validator); TEST(state_concepts_test, concepts_compile) { diff --git a/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp b/Kernel/tests/renderive/state/Published_State_Strategy_Test.cpp similarity index 55% rename from Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp rename to Kernel/tests/renderive/state/Published_State_Strategy_Test.cpp index 26c7936..d029a7f 100644 --- a/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp +++ b/Kernel/tests/renderive/state/Published_State_Strategy_Test.cpp @@ -2,17 +2,17 @@ #include #include #include "State_Test_Types.hpp" -TEST(double_state_strategy_test, constructs_from_state) { +TEST(published_state_strategy_test, constructs_from_state) { State_Plain_Strategy state({.count = 10, .name = "alpha"}); EXPECT_EQ(state.get<&State_Plain_Value::count>(), 10); EXPECT_EQ(state.get<&State_Plain_Value::name>(), "alpha"); } -TEST(double_state_strategy_test, calls_optional_state_validator_when_present) { +TEST(published_state_strategy_test, calls_optional_state_validator_when_present) { State_Checked_Value state{.minimum = 10, .maximum = 5}; EXPECT_THROW(State_Checked_Validator::validate_state(state), std::invalid_argument); } -TEST(double_state_strategy_test, keeps_cache_separate_until_publish) { +TEST(published_state_strategy_test, keeps_cache_separate_until_publish) { State_Render_State_Reader render; State_Checked_Strategy state( State_Checked_Value{.percent = 20, .batch_size = 4}); @@ -22,7 +22,7 @@ TEST(double_state_strategy_test, keeps_cache_separate_until_publish) { state.publish(); EXPECT_EQ(render.read(state).percent.get(), 60); } -TEST(double_state_strategy_test, keeps_acquired_render_reference_stable_until_publish) { +TEST(published_state_strategy_test, keeps_acquired_render_reference_stable_until_publish) { State_Render_State_Reader render; State_Plain_Strategy strategy(State_Plain_Value{.count = 1, .name = "snapshot"}); const auto snapshot = render.read(strategy); @@ -31,34 +31,34 @@ TEST(double_state_strategy_test, keeps_acquired_render_reference_stable_until_pu EXPECT_EQ(snapshot.count, 1); EXPECT_EQ(render.read(strategy).count, 2); } -TEST(double_state_strategy_test, keeps_cached_value_when_runtime_validation_fails) { +TEST(published_state_strategy_test, keeps_cached_value_when_runtime_validation_fails) { State_Checked_Strategy state( State_Checked_Value{.percent = 20, .batch_size = 4}); EXPECT_THROW((state.set<&State_Checked_Value::percent>(200)), std::out_of_range); EXPECT_EQ(state.get<&State_Checked_Value::percent>(), 20); } -TEST(double_state_strategy_test, supports_atomic_wait_mutex) { +TEST(published_state_strategy_test, supports_atomic_wait_mutex) { State_Render_State_Reader render; State_Wait_Strategy state({.count = 1, .name = "wait"}); state.set<&State_Plain_Value::count>(2); state.publish(); EXPECT_EQ(render.read(state).count, 2); } -struct Double_State_Observer_Test_Time_Source { +struct Published_State_Observer_Test_Time_Source { std::uint64_t value{100}; std::uint64_t now_ns() const noexcept { return value; } }; -struct Double_State_Observer_Test_Data { +struct Published_State_Observer_Test_Data { std::uint64_t event_count{}; std::uint64_t publish_count{}; int value{}; }; -struct Double_State_Observer_Test_Recorder { +struct Published_State_Observer_Test_Recorder { static constexpr bool enabled = true; - std::shared_ptr data{std::make_shared()}; + std::shared_ptr data{std::make_shared()}; template void observe(const Observation& observation) noexcept { ++data->event_count; @@ -66,26 +66,26 @@ struct Double_State_Observer_Test_Recorder { data->value = observation.state.count; } }; -TEST(double_state_strategy_test, notifies_observer_for_cache_update_and_publish) { - using Observer = Observer_State; +TEST(published_state_strategy_test, notifies_observer_for_cache_update_and_publish) { + using Observer = Observer_State; using Strategy = - Double_State_Storage; - Double_State_Observer_Test_Recorder recorder; - Strategy strategy(State_Plain_Value{}, With_Observer(Observer(recorder, Double_State_Observer_Test_Time_Source{}))); + Published_State_Storage; + Published_State_Observer_Test_Recorder recorder; + Strategy strategy(State_Plain_Value{}, With_Observer(Observer(recorder, Published_State_Observer_Test_Time_Source{}))); strategy.set<&State_Plain_Value::count>(8); strategy.publish(); EXPECT_EQ(recorder.data->event_count, 2); EXPECT_EQ(recorder.data->publish_count, 1); EXPECT_EQ(recorder.data->value, 8); } -struct Double_State_Throwing_Assignment_State { +struct Published_State_Throwing_Assignment_State { int first{}; int second{}; - Double_State_Throwing_Assignment_State() = default; - Double_State_Throwing_Assignment_State(int first, int second) : first(first), second(second) {} - Double_State_Throwing_Assignment_State(const Double_State_Throwing_Assignment_State&) = default; - Double_State_Throwing_Assignment_State(Double_State_Throwing_Assignment_State&&) noexcept = default; - Double_State_Throwing_Assignment_State& operator=(const Double_State_Throwing_Assignment_State& other) { + Published_State_Throwing_Assignment_State() = default; + Published_State_Throwing_Assignment_State(int first, int second) : first(first), second(second) {} + Published_State_Throwing_Assignment_State(const Published_State_Throwing_Assignment_State&) = default; + Published_State_Throwing_Assignment_State(Published_State_Throwing_Assignment_State&&) noexcept = default; + Published_State_Throwing_Assignment_State& operator=(const Published_State_Throwing_Assignment_State& other) { first = other.first; if (throw_on_copy_assignment) { throw std::runtime_error("state assignment failed"); @@ -93,19 +93,19 @@ struct Double_State_Throwing_Assignment_State { second = other.second; return *this; } - Double_State_Throwing_Assignment_State& operator=(Double_State_Throwing_Assignment_State&&) noexcept = default; + Published_State_Throwing_Assignment_State& operator=(Published_State_Throwing_Assignment_State&&) noexcept = default; inline static bool throw_on_copy_assignment{}; }; -TEST(double_state_strategy_test, failed_publish_keeps_render_state_and_revision_unchanged) { +TEST(published_state_strategy_test, failed_publish_keeps_render_state_and_revision_unchanged) { State_Render_State_Reader render; - using Strategy = Double_State_Storage; - Double_State_Throwing_Assignment_State::throw_on_copy_assignment = false; - Strategy strategy(Double_State_Throwing_Assignment_State(1, 2)); - strategy.set<&Double_State_Throwing_Assignment_State::first>(10); - strategy.set<&Double_State_Throwing_Assignment_State::second>(20); - Double_State_Throwing_Assignment_State::throw_on_copy_assignment = true; + using Strategy = Published_State_Storage; + Published_State_Throwing_Assignment_State::throw_on_copy_assignment = false; + Strategy strategy(Published_State_Throwing_Assignment_State(1, 2)); + strategy.set<&Published_State_Throwing_Assignment_State::first>(10); + strategy.set<&Published_State_Throwing_Assignment_State::second>(20); + Published_State_Throwing_Assignment_State::throw_on_copy_assignment = true; EXPECT_THROW(strategy.publish(), std::runtime_error); - Double_State_Throwing_Assignment_State::throw_on_copy_assignment = false; + Published_State_Throwing_Assignment_State::throw_on_copy_assignment = false; EXPECT_EQ(strategy.state_revision(), 0); EXPECT_EQ(render.read(strategy).first, 1); EXPECT_EQ(render.read(strategy).second, 2); diff --git a/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp b/Kernel/tests/renderive/state/Render_Acquired_State_Strategy_Test.cpp similarity index 52% rename from Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp rename to Kernel/tests/renderive/state/Render_Acquired_State_Strategy_Test.cpp index 6312337..24449b3 100644 --- a/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp +++ b/Kernel/tests/renderive/state/Render_Acquired_State_Strategy_Test.cpp @@ -5,23 +5,23 @@ #include #include "renderive/state/State_Strategy.hpp" #include "State_Test_Types.hpp" -struct Triple_State_Test_State { +struct Render_Acquired_State_Test_State { int value{}; }; -TEST(triple_state_strategy_test, publishes_then_acquires_render_state) { +TEST(render_acquired_state_strategy_test, publishes_then_acquires_render_state) { State_Render_State_Reader render; - Triple_State_Storage strategy; - strategy.set<&Triple_State_Test_State::value>(7); + Render_Acquired_State_Storage strategy; + strategy.set<&Render_Acquired_State_Test_State::value>(7); strategy.publish(); EXPECT_EQ(render.read(strategy).value, 0); EXPECT_EQ(strategy.acquire_render_state(), 1); EXPECT_EQ(render.read(strategy).value, 7); } -TEST(triple_state_strategy_test, returns_render_state_snapshot) { +TEST(render_acquired_state_strategy_test, returns_render_state_snapshot) { State_Render_State_Reader render; - Triple_State_Storage strategy; + Render_Acquired_State_Storage strategy; const auto snapshot = render.read(strategy); - strategy.set<&Triple_State_Test_State::value>(7); + strategy.set<&Render_Acquired_State_Test_State::value>(7); strategy.publish(); strategy.acquire_render_state(); EXPECT_EQ(snapshot.value, 0); @@ -33,10 +33,10 @@ struct Multi_State_A { struct Multi_State_B { int value{}; }; -struct Multi_State_A_Strategy : Double_State_Storage {}; -struct Multi_State_B_Strategy : Double_State_Storage {}; +struct Multi_State_A_Strategy : Published_State_Storage {}; +struct Multi_State_B_Strategy : Published_State_Storage {}; struct Multi_State_Product : Multi_State_A_Strategy, Multi_State_B_Strategy {}; -TEST(double_state_strategy_test, supports_named_base_access_in_multiple_inheritance) { +TEST(published_state_strategy_test, supports_named_base_access_in_multiple_inheritance) { State_Render_State_Reader render; Multi_State_Product product; product.Multi_State_A_Strategy::set<&Multi_State_A::value>(3); @@ -46,9 +46,9 @@ TEST(double_state_strategy_test, supports_named_base_access_in_multiple_inherita EXPECT_EQ(render.read(static_cast(product)).value, 3); EXPECT_EQ(render.read(static_cast(product)).value, 8); } -TEST(triple_state_strategy_test, concurrently_acquires_render_revision_without_data_race) { +TEST(render_acquired_state_strategy_test, concurrently_acquires_render_revision_without_data_race) { State_Render_State_Reader render; - Triple_State_Storage strategy; + Render_Acquired_State_Storage strategy; std::atomic finished{}; auto acquire = [&] { while (!finished.load(std::memory_order_acquire)) { @@ -59,7 +59,7 @@ TEST(triple_state_strategy_test, concurrently_acquires_render_revision_without_d std::thread first(acquire); std::thread second(acquire); for (int value = 1; value <= 1000; ++value) { - strategy.set<&Triple_State_Test_State::value>(value); + strategy.set<&Render_Acquired_State_Test_State::value>(value); strategy.publish(); } finished.store(true, std::memory_order_release); @@ -68,14 +68,14 @@ TEST(triple_state_strategy_test, concurrently_acquires_render_revision_without_d EXPECT_EQ(strategy.acquire_render_state(), 1000); EXPECT_EQ(render.read(strategy).value, 1000); } -struct Triple_State_Throwing_Assignment_State { +struct Render_Acquired_State_Throwing_Assignment_State { int first{}; int second{}; - Triple_State_Throwing_Assignment_State() = default; - Triple_State_Throwing_Assignment_State(int first, int second) : first(first), second(second) {} - Triple_State_Throwing_Assignment_State(const Triple_State_Throwing_Assignment_State&) = default; - Triple_State_Throwing_Assignment_State(Triple_State_Throwing_Assignment_State&&) noexcept = default; - Triple_State_Throwing_Assignment_State& operator=(const Triple_State_Throwing_Assignment_State& other) { + Render_Acquired_State_Throwing_Assignment_State() = default; + Render_Acquired_State_Throwing_Assignment_State(int first, int second) : first(first), second(second) {} + Render_Acquired_State_Throwing_Assignment_State(const Render_Acquired_State_Throwing_Assignment_State&) = default; + Render_Acquired_State_Throwing_Assignment_State(Render_Acquired_State_Throwing_Assignment_State&&) noexcept = default; + Render_Acquired_State_Throwing_Assignment_State& operator=(const Render_Acquired_State_Throwing_Assignment_State& other) { first = other.first; if (throw_on_copy_assignment) { throw std::runtime_error("state assignment failed"); @@ -83,19 +83,19 @@ struct Triple_State_Throwing_Assignment_State { second = other.second; return *this; } - Triple_State_Throwing_Assignment_State& operator=(Triple_State_Throwing_Assignment_State&&) noexcept = default; + Render_Acquired_State_Throwing_Assignment_State& operator=(Render_Acquired_State_Throwing_Assignment_State&&) noexcept = default; inline static bool throw_on_copy_assignment{}; }; -TEST(triple_state_strategy_test, failed_publish_keeps_published_revision_and_render_state_unchanged) { +TEST(render_acquired_state_strategy_test, failed_publish_keeps_published_revision_and_render_state_unchanged) { State_Render_State_Reader render; - using Strategy = Triple_State_Storage; - Triple_State_Throwing_Assignment_State::throw_on_copy_assignment = false; - Strategy strategy(Triple_State_Throwing_Assignment_State(1, 2)); - strategy.set<&Triple_State_Throwing_Assignment_State::first>(10); - strategy.set<&Triple_State_Throwing_Assignment_State::second>(20); - Triple_State_Throwing_Assignment_State::throw_on_copy_assignment = true; + using Strategy = Render_Acquired_State_Storage; + Render_Acquired_State_Throwing_Assignment_State::throw_on_copy_assignment = false; + Strategy strategy(Render_Acquired_State_Throwing_Assignment_State(1, 2)); + strategy.set<&Render_Acquired_State_Throwing_Assignment_State::first>(10); + strategy.set<&Render_Acquired_State_Throwing_Assignment_State::second>(20); + Render_Acquired_State_Throwing_Assignment_State::throw_on_copy_assignment = true; EXPECT_THROW(strategy.publish(), std::runtime_error); - Triple_State_Throwing_Assignment_State::throw_on_copy_assignment = false; + Render_Acquired_State_Throwing_Assignment_State::throw_on_copy_assignment = false; EXPECT_EQ(strategy.state_revision(), 0); EXPECT_EQ(strategy.acquire_render_state(), 0); EXPECT_EQ(render.read(strategy).first, 1); diff --git a/Kernel/tests/renderive/state/State_Test_Types.hpp b/Kernel/tests/renderive/state/State_Test_Types.hpp index 9d31835..a3792d2 100644 --- a/Kernel/tests/renderive/state/State_Test_Types.hpp +++ b/Kernel/tests/renderive/state/State_Test_Types.hpp @@ -27,7 +27,7 @@ struct State_Plain_Value { int count{}; std::string name; }; -using State_Plain_Strategy = Double_State_Storage; +using State_Plain_Strategy = Published_State_Storage; struct State_Checked_Value { struct Even_Validator { void operator()(const int& value) const { @@ -48,6 +48,6 @@ struct State_Checked_Validator { } } }; -using State_Checked_Strategy = Double_State_Storage; +using State_Checked_Strategy = Published_State_Storage; using State_Wait_Strategy = - Double_State_Storage; + Published_State_Storage; diff --git a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp index 490aad9..01b1cce 100644 --- a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp +++ b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp @@ -15,8 +15,8 @@ #include "renderive/renderable/Renderable.hpp" #include "renderive/renderable/Renderable_Test_Harness.hpp" #include "renderive/scene/Scene.hpp" -#include "renderive/state/Double_State_Storage.hpp" -#include "renderive/state/Triple_State_Storage.hpp" +#include "renderive/state/Published_State_Storage.hpp" +#include "renderive/state/Render_Acquired_State_Storage.hpp" namespace { struct Threading_Test_Frame { std::uint64_t value{}; @@ -70,10 +70,10 @@ using Threading_Revision_Observer_State = Observer_State; -using Threading_Test_Triple_State = - Triple_State_Storage; +using Threading_Test_Published_State = + Published_State_Storage; +using Threading_Test_Render_Acquired_State = + Render_Acquired_State_Storage; struct Threading_Test_Scene_Renderable : Renderable_Test_Harness { Threading_Test_Scene_Renderable(std::atomic& render_count, std::atomic& maximum_sequence) : Renderable_Test_Harness({.cache_enabled = false}), render_count(&render_count), maximum_sequence(&maximum_sequence) {} @@ -260,6 +260,7 @@ TEST(threading_contract_test, low_latency_strategy_keeps_frames_consistent_durin std::atomic updater_done{}; std::atomic configuration_done{}; std::atomic invalid{}; + std::atomic configuration_errors{}; std::vector producers; producers.reserve(producer_count); for (int producer = 0; producer < producer_count; ++producer) { @@ -285,7 +286,9 @@ TEST(threading_contract_test, low_latency_strategy_keeps_frames_consistent_durin wait_start(start); constexpr double frequencies[] = {30.0, 60.0, 120.0, 240.0}; for (int index = 0; index < frame_count; ++index) { - strategy.set_frequency_hz(frequencies[index % 4]); + if (strategy.set_frequency_hz(frequencies[index % 4]) != + Low_Latency_Strategy::Control_Error::none) + configuration_errors.fetch_add(1, std::memory_order_relaxed); } configuration_done.store(true, std::memory_order_release); }); @@ -313,6 +316,7 @@ TEST(threading_contract_test, low_latency_strategy_keeps_frames_consistent_durin configurator.join(); renderer.join(); EXPECT_EQ(invalid.load(std::memory_order_acquire), 0); + EXPECT_EQ(configuration_errors.load(std::memory_order_acquire), 0); EXPECT_EQ(strategy.state().real_time_data_update_sequence, update_count); EXPECT_TRUE(std::isfinite(strategy.state().frequency_hz)); } @@ -447,8 +451,8 @@ TEST(threading_contract_test, history_real_time_data_supports_concurrent_update_ EXPECT_EQ(state.retained_value_count, data.size()); EXPECT_GE(state.revision, update_count); } -TEST(threading_contract_test, double_state_keeps_cache_consistent_during_concurrent_set_publish_and_read) { - Threading_Test_Double_State strategy; +TEST(threading_contract_test, published_state_keeps_cache_consistent_during_concurrent_set_publish_and_read) { + Threading_Test_Published_State strategy; constexpr int update_count = 4000; std::atomic start{}; std::atomic writer_done{}; @@ -476,8 +480,8 @@ TEST(threading_contract_test, double_state_keeps_cache_consistent_during_concurr EXPECT_EQ(invalid.load(std::memory_order_acquire), 0); EXPECT_EQ(strategy.state_revision(), update_count); } -TEST(threading_contract_test, triple_state_returns_consistent_snapshots_during_concurrent_publish_and_acquire) { - Threading_Test_Triple_State strategy; +TEST(threading_contract_test, render_acquired_state_returns_consistent_snapshots_during_concurrent_publish_and_acquire) { + Threading_Test_Render_Acquired_State strategy; constexpr int update_count = 4000; std::atomic start{}; std::atomic writer_done{}; diff --git a/Qt/CMakeLists.txt b/Qt/CMakeLists.txt index 7c47570..f32500c 100644 --- a/Qt/CMakeLists.txt +++ b/Qt/CMakeLists.txt @@ -66,11 +66,9 @@ if (RENDERIVE_BUILD_TESTS) continue() endif () file(RELATIVE_PATH Renderive_Qt_test_name "${Renderive_Qt_test_dir}" "${Renderive_Qt_test_source}") - string(MD5 Renderive_Qt_test_hash "${Renderive_Qt_test_name}") - string(SUBSTRING "${Renderive_Qt_test_hash}" 0 8 Renderive_Qt_test_hash) - get_filename_component(Renderive_Qt_test_name "${Renderive_Qt_test_source}" NAME_WE) + string(REGEX REPLACE "\\.[^.]+$" "" Renderive_Qt_test_name "${Renderive_Qt_test_name}") string(MAKE_C_IDENTIFIER "${Renderive_Qt_test_name}" Renderive_Qt_test_name) - set(Renderive_Qt_test_target "Renderive_Qt_${Renderive_Qt_test_name}_${Renderive_Qt_test_hash}") + set(Renderive_Qt_test_target "Renderive_Qt_${Renderive_Qt_test_name}") add_executable("${Renderive_Qt_test_target}" "${Renderive_Qt_test_source}") target_link_libraries("${Renderive_Qt_test_target}" PRIVATE Renderive_Qt GTest::gtest) renderive_stage_kernel_runtime("${Renderive_Qt_test_target}") diff --git a/cmake/RenderiveInstall.cmake b/cmake/RenderiveInstall.cmake index eb41ea9..a0da68d 100644 --- a/cmake/RenderiveInstall.cmake +++ b/cmake/RenderiveInstall.cmake @@ -36,11 +36,11 @@ install(FILES if (RENDERIVE_INSTALL_3D AND WIN32) install(FILES - "${PROJECT_SOURCE_DIR}/render_3D/cmake/module/FindPThreads4W.cmake" + "${CMAKE_CURRENT_LIST_DIR}/../render_3D/cmake/module/FindPThreads4W.cmake" DESTINATION "${Renderive_install_cmake_dir}/modules") endif () if (RENDERIVE_INSTALL_QT) - install(FILES "${PROJECT_SOURCE_DIR}/export.h" + install(FILES "${CMAKE_CURRENT_LIST_DIR}/../export.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") endif () diff --git a/render_2D/CMakeLists.txt b/render_2D/CMakeLists.txt index bf669e3..e6b4871 100644 --- a/render_2D/CMakeLists.txt +++ b/render_2D/CMakeLists.txt @@ -49,11 +49,9 @@ if (RENDERIVE_BUILD_TESTS) continue() endif () file(RELATIVE_PATH Renderive_render_2D_test_name "${Renderive_render_2D_test_dir}" "${Renderive_render_2D_test_source}") - string(MD5 Renderive_render_2D_test_hash "${Renderive_render_2D_test_name}") - string(SUBSTRING "${Renderive_render_2D_test_hash}" 0 8 Renderive_render_2D_test_hash) - get_filename_component(Renderive_render_2D_test_name "${Renderive_render_2D_test_source}" NAME_WE) + string(REGEX REPLACE "\\.[^.]+$" "" Renderive_render_2D_test_name "${Renderive_render_2D_test_name}") string(MAKE_C_IDENTIFIER "${Renderive_render_2D_test_name}" Renderive_render_2D_test_name) - set(Renderive_render_2D_test_target "Renderive_render_2D_${Renderive_render_2D_test_name}_${Renderive_render_2D_test_hash}") + set(Renderive_render_2D_test_target "Renderive_render_2D_${Renderive_render_2D_test_name}") add_executable("${Renderive_render_2D_test_target}" "${Renderive_render_2D_test_source}") target_link_libraries("${Renderive_render_2D_test_target}" PRIVATE Renderive_render_2D GTest::gtest_main) renderive_stage_kernel_runtime("${Renderive_render_2D_test_target}") diff --git a/render_2D/render_2D/plottable/Plottable.h b/render_2D/render_2D/plottable/Plottable.h index 1e644b5..12db0a3 100644 --- a/render_2D/render_2D/plottable/Plottable.h +++ b/render_2D/render_2D/plottable/Plottable.h @@ -139,16 +139,16 @@ protected: private: template using State_Storage = - Double_State_Storage; + Published_State_Storage; template [[nodiscard]] State_Storage& state_storage() noexcept { - return dynamic_cast&>( - this->template d_func<::renderive::Renderable::Impl>()); + return static_cast&>( + *this->template d_func<::renderive::Renderable::Impl>().state_strategy); } template [[nodiscard]] const State_Storage& state_storage() const noexcept { - return dynamic_cast&>( - this->template d_func<::renderive::Renderable::Impl>()); + return static_cast&>( + *this->template d_func<::renderive::Renderable::Impl>().state_strategy); } }; } diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp index a3559eb..d3d49ba 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -15,7 +15,7 @@ struct Selection_Interaction { PointF selection_start{}; PointF selection_current{}; }; -using Selection_Interaction_State = Double_State_Storage; +using Selection_Interaction_State = Published_State_Storage; RectF axis_content_rect(const Axis_Transform& first, const Axis_Transform& second) { return mapped_rect(first, second, first.coordinate_range, second.coordinate_range); } diff --git a/render_2D/render_2D/plottable/Spectrum.cpp b/render_2D/render_2D/plottable/Spectrum.cpp index 2739a43..805f1db 100644 --- a/render_2D/render_2D/plottable/Spectrum.cpp +++ b/render_2D/render_2D/plottable/Spectrum.cpp @@ -16,7 +16,7 @@ struct Spectrum_Interaction { int selected_marker = -1; Hover_Tooltip_Runtime tooltip; }; -using Spectrum_Interaction_State = Double_State_Storage; +using Spectrum_Interaction_State = Published_State_Storage; struct Prepared_Curve { std::vector points; std::vector fill; diff --git a/render_2D/render_2D/plottable/Waterfall.cpp b/render_2D/render_2D/plottable/Waterfall.cpp index c2b1277..82202cb 100644 --- a/render_2D/render_2D/plottable/Waterfall.cpp +++ b/render_2D/render_2D/plottable/Waterfall.cpp @@ -20,7 +20,7 @@ struct Waterfall_Interaction { Hover_Tooltip_Runtime tooltip; }; using Waterfall_History = Plottable_History_Real_Time_Data>; -using Waterfall_Interaction_State = Double_State_Storage; +using Waterfall_Interaction_State = Published_State_Storage; struct Waterfall_Prepare_Buffer { ::renderive::Waterfall::Properties properties; std::deque rows; diff --git a/render_2D/render_2D/renderable/Renderable.cpp b/render_2D/render_2D/renderable/Renderable.cpp index 1d4ba92..91a1f6b 100644 --- a/render_2D/render_2D/renderable/Renderable.cpp +++ b/render_2D/render_2D/renderable/Renderable.cpp @@ -1,10 +1,7 @@ #include "Renderable.h" #include "Renderable_p.h" - #include "../render/Blend2D_Cache.h" - #include - namespace renderive { namespace detail { namespace { @@ -12,49 +9,38 @@ struct Renderable_Group final : Renderable { using Renderable::Renderable; }; } - renderive_Owner make_renderable_group(bool cache_enabled) { return renderive_Owner::make(cache_enabled); } } // namespace detail - Renderable::Renderable(bool cache_enabled) : render_base(std::make_unique(), {.cache_enabled = cache_enabled}) {} - Renderable::~Renderable() = default; - Renderable_Cache_Mode Renderable::get_cache_mode() const noexcept { return d_func().configuration.load(std::memory_order_acquire).cache_enabled ? Renderable_Cache_Mode::Local_Pixel : Renderable_Cache_Mode::Direct; } - void Renderable::set_cache_mode(Renderable_Cache_Mode mode) { d_func().set_configuration( {.cache_enabled = mode == Renderable_Cache_Mode::Local_Pixel}); } - Renderable_Observation Renderable::observation() const noexcept { const auto& d = d_func(); std::lock_guard lock(d.observation_mutex); return d.observation; } - void Renderable::dispatch_event(const Event& event) { if (auto* handler = - dynamic_cast(&d_func())) + dynamic_cast(&d_func())) handler->handle_event(event); } - Renderable& Renderable::Impl::renderable() noexcept { return static_cast(owner()); } - void Renderable::Impl::prepare_frame(const Prepare_Render_Context&) {} - void Renderable::Impl::paint(detail::Painter&, const Paint_Render_Context&) {} - void Renderable::Impl::build_paint_graph( Renderable_Graph_Builder& builder) { const auto paint_task = add_paint_task( @@ -65,18 +51,15 @@ void Renderable::Impl::build_paint_graph( }); builder.precede(builder.find("prepare"), paint_task); } - void Renderable::Impl::prepare(const Prepare_Render_Context& context) { prepare_frame(context); } - Renderable_Graph_Builder::Task Renderable::Impl::add_prepare_task( Renderable_Graph_Builder& builder, std::string logical_key, std::string name, Prepare_Task_Function function) { return builder.emplace(std::move(logical_key), std::move(name), std::move(function)); } - Renderable_Graph_Builder::Task Renderable::Impl::add_paint_task( Renderable_Graph_Builder& builder, std::string logical_key, std::string name, Paint_Task_Function function) { @@ -91,24 +74,24 @@ Renderable_Graph_Builder::Task Renderable::Impl::add_paint_task( if (!cache) return; detail::Painter painter( - *cache, {context.frame.viewport.width, - context.frame.viewport.height}); + *cache, { + context.frame.viewport.width, + context.frame.viewport.height + }); if (painter) function(painter, context); }); } - void Renderable::Impl::observe_state( Renderable_Observer_Event event, std::uint64_t time_ns, std::uint64_t cache_update_count, - std::uint64_t publish_count) noexcept { + std::uint64_t publish_count) { std::lock_guard lock(observation_mutex); observation.event = event; observation.event_time_ns = time_ns; observation.cache_update_count = cache_update_count; observation.publish_count = publish_count; } - void Renderable::Impl::observe_data( std::uint64_t revision, std::size_t item_count, std::size_t auxiliary_item_count) noexcept { @@ -117,5 +100,4 @@ void Renderable::Impl::observe_data( observation.item_count = item_count; observation.auxiliary_item_count = auxiliary_item_count; } - } // namespace renderive diff --git a/render_2D/render_2D/renderable/Renderable.h b/render_2D/render_2D/renderable/Renderable.h index c673630..3fdb36e 100644 --- a/render_2D/render_2D/renderable/Renderable.h +++ b/render_2D/render_2D/renderable/Renderable.h @@ -65,7 +65,7 @@ struct Renderable_State_Observer { static constexpr bool enabled = true; explicit Renderable_State_Observer(Renderable* renderable) noexcept : renderable_(renderable) {} template - void observe(const Observation& observation) noexcept { + void observe(const Observation& observation) { renderable_->report_state_observation(observation); } private: diff --git a/render_2D/render_2D/renderable/Renderable_p.h b/render_2D/render_2D/renderable/Renderable_p.h index bc1ffce..1580863 100644 --- a/render_2D/render_2D/renderable/Renderable_p.h +++ b/render_2D/render_2D/renderable/Renderable_p.h @@ -56,7 +56,7 @@ struct Renderable::Impl struct Observer : renderable_inheritance::Observer_Root { static void handle( Impl& impl, - const detail::Renderable_Event_View& value) noexcept { + const detail::Renderable_Event_View& value) { impl.observe_state(value.event, value.time_ns, value.cache_update_count, value.publish_count); @@ -90,7 +90,7 @@ private: [[nodiscard]] Renderable& renderable() noexcept; void observe_state(Renderable_Observer_Event event, std::uint64_t time_ns, std::uint64_t cache_update_count, - std::uint64_t publish_count) noexcept; + std::uint64_t publish_count); mutable std::mutex observation_mutex; Renderable_Observation observation; friend class Renderable; diff --git a/render_2D/render_2D/scene/Plot_Scene.cpp b/render_2D/render_2D/scene/Plot_Scene.cpp index c9f4662..27fccc7 100644 --- a/render_2D/render_2D/scene/Plot_Scene.cpp +++ b/render_2D/render_2D/scene/Plot_Scene.cpp @@ -76,8 +76,8 @@ Scene_2D_Base::Attach_Builder Plot_Scene::attach_builder() { return static_cast(render_scene()).attach_builder(); } -void Plot_Scene::edit_renderables(Scene_2D_Base::Renderable_Edit edit) { - static_cast(render_scene()).edit_renderables(std::move(edit)); +Scene_Base::Edit_Operation Plot_Scene::edit_renderables(Scene_2D_Base::Renderable_Edit edit) { + return static_cast(render_scene()).edit_renderables(std::move(edit)); } Scene_Base& Plot_Scene::render_scene() noexcept { @@ -146,24 +146,22 @@ bool Plot_Scene::view_active() const noexcept { void Plot_Scene::request_redraw() noexcept { render_scene().notify_model_dirty(); } -void Plot_Scene::set_max_render_fps(double fps) { - impl_->apply([fps](auto& scene) { scene.set_max_render_fps(fps); }); +Plot_Control_Error Plot_Scene::set_max_render_fps(double fps) { + return impl_->apply([fps](auto& scene) { return scene.set_max_render_fps(fps); }); } - -void Plot_Scene::clear_max_render_fps() { - impl_->apply([](auto& scene) { scene.clear_max_render_fps(); }); +Plot_Control_Error Plot_Scene::clear_max_render_fps() { + return impl_->apply([](auto& scene) { return scene.clear_max_render_fps(); }); } double Plot_Scene::max_render_fps() const noexcept { return impl_->apply([](const auto& scene) { return scene.max_render_fps(); }); } -void Plot_Scene::set_consumer_feedback(Frame_Consumer_Feedback feedback) { - impl_->apply([feedback](auto& scene) { scene.set_consumer_feedback(feedback); }); +Plot_Control_Error Plot_Scene::set_consumer_feedback(Frame_Consumer_Feedback feedback) { + return impl_->apply([feedback](auto& scene) { return scene.set_consumer_feedback(feedback); }); } - -void Plot_Scene::clear_consumer_feedback() { - impl_->apply([](auto& scene) { scene.clear_consumer_feedback(); }); +Plot_Control_Error Plot_Scene::clear_consumer_feedback() { + return impl_->apply([](auto& scene) { return scene.clear_consumer_feedback(); }); } Plot_Frame_Mode Plot_Scene::frame_mode() const noexcept { return impl_->mode; } diff --git a/render_2D/render_2D/scene/Plot_Scene.h b/render_2D/render_2D/scene/Plot_Scene.h index 380c384..e88efe9 100644 --- a/render_2D/render_2D/scene/Plot_Scene.h +++ b/render_2D/render_2D/scene/Plot_Scene.h @@ -21,6 +21,12 @@ struct Performance_Overlay; struct Plot_Scene_Options { Plot_Frame_Mode frame_mode{Plot_Frame_Mode::Low_Latency}; }; +enum class Plot_Control_Error : std::uint8_t { + none, + unsupported_frame_mode, + invalid_max_render_fps, + invalid_consumer_feedback +}; struct Presentation_Sink { virtual ~Presentation_Sink() = default; @@ -40,7 +46,7 @@ public: void init(); [[nodiscard]] ::renderive_Owner root_renderable() const; [[nodiscard]] Scene_2D_Base::Attach_Builder attach_builder(); - void edit_renderables(Scene_2D_Base::Renderable_Edit edit); + [[nodiscard]] Scene_Base::Edit_Operation edit_renderables(Scene_2D_Base::Renderable_Edit edit); [[nodiscard]] Scene_Base& render_scene() noexcept; [[nodiscard]] const Scene_Base& render_scene() const noexcept; @@ -62,11 +68,11 @@ public: [[nodiscard]] bool view_active() const noexcept; void request_redraw() noexcept; - void set_max_render_fps(double fps); - void clear_max_render_fps(); + [[nodiscard]] Plot_Control_Error set_max_render_fps(double fps); + [[nodiscard]] Plot_Control_Error clear_max_render_fps(); [[nodiscard]] double max_render_fps() const noexcept; - void set_consumer_feedback(Frame_Consumer_Feedback feedback); - void clear_consumer_feedback(); + [[nodiscard]] Plot_Control_Error set_consumer_feedback(Frame_Consumer_Feedback feedback); + [[nodiscard]] Plot_Control_Error clear_consumer_feedback(); [[nodiscard]] Plot_Frame_Mode frame_mode() const noexcept; [[nodiscard]] Plot_Frame_Status frame_status() const; diff --git a/render_2D/render_2D/scene/Scene_Context.hpp b/render_2D/render_2D/scene/Scene_Context.hpp index 760ef0b..eaccea4 100644 --- a/render_2D/render_2D/scene/Scene_Context.hpp +++ b/render_2D/render_2D/scene/Scene_Context.hpp @@ -8,7 +8,7 @@ #include "renderive/base/observer/Observer.hpp" #include "renderive/frame_control/Frame_Control.hpp" #include "renderive/renderable/color/Concepts.hpp" -#include "renderive/state/Double_State_Storage.hpp" +#include "renderive/state/Published_State_Storage.hpp" #include "renderive/scene/base/Scene_Base.hpp" #include "renderive/scene/base/Frame_Viewport.hpp" struct Scene2D_Frame_Data : Abstract_Frame {}; @@ -24,12 +24,12 @@ concept Scene2D_Frame_Control_Constructible = std::constructible_from, Color_Cache_Type Cache = Recording_Color_Cache, class State = Scene2D_State, class State_Observer = Observer_State<>, class Scene_Observer = Observer_State<>> requires Frame_Control_Strategy_For && Scene2D_State_Value class Scene2D_Context : public Scene_2D_Base, - public Double_State_Storage, public Scene_Compositor { public: using Scene_State_Strategy = - Double_State_Storage; + Published_State_Storage; using Frame_Control = Strategy; using Frame = typename Frame_Control::Frame; using Painter_Lease = typename Frame_Control::Painter_Lease; @@ -86,6 +86,9 @@ protected: if (context.renderable && context.renderable->visible && context.color_cache) final_color_cache_.composite(*context.color_cache); } + void publish_scene_state() override { + this->Scene_State_Strategy::publish(); + } std::uint64_t acquire_scene_state() override { return this->Scene_State_Strategy::state_revision(); } diff --git a/render_2D/render_2D/scene/detail/Plot_Scene_Model.hpp b/render_2D/render_2D/scene/detail/Plot_Scene_Model.hpp index 65116c5..01097eb 100644 --- a/render_2D/render_2D/scene/detail/Plot_Scene_Model.hpp +++ b/render_2D/render_2D/scene/detail/Plot_Scene_Model.hpp @@ -48,7 +48,7 @@ struct Frame_Observer { static constexpr bool enabled = true; std::shared_ptr data; template - void observe(const Observation& observation) noexcept { + void observe(const Observation& observation) { std::lock_guard lock(data->mutex); data->last_event = static_cast(observation.event); ++data->observation_count; @@ -192,9 +192,7 @@ struct Plot_Scene_Model final : ::Scene2D_Contextframe_control.acquire_renderer(); if (!render_frame) return false; - this->render(*render_frame); - this->wait_for_render(); - return true; + return Scene_Base::render(*render_frame) == Scene_Render_Error::none; } [[nodiscard]] bool discard_pending_frame() { if constexpr (Mode == Plot_Frame_Mode::Playback) @@ -259,37 +257,44 @@ struct Plot_Scene_Model final : ::Scene2D_Contextframe_control.set_frequency_hz(fps); + return Plot_Control_Error::unsupported_frame_mode; + } else { + using Error = typename Frame_Control::Control_Error; + const auto error = this->frame_control.set_frequency_hz(fps); + if (error != Error::none) + return Plot_Control_Error::invalid_max_render_fps; this->notify_model_dirty(); + return Plot_Control_Error::none; } } - void clear_max_render_fps() { + [[nodiscard]] Plot_Control_Error clear_max_render_fps() { if constexpr (Mode != Plot_Frame_Mode::Low_Latency) { - throw std::logic_error("maximum render FPS is only available in low-latency mode"); - } - else { + return Plot_Control_Error::unsupported_frame_mode; + } else { this->frame_control.clear_frequency_limit(); this->notify_model_dirty(); + return Plot_Control_Error::none; } } - void set_consumer_feedback(Frame_Consumer_Feedback feedback) { - if constexpr (Mode != Plot_Frame_Mode::Low_Latency) - throw std::logic_error("consumer feedback is only available in low-latency mode"); - else - this->frame_control.set_consumer_feedback(feedback); + [[nodiscard]] Plot_Control_Error set_consumer_feedback(Frame_Consumer_Feedback feedback) { + if constexpr (Mode != Plot_Frame_Mode::Low_Latency) { + return Plot_Control_Error::unsupported_frame_mode; + } else { + using Error = typename Frame_Control::Control_Error; + const auto error = this->frame_control.set_consumer_feedback(feedback); + return error == Error::none ? Plot_Control_Error::none + : Plot_Control_Error::invalid_consumer_feedback; + } } - void clear_consumer_feedback() { - if constexpr (Mode != Plot_Frame_Mode::Low_Latency) - throw std::logic_error("consumer feedback is only available in low-latency mode"); - else + [[nodiscard]] Plot_Control_Error clear_consumer_feedback() { + if constexpr (Mode != Plot_Frame_Mode::Low_Latency) { + return Plot_Control_Error::unsupported_frame_mode; + } else { this->frame_control.clear_consumer_feedback(); + return Plot_Control_Error::none; + } } [[nodiscard]] double max_render_fps() const noexcept { if constexpr (Mode == Plot_Frame_Mode::Low_Latency) diff --git a/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp b/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp index 9f764c7..e43168a 100644 --- a/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp +++ b/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp @@ -3,7 +3,6 @@ #include #include #include -#include namespace renderive { namespace { TEST(Renderive_Core2_Frame_Pipeline, ManualLifecycleSeparatesPrepareRefreshAndRender) { @@ -39,7 +38,7 @@ TEST(Renderive_Core2_Frame_Pipeline, HighFrequencyLowLatencyLifecycleExposesKern Plot_Scene plot; plot.init(); plot.set_viewport_size({96, 54}); - plot.set_max_render_fps(1'000'000'000.0); + EXPECT_EQ(plot.set_max_render_fps(1'000'000'000.0), Plot_Control_Error::none); ASSERT_TRUE(plot.prepare_frame()); auto snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "published"); @@ -72,8 +71,8 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyConsumerFeedbackFlowsThroughCore2 Plot_Scene plot; plot.init(); plot.set_viewport_size({96, 54}); - plot.set_max_render_fps(100.0); - plot.set_consumer_feedback({40'000'000}); + EXPECT_EQ(plot.set_max_render_fps(100.0), Plot_Control_Error::none); + EXPECT_EQ(plot.set_consumer_feedback({40'000'000}), Plot_Control_Error::none); ASSERT_TRUE(plot.prepare_frame()); ASSERT_TRUE(plot.render_prepared_frame()); const auto snapshot = plot.frame_status(); @@ -93,8 +92,8 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyLimitsCanBeClearedIndependently) Plot_Scene plot; plot.init(); plot.set_viewport_size({96, 54}); - plot.set_max_render_fps(100.0); - plot.set_consumer_feedback({40'000'000}); + EXPECT_EQ(plot.set_max_render_fps(100.0), Plot_Control_Error::none); + EXPECT_EQ(plot.set_consumer_feedback({40'000'000}), Plot_Control_Error::none); plot.clear_consumer_feedback(); plot.clear_max_render_fps(); ASSERT_TRUE(plot.prepare_frame()); @@ -250,18 +249,18 @@ TEST(Renderive_Core2_Frame_Pipeline, EveryModeHonorsActiveDirtyAndForceRenderGat } TEST(Renderive_Core2_Frame_Pipeline, FrequencyConfigurationRejectsInvalidValuesAndWrongModes) { Plot_Scene low_latency; - EXPECT_THROW(low_latency.set_max_render_fps(0.0), std::invalid_argument); - EXPECT_THROW(low_latency.set_max_render_fps(-1.0), std::invalid_argument); - EXPECT_THROW(low_latency.set_max_render_fps(std::numeric_limits::quiet_NaN()), - std::invalid_argument); - EXPECT_THROW(low_latency.set_max_render_fps(std::numeric_limits::infinity()), - std::invalid_argument); - low_latency.set_max_render_fps(1'000.0); + EXPECT_EQ(low_latency.set_max_render_fps(0.0), Plot_Control_Error::invalid_max_render_fps); + EXPECT_EQ(low_latency.set_max_render_fps(-1.0), Plot_Control_Error::invalid_max_render_fps); + EXPECT_EQ(low_latency.set_max_render_fps(std::numeric_limits::quiet_NaN()), + Plot_Control_Error::invalid_max_render_fps); + EXPECT_EQ(low_latency.set_max_render_fps(std::numeric_limits::infinity()), + Plot_Control_Error::invalid_max_render_fps); + EXPECT_EQ(low_latency.set_max_render_fps(1'000.0), Plot_Control_Error::none); EXPECT_DOUBLE_EQ(low_latency.max_render_fps(), 1'000.0); Plot_Scene manual({.frame_mode = Plot_Frame_Mode::Manual}); Plot_Scene playback({.frame_mode = Plot_Frame_Mode::Playback}); - EXPECT_THROW(manual.set_max_render_fps(60.0), std::logic_error); - EXPECT_THROW(playback.set_max_render_fps(60.0), std::logic_error); + EXPECT_EQ(manual.set_max_render_fps(60.0), Plot_Control_Error::unsupported_frame_mode); + EXPECT_EQ(playback.set_max_render_fps(60.0), Plot_Control_Error::unsupported_frame_mode); EXPECT_FALSE(playback.refresh_manual_frame()); EXPECT_FALSE(playback.discard_pending_frame()); } diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 92f57ce..886d375 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -61,7 +61,7 @@ TEST(Renderive_Core2, RootIdentityAndRefreshDiagnosticsComeFromKernelState) { EXPECT_EQ(plot.root_renderable(), root); plot.set_viewport_size({32, 24}); plot.activate_view(); - plot.set_max_render_fps(144.0); + EXPECT_EQ(plot.set_max_render_fps(144.0), Plot_Control_Error::none); ASSERT_TRUE(plot.render_frame()); const auto diagnostics = plot.diagnostics(); EXPECT_DOUBLE_EQ(diagnostics.refresh.frequency_hz, 144.0); @@ -296,7 +296,7 @@ TEST(Renderive_Core2, PerformanceOverlayConsumesKernelFrameDiagnostics) { plot.init(); plot.set_viewport_size({160, 90}); plot.activate_view(); - plot.set_max_render_fps(120.0); + EXPECT_EQ(plot.set_max_render_fps(120.0), Plot_Control_Error::none); Performance_Overlay_Options options; options.log.enabled = false; const auto overlay = attach_performance_overlay(plot, options); @@ -1018,7 +1018,9 @@ TEST(Renderive_Core2, DynamicWaterfallCaptureStressPreservesPlansSlotsAndExactSe constexpr std::array partition_counts{64, 16, 1}; std::uint64_t tick{1}; for (std::size_t round = 0; round < 3; ++round) { - const Capture_Session_Id session_id = scene.render_scene().capture_frames(8); + const auto request = scene.render_scene().capture_frames(8); + ASSERT_TRUE(request); + const Capture_Session_Id session_id = request.session_id; for (std::size_t frame = 0; frame < 8; ++frame) { if (frame % 2 == 0) { waterfall->set<&Waterfall::Properties::partition_count>( diff --git a/render_3D/CMakeLists.txt b/render_3D/CMakeLists.txt index 1be8859..e91c178 100644 --- a/render_3D/CMakeLists.txt +++ b/render_3D/CMakeLists.txt @@ -147,14 +147,13 @@ if (RENDERIVE_BUILD_TESTS) continue() endif () file(RELATIVE_PATH Renderive_render_3D_test_name "${Renderive_render_3D_test_dir}" "${Renderive_render_3D_test_source}") - string(MD5 Renderive_render_3D_test_hash "${Renderive_render_3D_test_name}") - string(SUBSTRING "${Renderive_render_3D_test_hash}" 0 8 Renderive_render_3D_test_hash) - get_filename_component(Renderive_render_3D_test_name "${Renderive_render_3D_test_source}" NAME_WE) + string(REGEX REPLACE "\\.[^.]+$" "" Renderive_render_3D_test_name "${Renderive_render_3D_test_name}") string(MAKE_C_IDENTIFIER "${Renderive_render_3D_test_name}" Renderive_render_3D_test_name) - set(Renderive_render_3D_test_target "Renderive_render_3D_${Renderive_render_3D_test_name}_${Renderive_render_3D_test_hash}") + set(Renderive_render_3D_test_target "Renderive_render_3D_${Renderive_render_3D_test_name}") add_executable("${Renderive_render_3D_test_target}" "${Renderive_render_3D_test_source}") target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE Renderive_render_3D GTest::gtest_main) - if (Renderive_render_3D_test_name STREQUAL "Gpu_Completion_Service_Tests") + get_filename_component(Renderive_render_3D_test_file_name "${Renderive_render_3D_test_source}" NAME_WE) + if (Renderive_render_3D_test_file_name STREQUAL "Gpu_Completion_Service_Tests") target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE volk::volk_headers) endif () renderive_stage_render_3D_runtime("${Renderive_render_3D_test_target}") diff --git a/render_3D/render_3D/Point_Scene.cpp b/render_3D/render_3D/Point_Scene.cpp index af8c5f9..e27fc09 100644 --- a/render_3D/render_3D/Point_Scene.cpp +++ b/render_3D/render_3D/Point_Scene.cpp @@ -24,18 +24,15 @@ namespace renderive::render_3d { namespace { -void validate(Extent extent) { - if (extent.empty()) - throw std::invalid_argument("Point_Scene viewport must be nonempty"); +bool valid(Extent extent) noexcept { + return !extent.empty(); } - -void validate(Clear_Color color) { +bool valid(Clear_Color color) noexcept { const auto component = [](float value) { return std::isfinite(value) && value >= 0.0F && value <= 1.0F; }; - if (!component(color.red) || !component(color.green) || - !component(color.blue) || !component(color.alpha)) - throw std::invalid_argument("Point_Scene clear color must be in [0, 1]"); + return component(color.red) && component(color.green) && + component(color.blue) && component(color.alpha); } float datoviz_wheel_step(float pixel_delta, float angle_delta) noexcept { @@ -90,14 +87,15 @@ struct Scene_Model { virtual ~Scene_Model() = default; virtual void resize(Extent extent) = 0; virtual void set_clear_color(Clear_Color color) = 0; - virtual void dispatch_pointer( + [[nodiscard]] virtual Scene_Control_Error dispatch_pointer( ::renderive::Event_Type type, float x, float y, ::renderive::Mouse_Button button, ::renderive::Keyboard_Modifier modifiers) = 0; - virtual void dispatch_wheel( + [[nodiscard]] virtual Scene_Control_Error dispatch_wheel( float x, float y, float delta_x, float delta_y, ::renderive::Keyboard_Modifier modifiers) = 0; - virtual void dispatch_key(const ::renderive::Key_Event& event) = 0; + [[nodiscard]] virtual Scene_Control_Error dispatch_key( + const ::renderive::Key_Event& event) = 0; [[nodiscard]] virtual bool prepare_frame() = 0; [[nodiscard]] virtual bool refresh_manual_frame() = 0; [[nodiscard]] virtual bool discard_pending_frame() = 0; @@ -121,39 +119,26 @@ struct Point_Backend_State final { ~Point_Backend_State() { if (backend) - render_domain->invoke([this] { backend.reset(); }); + static_cast(render_domain->invoke([this] { backend.reset(); })); } detail::Datoviz_Visual_Backend& require_backend() { - if (backend) - return *backend; - if (backend_failure) - std::rethrow_exception(backend_failure); - throw std::runtime_error("Datoviz backend is unavailable"); + if (!backend) + throw std::logic_error("Datoviz backend invariant is unavailable"); + return *backend; } - - void quarantine_backend(std::exception_ptr failure) noexcept { - if (!failure) { - try { - throw std::runtime_error("Datoviz backend was quarantined"); - } catch (...) { - failure = std::current_exception(); - } - } - backend_failure = std::move(failure); - // An abandoned fence means the driver never proved that the submitted - // work stopped touching the backend resources. Destroying or reusing - // them would be unsafe. Deliberately relinquish ownership of the whole - // backend and let the OS/driver reclaim it at process teardown. This is - // a fault-containment path only; all subsequent scene operations fail. + [[nodiscard]] bool backend_available() const noexcept { + return !quarantined.load(std::memory_order_acquire); + } + void quarantine_backend() { + quarantined.store(true, std::memory_order_release); static_cast(backend.release()); std::lock_guard lock(frame_mutex); latest.reset(); } - std::shared_ptr render_domain; std::unique_ptr backend; - std::exception_ptr backend_failure; + std::atomic_bool quarantined{}; mutable std::mutex frame_mutex; std::shared_ptr latest; }; @@ -207,10 +192,12 @@ struct Basic_Point_Scene final const detail::Scene_State initial{ options.viewport, options.clear_color, options.visual_family}; const auto state = backend_state; - state->render_domain->invoke([state, &options, &initial] { + const auto invocation = state->render_domain->invoke([state, &options, &initial] { state->backend = std::make_unique( options.gpu_index, options.validation_enabled, initial); }); + if (!invocation) + throw std::logic_error("render domain stopped during Point_Scene initialization"); } void resize(Extent extent) override { @@ -225,39 +212,70 @@ struct Basic_Point_Scene final this->notify_model_dirty(); } - void dispatch_pointer( + Scene_Control_Error dispatch_pointer( ::renderive::Event_Type type, float x, float y, ::renderive::Mouse_Button button, ::renderive::Keyboard_Modifier modifiers) override { const Extent viewport = this->Scene_State_Strategy::template get< &detail::Scene_State::viewport>(); const auto state = backend_state; - state->render_domain->invoke([state, type, x, y, button, modifiers, viewport] { - state->require_backend().dispatch_pointer(type, x, y, button, modifiers, viewport); - }); - this->notify_model_dirty(); + if (!state->backend_available()) + return Scene_Control_Error::backend_unavailable; + const auto invocation = state->render_domain->invoke( + [state, type, x, y, button, modifiers, viewport] { + if (!state->backend_available()) + return Scene_Control_Error::backend_unavailable; + state->require_backend().dispatch_pointer( + type, x, y, button, modifiers, viewport); + return Scene_Control_Error::none; + }); + if (!invocation) + return Scene_Control_Error::backend_unavailable; + const auto error = *invocation.value; + if (error == Scene_Control_Error::none) + this->notify_model_dirty(); + return error; } - - void dispatch_wheel( + Scene_Control_Error dispatch_wheel( float x, float y, float delta_x, float delta_y, ::renderive::Keyboard_Modifier modifiers) override { const Extent viewport = this->Scene_State_Strategy::template get< &detail::Scene_State::viewport>(); const auto state = backend_state; - state->render_domain->invoke( + if (!state->backend_available()) + return Scene_Control_Error::backend_unavailable; + const auto invocation = state->render_domain->invoke( [state, x, y, delta_x, delta_y, modifiers, viewport] { - state->require_backend().dispatch_wheel(x, y, delta_x, delta_y, modifiers, - viewport); + if (!state->backend_available()) + return Scene_Control_Error::backend_unavailable; + state->require_backend().dispatch_wheel( + x, y, delta_x, delta_y, modifiers, viewport); + return Scene_Control_Error::none; }); - this->notify_model_dirty(); + if (!invocation) + return Scene_Control_Error::backend_unavailable; + const auto error = *invocation.value; + if (error == Scene_Control_Error::none) + this->notify_model_dirty(); + return error; } - - void dispatch_key(const ::renderive::Key_Event& event) override { + Scene_Control_Error dispatch_key( + const ::renderive::Key_Event& event) override { const auto state = backend_state; - state->render_domain->invoke([state, event] { + if (!state->backend_available()) + return Scene_Control_Error::backend_unavailable; + const auto invocation = state->render_domain->invoke([state, event] { + if (!state->backend_available()) + return Scene_Control_Error::backend_unavailable; state->require_backend().dispatch_key(event); + return Scene_Control_Error::none; }); - this->notify_model_dirty(); + if (!invocation) + return Scene_Control_Error::backend_unavailable; + const auto error = *invocation.value; + if (error == Scene_Control_Error::none) + this->notify_model_dirty(); + return error; } [[nodiscard]] bool prepare_frame() override { @@ -285,9 +303,7 @@ struct Basic_Point_Scene final auto renderer = this->frame_control.acquire_renderer(); if (!renderer) return false; - this->render(*renderer); - this->wait_for_render(); - return true; + return Scene_Base::render(*renderer) == Scene_Render_Error::none; } [[nodiscard]] bool request_frame() override { @@ -369,6 +385,9 @@ struct Basic_Point_Scene final Node_Execution_Result render_scene( const Scene_Render_Context& context) override { + if (!backend_state->backend_available()) + return Node_Execution_Result::failed( + External_Operation_Error::external_failure); const auto prepared = context.prepared(point_id); if (!prepared) throw std::logic_error("Point_Visual did not publish prepared data"); @@ -393,85 +412,102 @@ struct Basic_Point_Scene final Completion_Result completion; }; auto async_frame = std::make_shared(); + auto prepared_completion = 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( + "GPU completion has no pending frame")))); + return; + } + auto pending = std::move(*async_frame->pending); + async_frame->pending.reset(); + pending.trace.gpu_fence_wait_ns = + async_frame->completion.wait_duration_ns; + if (async_frame->completion.error != + detail::Gpu_Completion_Service::Completion_Error::none) { + try { + state->quarantine_backend(); + static_cast(source->fail( + External_Operation_Error::external_failure)); + } catch (...) { + static_cast(source->fail(std::current_exception())); + } + return; + } + try { + auto completed = state->require_backend().collect( + std::move(pending)); + auto frame = std::move(completed.frame); + 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())); + } + }); + if (!prepared_completion) { + static_cast(source->cancel()); + return Node_Execution_Result::external(operation); + } auto render_completion = std::make_shared< detail::Render_Domain::Prepared_Task>( - 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( - "GPU completion has no pending frame")))); - return; - } - auto pending = std::move(*async_frame->pending); - async_frame->pending.reset(); - pending.trace.gpu_fence_wait_ns = - async_frame->completion.wait_duration_ns; - if (async_frame->completion.abandoned) { - auto failure = std::move(async_frame->completion.error); - state->quarantine_backend(failure); - static_cast(source->fail(std::move(failure))); - return; - } - if (async_frame->completion.error) { - try { - state->require_backend().discard(std::move(pending)); - static_cast(source->fail( - std::move(async_frame->completion.error))); - } catch (...) { - static_cast(source->fail( - std::current_exception())); - } - return; - } - try { - auto completed = state->require_backend().collect( - std::move(pending)); - auto frame = std::move(completed.frame); - 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())); - } - })); + std::move(prepared_completion.task)); + auto prepared_gpu_completion = + detail::Gpu_Completion_Service::instance().prepare( + [state, async_frame, render_completion, source]( + Completion_Result result) { + async_frame->completion = std::move(result); + try { + if (state->render_domain->post( + std::move(*render_completion)) != + detail::Render_Domain::Error::none) + static_cast(source->cancel()); + } catch (...) { + static_cast(source->fail( + std::current_exception())); + } + }, + observe); + if (!prepared_gpu_completion) { + static_cast(source->cancel()); + return Node_Execution_Result::external(operation); + } auto completion = std::make_shared< detail::Gpu_Completion_Service::Reservation>( - detail::Gpu_Completion_Service::instance().prepare( - [state, async_frame, render_completion]( - Completion_Result result) noexcept { - async_frame->completion = std::move(result); - state->render_domain->post( - std::move(*render_completion)); - }, - observe)); + std::move(prepared_gpu_completion.reservation)); const auto queued_at = observe ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; try { - state->render_domain->post( + const auto post_error = 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; + if (!state->backend_available()) { + static_cast(source->fail( + External_Operation_Error::external_failure)); + return; + } std::uint64_t queue_wait_ns{}; if (observe) { const auto queue_wait = @@ -503,6 +539,8 @@ struct Basic_Point_Scene final source->fail(std::current_exception())); } }); + if (post_error != detail::Render_Domain::Error::none) + static_cast(source->cancel()); } catch (...) { static_cast(source->fail(std::current_exception())); } @@ -545,8 +583,10 @@ struct Point_Scene::Impl { Point_Scene::Point_Scene(Scene_Options options, std::shared_ptr visual) { - validate(options.viewport); - validate(options.clear_color); + if (!valid(options.viewport)) + throw std::invalid_argument("Point_Scene viewport must be nonempty"); + if (!valid(options.clear_color)) + throw std::invalid_argument("Point_Scene clear color must be in [0, 1]"); if (!std::isfinite(options.maximum_frames_per_second) || options.maximum_frames_per_second <= 0.0) throw std::invalid_argument( @@ -556,48 +596,51 @@ Point_Scene::Point_Scene(Scene_Options options, Point_Scene::~Point_Scene() = default; -void Point_Scene::resize(Extent extent) { - validate(extent); +Scene_Control_Error Point_Scene::resize(Extent extent) { + if (!valid(extent)) + return Scene_Control_Error::empty_viewport; impl_->model->resize(extent); + return Scene_Control_Error::none; } - -void Point_Scene::set_clear_color(Clear_Color color) { - validate(color); +Scene_Control_Error Point_Scene::set_clear_color(Clear_Color color) { + if (!valid(color)) + return Scene_Control_Error::invalid_clear_color; impl_->model->set_clear_color(color); + return Scene_Control_Error::none; } -void Point_Scene::dispatch(const ::renderive::Event&) { - // Show, hide and leave carry no Datoviz controller payload. +Scene_Control_Error Point_Scene::dispatch(const ::renderive::Event&) { + return Scene_Control_Error::none; } -void Point_Scene::dispatch_pointer( +Scene_Control_Error Point_Scene::dispatch_pointer( ::renderive::Event_Type type, float x, float y, ::renderive::Mouse_Button button, ::renderive::Mouse_Button_Mask, ::renderive::Keyboard_Modifier modifiers) { if (!std::isfinite(x) || !std::isfinite(y)) - return; + return Scene_Control_Error::invalid_event; if (type != ::renderive::Event_Type::Pointer_Move && type != ::renderive::Event_Type::Pointer_Press && type != ::renderive::Event_Type::Pointer_Release) - throw std::invalid_argument("Point_Scene expected a pointer event"); - impl_->model->dispatch_pointer(type, x, y, button, modifiers); + return Scene_Control_Error::invalid_event; + return impl_->model->dispatch_pointer(type, x, y, button, modifiers); } -void Point_Scene::dispatch_wheel( +Scene_Control_Error Point_Scene::dispatch_wheel( float x, float y, float pixel_delta_x, float pixel_delta_y, float angle_delta_x, float angle_delta_y, ::renderive::Keyboard_Modifier modifiers) { if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(pixel_delta_x) || !std::isfinite(pixel_delta_y) || !std::isfinite(angle_delta_x) || !std::isfinite(angle_delta_y)) - return; - impl_->model->dispatch_wheel( + return Scene_Control_Error::invalid_event; + return impl_->model->dispatch_wheel( x, y, datoviz_wheel_step(pixel_delta_x, angle_delta_x), datoviz_wheel_step(pixel_delta_y, angle_delta_y), modifiers); } -void Point_Scene::dispatch(const ::renderive::Key_Event& event) { - impl_->model->dispatch_key(event); +Scene_Control_Error Point_Scene::dispatch(const ::renderive::Key_Event& event) { + return impl_->model->dispatch_key(event); } bool Point_Scene::prepare_frame() { return impl_->model->prepare_frame(); } diff --git a/render_3D/render_3D/Point_Scene.h b/render_3D/render_3D/Point_Scene.h index b365fbf..b8fa514 100644 --- a/render_3D/render_3D/Point_Scene.h +++ b/render_3D/render_3D/Point_Scene.h @@ -27,6 +27,13 @@ struct Clear_Color { float alpha{1.0F}; bool operator==(const Clear_Color&) const = default; }; +enum class Scene_Control_Error : std::uint8_t { + none, + empty_viewport, + invalid_clear_color, + invalid_event, + backend_unavailable +}; enum class Frame_Mode : std::uint8_t { Manual, @@ -109,28 +116,30 @@ public: Point_Scene(Point_Scene&&) = delete; Point_Scene& operator=(Point_Scene&&) = delete; - void resize(Extent extent); - void set_clear_color(Clear_Color color); - void dispatch(const ::renderive::Event& event); + [[nodiscard]] Scene_Control_Error resize(Extent extent); + [[nodiscard]] Scene_Control_Error set_clear_color(Clear_Color color); + [[nodiscard]] Scene_Control_Error dispatch(const ::renderive::Event& event); template <::renderive::Event_Point Point_Type> - void dispatch(const ::renderive::Basic_Pointer_Event& event) { - dispatch_pointer(event.type, static_cast(event.position.x), - static_cast(event.position.y), event.button, - event.buttons, event.modifiers); + [[nodiscard]] Scene_Control_Error dispatch( + const ::renderive::Basic_Pointer_Event& event) { + return dispatch_pointer(event.type, static_cast(event.position.x), + static_cast(event.position.y), event.button, + event.buttons, event.modifiers); } template <::renderive::Event_Point Point_Type> - void dispatch(const ::renderive::Basic_Wheel_Event& event) { - dispatch_wheel(static_cast(event.position.x), - static_cast(event.position.y), - static_cast(event.pixel_delta_x), - static_cast(event.pixel_delta_y), - static_cast(event.angle_delta_x), - static_cast(event.angle_delta_y), event.modifiers); + [[nodiscard]] Scene_Control_Error dispatch( + const ::renderive::Basic_Wheel_Event& event) { + return dispatch_wheel(static_cast(event.position.x), + static_cast(event.position.y), + static_cast(event.pixel_delta_x), + static_cast(event.pixel_delta_y), + static_cast(event.angle_delta_x), + static_cast(event.angle_delta_y), event.modifiers); } - void dispatch(const ::renderive::Key_Event& event); + [[nodiscard]] Scene_Control_Error dispatch(const ::renderive::Key_Event& event); // Frame strategy operations. request_frame() is the complete low-latency path. [[nodiscard]] bool prepare_frame(); @@ -146,11 +155,11 @@ public: [[nodiscard]] const Scene_Base& render_scene() const noexcept; private: - void dispatch_pointer(::renderive::Event_Type type, float x, float y, + [[nodiscard]] Scene_Control_Error dispatch_pointer(::renderive::Event_Type type, float x, float y, ::renderive::Mouse_Button button, ::renderive::Mouse_Button_Mask buttons, ::renderive::Keyboard_Modifier modifiers); - void dispatch_wheel(float x, float y, float pixel_delta_x, + [[nodiscard]] Scene_Control_Error dispatch_wheel(float x, float y, float pixel_delta_x, float pixel_delta_y, float angle_delta_x, float angle_delta_y, ::renderive::Keyboard_Modifier modifiers); diff --git a/render_3D/render_3D/Point_Visual.cpp b/render_3D/render_3D/Point_Visual.cpp index 0ec83c4..7b85333 100644 --- a/render_3D/render_3D/Point_Visual.cpp +++ b/render_3D/render_3D/Point_Visual.cpp @@ -18,15 +18,15 @@ namespace renderive::render_3d::detail { namespace { -void validate_points(const std::vector& points) { +bool valid_points(const std::vector& points) noexcept { for (const auto& point : points) { if (!std::isfinite(point.position.x) || !std::isfinite(point.position.y) || !std::isfinite(point.position.z) || !std::isfinite(point.diameter_px) || point.diameter_px <= 0.0F) - throw std::invalid_argument( - "point payload contains invalid coordinates or diameter"); + return false; } + return true; } struct Point_Payload_Tag {}; @@ -72,27 +72,29 @@ struct Point_Visual::Impl : next_Impl, Point_Buffer_Strategy { }; explicit Impl(std::vector points) { - update_points(std::move(points)); + if (update_points(std::move(points)) != Visual_Data_Error::none) + throw std::invalid_argument("point payload contains invalid coordinates or diameter"); } std::shared_ptr real_time_data_binding; - void update_points(std::vector points) { - validate_points(points); + [[nodiscard]] Visual_Data_Error update_points(std::vector points) { + if (!valid_points(points)) + return Visual_Data_Error::invalid_data; const std::size_t count = points.size(); write( std::make_shared>(std::move(points))); report_observation(Point_Data_Observation{ cache_revision(), count}); + return Visual_Data_Error::none; } - - void edit_points(const std::function&)>& edit) { + [[nodiscard]] Visual_Data_Error edit_points(const std::function&)>& edit) { if (!edit) - throw std::invalid_argument("Point_Visual point edit is empty"); + return Visual_Data_Error::empty_edit; const auto& cached = cache_buffer_value(); auto next = cached ? *cached : std::vector{}; edit(next); - update_points(std::move(next)); + return update_points(std::move(next)); } [[nodiscard]] const Point_Payload& points() const noexcept { @@ -220,15 +222,18 @@ Point_Visual::Point_Visual(const State& state, std::vector points) Point_Visual::~Point_Visual() = default; -void Point_Visual::update_points(std::vector points) { - d_func().update_points(std::move(points)); - d_func().changed(); +Visual_Data_Error Point_Visual::update_points(std::vector points) { + const auto error = d_func().update_points(std::move(points)); + if (error == Visual_Data_Error::none) + d_func().changed(); + return error; } - -void Point_Visual::edit_points( +Visual_Data_Error Point_Visual::edit_points( const std::function&)>& edit) { - d_func().edit_points(edit); - d_func().changed(); + const auto error = d_func().edit_points(edit); + if (error == Visual_Data_Error::none) + d_func().changed(); + return error; } std::size_t Point_Visual::point_count() const { diff --git a/render_3D/render_3D/Point_Visual.h b/render_3D/render_3D/Point_Visual.h index 31c1617..66f40fc 100644 --- a/render_3D/render_3D/Point_Visual.h +++ b/render_3D/render_3D/Point_Visual.h @@ -42,6 +42,11 @@ struct Point { float diameter_px{8.0F}; bool operator==(const Point&) const = default; }; +enum class Visual_Data_Error : std::uint8_t { + none, + invalid_data, + empty_edit +}; enum class Point_Aspect : std::uint8_t { Filled, @@ -82,8 +87,8 @@ struct Point_Visual : Renderable { std::vector points = {}); ~Point_Visual() override; - void update_points(std::vector points); - void edit_points(const std::function&)>& edit); + [[nodiscard]] Visual_Data_Error update_points(std::vector points); + [[nodiscard]] Visual_Data_Error edit_points(const std::function&)>& edit); [[nodiscard]] std::size_t point_count() const; [[nodiscard]] std::uint64_t data_revision() const; diff --git a/render_3D/render_3D/Scene_Context.hpp b/render_3D/render_3D/Scene_Context.hpp index b7f0287..8ccacab 100644 --- a/render_3D/render_3D/Scene_Context.hpp +++ b/render_3D/render_3D/Scene_Context.hpp @@ -5,7 +5,7 @@ #include #include "renderive/base/observer/Observer.hpp" #include "renderive/frame_control/Frame_Control.hpp" -#include "renderive/state/Triple_State_Storage.hpp" +#include "renderive/state/Render_Acquired_State_Storage.hpp" #include "renderive/scene/base/Scene_Base.hpp" struct Scene3D_Frame_Data : Abstract_Frame {}; struct Scene3D_State { @@ -16,11 +16,11 @@ concept Scene3D_Frame_Control_Constructible = std::constructible_from, class State = Scene3D_State, class State_Observer = Observer_State<>, class Scene_Observer = Observer_State<>> requires Frame_Control_Strategy_For && State_Value class Scene3D_Context : public Scene_3D_Base, - public Triple_State_Storage< + public Render_Acquired_State_Storage< State, Atomic_Spin_Mutex, State_Observer> { public: using Scene_State_Strategy = - Triple_State_Storage; + Render_Acquired_State_Storage; using Frame_Control = Strategy; using Frame = typename Frame_Control::Frame; using Painter_Lease = typename Frame_Control::Painter_Lease; @@ -59,6 +59,9 @@ protected: const Frame_Control_Strategy_Base& frame_control_strategy_impl() const noexcept override { return frame_control; } + void publish_scene_state() override { + this->Scene_State_Strategy::publish(); + } std::uint64_t acquire_scene_state() override { return this->Scene_State_Strategy::acquire_render_state(); } diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp index 6b9d7f7..48aa4a1 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp @@ -1,7 +1,6 @@ #include "Gpu_Completion_Service.h" #include #include -#include #include #include namespace renderive::render_3d::detail { @@ -26,15 +25,15 @@ Gpu_Completion_Service::Reservation::~Reservation() { cancel(); } Gpu_Completion_Service::Reservation::Reservation(Reservation&& other) noexcept : pending_(std::exchange(other.pending_, {})) {} void Gpu_Completion_Service::Reservation::watch(VkDevice device, - VkFence fence) noexcept { + VkFence fence) { if (!pending_ || device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE) - std::terminate(); + throw std::logic_error("GPU completion reservation or fence is invalid"); auto pending = std::exchange(pending_, {}); auto* const service = pending->service; { std::lock_guard lock(pending->mutex); if (pending->status != Pending_Fence::Status::reserved) - std::terminate(); + throw std::logic_error("GPU completion reservation is not reserved"); pending->device = device; pending->fence = fence; // This timestamp is part of correctness, not only observability: it @@ -90,23 +89,27 @@ void Gpu_Completion_Service::release_slot() noexcept { in_flight_.fetch_sub(1, std::memory_order_relaxed); slots_.release(); } -Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare( +Gpu_Completion_Service::Prepare_Result Gpu_Completion_Service::prepare( Completion completion, bool observe) { if (!completion) throw std::invalid_argument("GPU completion callback is empty"); if (stopping_.load(std::memory_order_acquire)) - throw std::runtime_error("GPU completion service is stopping"); + return {{}, Error::stopping}; auto pending = std::make_shared(); pending->completion = std::move(completion); pending->observe = observe; pending->service = this; acquire_slot(); + if (stopping_.load(std::memory_order_acquire)) { + release_slot(); + return {{}, Error::stopping}; + } if (!pending_.try_push(pending)) { release_slot(); throw std::logic_error("GPU completion admission invariant violated"); } wake(); - return Reservation(std::move(pending)); + return {Reservation(std::move(pending)), Error::none}; } Gpu_Completion_Service::Statistics Gpu_Completion_Service::statistics() const noexcept { @@ -142,8 +145,7 @@ void Gpu_Completion_Service::run() noexcept { }; const auto finish = [this, &wait_age_ns]( const std::shared_ptr& pending, - VkResult result, bool abandoned, - const char* abandonment_reason = nullptr) { + VkResult result, Completion_Error error) { Completion completion; std::chrono::steady_clock::time_point watched_at{}; bool observe{}; @@ -156,28 +158,13 @@ void Gpu_Completion_Service::run() noexcept { } watched_.fetch_sub(1, std::memory_order_relaxed); Result completion_result; - const std::uint64_t wait_ns = wait_age_ns(watched_at); + completion_result.error = error; + completion_result.vulkan_result = result; if (observe) - completion_result.wait_duration_ns = wait_ns; - completion_result.abandoned = abandoned; - if (abandoned || result != VK_SUCCESS) { + completion_result.wait_duration_ns = wait_age_ns(watched_at); + if (error != Completion_Error::none) { fault_count_.fetch_add(1, std::memory_order_relaxed); - if (abandoned) - abandoned_count_.fetch_add(1, std::memory_order_relaxed); - try { - if (abandoned) { - std::string message = abandonment_reason != nullptr - ? abandonment_reason - : "GPU fence was abandoned"; - message += " after " + std::to_string(wait_ns) + " ns"; - throw std::runtime_error(std::move(message)); - } - throw std::runtime_error( - "GPU fence wait failed with Vulkan result " + - std::to_string(static_cast(result))); - } catch (...) { - completion_result.error = std::current_exception(); - } + abandoned_count_.fetch_add(1, std::memory_order_relaxed); } try { completion(std::move(completion_result)); @@ -254,16 +241,14 @@ void Gpu_Completion_Service::run() noexcept { if (stopping_.load(std::memory_order_acquire)) { auto pending = *iterator; iterator = active.erase(iterator); - finish(pending, VK_TIMEOUT, true, - "GPU completion service stopped with an in-flight fence; submission quarantined"); + finish(pending, VK_TIMEOUT, Completion_Error::fence_abandoned); completed_any = true; continue; } if (wait_age_ns(watched_at) >= maximum_fence_age_ns) { auto pending = *iterator; iterator = active.erase(iterator); - finish(pending, VK_TIMEOUT, true, - "GPU fence exceeded the maximum completion age; submission quarantined"); + finish(pending, VK_TIMEOUT, Completion_Error::fence_abandoned); completed_any = true; continue; } @@ -274,10 +259,9 @@ void Gpu_Completion_Service::run() noexcept { } auto pending = *iterator; iterator = active.erase(iterator); - finish(pending, result, result != VK_SUCCESS, - result == VK_SUCCESS - ? nullptr - : "GPU fence query returned an error; submission quarantined"); + finish(pending, result, result == VK_SUCCESS + ? Completion_Error::none + : Completion_Error::vulkan_failure); completed_any = true; } if (completed_any) @@ -323,10 +307,9 @@ void Gpu_Completion_Service::run() noexcept { } auto pending = *iterator; iterator = active.erase(iterator); - finish(pending, result, result != VK_SUCCESS, - result == VK_SUCCESS - ? nullptr - : "GPU fence wait returned an error; submission quarantined"); + finish(pending, result, result == VK_SUCCESS + ? Completion_Error::none + : Completion_Error::vulkan_failure); } } } diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.h b/render_3D/render_3D/detail/Gpu_Completion_Service.h index 1f08ed0..ab2bd9b 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.h +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.h @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -16,14 +15,19 @@ namespace renderive::render_3d::detail { class Gpu_Completion_Service final { struct Pending_Fence; public: + enum class Error : std::uint8_t { + none, + stopping + }; + enum class Completion_Error : std::uint8_t { + none, + fence_abandoned, + vulkan_failure + }; struct Result { - std::exception_ptr error; + Completion_Error error{}; + VkResult vulkan_result{VK_SUCCESS}; std::uint64_t wait_duration_ns{}; - // True when the service cannot prove that the submission completed: - // a fence exceeded the bounded wait policy, Vulkan returned an error, - // or the service is shutting down. Callers must not recycle or destroy - // resources referenced by that submission. - bool abandoned{}; }; struct Statistics { std::size_t capacity{}; @@ -45,17 +49,24 @@ public: Reservation& operator=(const Reservation&) = delete; Reservation(Reservation&& other) noexcept; Reservation& operator=(Reservation&&) = delete; - void watch(VkDevice device, VkFence fence) noexcept; + void watch(VkDevice device, VkFence fence); private: explicit Reservation(std::shared_ptr pending) noexcept; void cancel() noexcept; std::shared_ptr pending_; friend class Gpu_Completion_Service; }; + struct Prepare_Result { + Reservation reservation; + Error error{}; + [[nodiscard]] explicit operator bool() const noexcept { + return error == Error::none; + } + }; static Gpu_Completion_Service& instance(); Gpu_Completion_Service(const Gpu_Completion_Service&) = delete; Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete; - [[nodiscard]] Reservation prepare(Completion completion, bool observe); + [[nodiscard]] Prepare_Result prepare(Completion completion, bool observe); [[nodiscard]] Statistics statistics() const noexcept; private: struct Pending_Fence { diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp index 9521a3e..8327f4d 100644 --- a/render_3D/render_3D/detail/Render_Domain.cpp +++ b/render_3D/render_3D/detail/Render_Domain.cpp @@ -59,21 +59,8 @@ void Render_Domain::destroy(Render_Domain* domain) noexcept { 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 (...) { - } + domain->stopping_.store(true, std::memory_order_release); + domain->destroy_on_exit_.store(true, std::memory_order_release); } void Render_Domain::request_stop() noexcept { bool expected = false; @@ -105,30 +92,40 @@ void Render_Domain::release_admission() noexcept { admitted_.fetch_sub(1, std::memory_order_relaxed); slots_.release(); } -Render_Domain::Prepared_Task Render_Domain::prepare(std::function function) { +Render_Domain::Prepare_Result Render_Domain::prepare(std::function function) { if (!function) throw std::invalid_argument("render domain task is empty"); if (stopping_.load(std::memory_order_acquire)) - throw std::runtime_error("render domain is stopping"); + return {{}, Error::stopping}; acquire_admission(); + if (stopping_.load(std::memory_order_acquire)) { + release_admission(); + return {{}, Error::stopping}; + } try { - return Prepared_Task(shared_from_this(), std::make_unique(std::move(function))); + return {Prepared_Task(shared_from_this(), std::make_unique(std::move(function))), Error::none}; } catch (...) { release_admission(); throw; } } -void Render_Domain::post(std::function function) { - post(prepare(std::move(function))); +Render_Domain::Error Render_Domain::post(std::function function) { + auto prepared = prepare(std::move(function)); + if (!prepared) + return prepared.error; + return post(std::move(prepared.task)); } -void Render_Domain::post(Prepared_Task task) noexcept { - if (!task.task_ || task.domain_.get() != this || stopping_.load(std::memory_order_acquire)) - std::terminate(); +Render_Domain::Error Render_Domain::post(Prepared_Task task) { + if (!task.task_ || task.domain_.get() != this) + throw std::logic_error("render domain prepared task is invalid"); + if (stopping_.load(std::memory_order_acquire)) + return Error::stopping; if (!tasks_.try_push(std::move(task.task_))) - std::terminate(); + throw std::logic_error("render domain admission invariant violated"); const std::size_t queued = queued_.fetch_add(1, std::memory_order_relaxed) + 1; update_peak(peak_queued_, queued); task.domain_.reset(); + return Error::none; } Render_Domain::Statistics Render_Domain::statistics() const noexcept { return { @@ -159,6 +156,14 @@ void Render_Domain::run() { } catch (...) { unhandled_exception_count_.fetch_add(1, std::memory_order_relaxed); } + task.reset(); + if (destroy_on_exit_.load(std::memory_order_acquire)) { + current_domain_ = nullptr; + if (thread_.joinable()) + thread_.detach(); + delete this; + return; + } } } } diff --git a/render_3D/render_3D/detail/Render_Domain.h b/render_3D/render_3D/detail/Render_Domain.h index cda42fd..8396069 100644 --- a/render_3D/render_3D/detail/Render_Domain.h +++ b/render_3D/render_3D/detail/Render_Domain.h @@ -6,11 +6,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace renderive::render_3d::detail { class Render_Domain final : public std::enable_shared_from_this { @@ -19,6 +21,10 @@ class Render_Domain final : public std::enable_shared_from_this { std::function function; }; public: + enum class Error : std::uint8_t { + none, + stopping + }; struct Statistics { std::size_t capacity{}; std::size_t admitted{}; @@ -45,31 +51,54 @@ public: std::unique_ptr task_; friend class Render_Domain; }; + struct Prepare_Result { + Prepared_Task task; + Error error{}; + [[nodiscard]] explicit operator bool() const noexcept { + return error == Error::none; + } + }; static std::shared_ptr acquire(std::uint32_t gpu_index); ~Render_Domain(); Render_Domain(const Render_Domain&) = delete; Render_Domain& operator=(const Render_Domain&) = delete; - [[nodiscard]] Prepared_Task prepare(std::function function); - void post(std::function function); - void post(Prepared_Task task) noexcept; + [[nodiscard]] Prepare_Result prepare(std::function function); + [[nodiscard]] Error post(std::function function); + [[nodiscard]] Error post(Prepared_Task task); + template + struct Invoke_Result { + using Value = std::conditional_t, std::monostate, Result>; + std::optional value; + Error error{}; + [[nodiscard]] explicit operator bool() const noexcept { + return error == Error::none; + } + }; template - auto invoke(Function&& function) -> std::invoke_result_t { + [[nodiscard]] auto invoke(Function&& function) -> Invoke_Result> { using Result = std::invoke_result_t; + Invoke_Result output; if (current_domain_ == this) { if constexpr (std::is_void_v) { std::invoke(std::forward(function)); - return; + output.value.emplace(); } else { - return std::invoke(std::forward(function)); + output.value.emplace(std::invoke(std::forward(function))); } + return output; } auto task = std::make_shared>(std::forward(function)); auto result = task->get_future(); - post([task] { (*task)(); }); - if constexpr (std::is_void_v) + output.error = post([task] { (*task)(); }); + if (output.error != Error::none) + return output; + if constexpr (std::is_void_v) { result.get(); - else - return result.get(); + output.value.emplace(); + } else { + output.value.emplace(result.get()); + } + return output; } [[nodiscard]] Statistics statistics() const noexcept; private: @@ -92,6 +121,7 @@ private: std::atomic_uint64_t backpressure_wait_ns_{}; std::atomic_uint64_t unhandled_exception_count_{}; std::atomic_bool stopping_{}; + std::atomic_bool destroy_on_exit_{}; std::thread thread_; }; } diff --git a/render_3D/render_3D/renderable/Inheritance.h b/render_3D/render_3D/renderable/Inheritance.h index 31012f7..f457661 100644 --- a/render_3D/render_3D/renderable/Inheritance.h +++ b/render_3D/render_3D/renderable/Inheritance.h @@ -94,21 +94,21 @@ protected: private: template - using State_Storage = Double_State_Storage< + using State_Storage = Published_State_Storage< State_Type, Atomic_Spin_Mutex, State_Observer>; template State_Storage& state_storage() noexcept { - return dynamic_cast&>( - this->template d_func< - ::renderive::render_3d::Renderable::Impl>()); + return static_cast&>( + *this->template d_func< + ::renderive::render_3d::Renderable::Impl>().state_strategy); } template const State_Storage& state_storage() const noexcept { - return dynamic_cast&>( - this->template d_func< - ::renderive::render_3d::Renderable::Impl>()); + return static_cast&>( + *this->template d_func< + ::renderive::render_3d::Renderable::Impl>().state_strategy); } }; diff --git a/render_3D/render_3D/renderable/Renderable.cpp b/render_3D/render_3D/renderable/Renderable.cpp index 885b1bb..52ff2eb 100644 --- a/render_3D/render_3D/renderable/Renderable.cpp +++ b/render_3D/render_3D/renderable/Renderable.cpp @@ -1,33 +1,26 @@ #include "Renderable.h" #include "Renderable_p.h" - namespace renderive::render_3d { - Renderable::Renderable() : render_base(std::make_unique()) {} - Renderable::~Renderable() = default; - -Renderable_Observation Renderable::observation() const noexcept { +Renderable_Observation Renderable::observation() const { const auto& impl = d_func(); std::lock_guard lock(impl.observation_mutex); return impl.observation; } - void Renderable::Impl::observe( - const detail::Renderable_Event_View& value) noexcept { + const detail::Renderable_Event_View& value) { std::lock_guard lock(observation_mutex); observation.event = value.event; observation.event_time_ns = value.time_ns; observation.cache_update_count = value.cache_update_count; observation.publish_count = value.publish_count; } - void Renderable::Impl::observe_data(std::uint64_t revision, - std::size_t item_count) noexcept { + std::size_t item_count) { std::lock_guard lock(observation_mutex); observation.event = Renderable_Observer_Event::Data_Updated; observation.data_revision = revision; observation.item_count = item_count; } - } // namespace renderive::render_3d diff --git a/render_3D/render_3D/renderable/Renderable.h b/render_3D/render_3D/renderable/Renderable.h index ebef646..1ab2bbd 100644 --- a/render_3D/render_3D/renderable/Renderable.h +++ b/render_3D/render_3D/renderable/Renderable.h @@ -56,7 +56,7 @@ struct Renderable Renderable(); ~Renderable() override; - [[nodiscard]] Renderable_Observation observation() const noexcept; + [[nodiscard]] Renderable_Observation observation() const; protected: struct Impl; @@ -84,7 +84,7 @@ struct Renderable_State_Observer { : renderable_(renderable) {} template - void observe(const Observation& observation) noexcept { + void observe(const Observation& observation) { renderable_->report_state_observation(observation); } diff --git a/render_3D/render_3D/renderable/Renderable_p.h b/render_3D/render_3D/renderable/Renderable_p.h index ea0d7fa..88b8b03 100644 --- a/render_3D/render_3D/renderable/Renderable_p.h +++ b/render_3D/render_3D/renderable/Renderable_p.h @@ -46,7 +46,7 @@ struct Renderable::Impl struct Observer : ::renderive::renderable_inheritance::Observer_Root { static void handle( - Impl& impl, const detail::Renderable_Event_View& value) noexcept { + Impl& impl, const detail::Renderable_Event_View& value) { impl.observe(value); } }; @@ -59,10 +59,10 @@ protected: dispatch(detail::renderable_payload_view(payload)); } void observe_data(std::uint64_t revision, - std::size_t item_count) noexcept; + std::size_t item_count); private: - void observe(const detail::Renderable_Event_View& value) noexcept; + void observe(const detail::Renderable_Event_View& value); mutable std::mutex observation_mutex; Renderable_Observation observation; diff --git a/render_3D/render_3D/visuals/Basic_Visual.h b/render_3D/render_3D/visuals/Basic_Visual.h index aa87de9..aee51ad 100644 --- a/render_3D/render_3D/visuals/Basic_Visual.h +++ b/render_3D/render_3D/visuals/Basic_Visual.h @@ -90,21 +90,25 @@ struct Basic_Visual : Renderable> { } }; - explicit Impl(std::vector items) { update(std::move(items)); } + explicit Impl(std::vector items) { + if (update(std::move(items)) != Visual_Data_Error::none) + throw std::invalid_argument(std::string(Spec::name) + + " payload contains invalid data"); + } std::shared_ptr real_time_data_binding; - void update(std::vector items) { + [[nodiscard]] Visual_Data_Error update(std::vector items) { for (const auto& item : items) { if (!Spec::valid(item)) - throw std::invalid_argument(std::string(Spec::name) + - " payload contains invalid data"); + return Visual_Data_Error::invalid_data; } const auto count = items.size(); this->template write>( std::make_shared>(std::move(items))); this->report_observation(Visual_Data_Observation{ this->template cache_revision>(), count}); + return Visual_Data_Error::none; } [[nodiscard]] const Payload& published_items() const noexcept { @@ -147,19 +151,20 @@ struct Basic_Visual : Renderable> { } ~Basic_Visual() override = default; - void update_items(std::vector items) { - this->template d_func().update(std::move(items)); - this->d_func().changed(); + [[nodiscard]] Visual_Data_Error update_items(std::vector items) { + const auto error = this->template d_func().update(std::move(items)); + if (error == Visual_Data_Error::none) + this->d_func().changed(); + return error; } - - void edit_items(const std::function&)>& edit) { + [[nodiscard]] Visual_Data_Error edit_items(const std::function&)>& edit) { if (!edit) - throw std::invalid_argument("visual item edit is empty"); + return Visual_Data_Error::empty_edit; const auto& cached = this->template d_func() .template cache_buffer_value>(); auto next = cached ? *cached : std::vector{}; edit(next); - update_items(std::move(next)); + return update_items(std::move(next)); } [[nodiscard]] std::size_t item_count() const noexcept { diff --git a/render_3D/tests/Gpu_Completion_Service_Tests.cpp b/render_3D/tests/Gpu_Completion_Service_Tests.cpp index beb0fc9..6272895 100644 --- a/render_3D/tests/Gpu_Completion_Service_Tests.cpp +++ b/render_3D/tests/Gpu_Completion_Service_Tests.cpp @@ -6,17 +6,17 @@ #include namespace renderive::render_3d::detail { namespace { -static_assert(noexcept(std::declval().watch(VK_NULL_HANDLE, VK_NULL_HANDLE))); TEST(GpuCompletionService, UsesOneProcessWideService) { EXPECT_EQ(&Gpu_Completion_Service::instance(), &Gpu_Completion_Service::instance()); } TEST(GpuCompletionService, AbandonedReservationDoesNotComplete) { std::atomic completion_count{}; { - auto reservation = Gpu_Completion_Service::instance().prepare( + auto prepared = Gpu_Completion_Service::instance().prepare( [&](Gpu_Completion_Service::Result) { completion_count.fetch_add(1, std::memory_order_relaxed); }, false); + ASSERT_TRUE(prepared); } EXPECT_EQ(completion_count.load(std::memory_order_relaxed), 0); } @@ -24,7 +24,8 @@ TEST(GpuCompletionService, CanceledReservationWakesIdleService) { auto& service = Gpu_Completion_Service::instance(); const auto baseline = service.statistics().in_flight; { - auto reservation = service.prepare([](Gpu_Completion_Service::Result) {}, false); + auto prepared = service.prepare([](Gpu_Completion_Service::Result) {}, false); + ASSERT_TRUE(prepared); EXPECT_EQ(service.statistics().in_flight, baseline + 1); } const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); diff --git a/render_3D/tests/Point_Render_Integration_Tests.cpp b/render_3D/tests/Point_Render_Integration_Tests.cpp index de59806..785656a 100644 --- a/render_3D/tests/Point_Render_Integration_Tests.cpp +++ b/render_3D/tests/Point_Render_Integration_Tests.cpp @@ -128,7 +128,9 @@ TEST(PointRenderIntegration, try { auto demo = make_test_scene(Scene_Options{.viewport = {320, 200}}); auto& kernel_scene = demo.scene->render_scene(); - const Capture_Session_Id capture = kernel_scene.capture_next_frame(); + const auto request = kernel_scene.capture_next_frame(); + ASSERT_TRUE(request); + const Capture_Session_Id capture = request.session_id; ASSERT_TRUE(demo.scene->request_frame()); const auto session = kernel_scene.capture_session(capture); diff --git a/render_3D/tests/Point_State_Tests.cpp b/render_3D/tests/Point_State_Tests.cpp index 2446381..0c2ec5f 100644 --- a/render_3D/tests/Point_State_Tests.cpp +++ b/render_3D/tests/Point_State_Tests.cpp @@ -41,8 +41,7 @@ struct Test_Scene final } auto renderer = frame_control.acquire_renderer(); ASSERT_TRUE(renderer); - render(*renderer); - wait_for_render(); + Scene_Base::render(*renderer); } Node_Execution_Result render_scene( diff --git a/render_3D/tests/Render_Domain_Tests.cpp b/render_3D/tests/Render_Domain_Tests.cpp index df54e3d..aeb3fa4 100644 --- a/render_3D/tests/Render_Domain_Tests.cpp +++ b/render_3D/tests/Render_Domain_Tests.cpp @@ -8,7 +8,6 @@ #include namespace renderive::render_3d::detail { namespace { -static_assert(noexcept(std::declval().post(std::declval()))); TEST(RenderDomain, SharesDomainPerGpuIndex) { auto first = Render_Domain::acquire(0x7ffffffcU); auto second = Render_Domain::acquire(0x7ffffffcU); @@ -19,26 +18,32 @@ TEST(RenderDomain, SharesDomainPerGpuIndex) { TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) { auto domain = Render_Domain::acquire(0x7ffffffaU); std::atomic executed{}; - auto task = domain->prepare([&] { + auto prepared = domain->prepare([&] { executed.store(true, std::memory_order_release); }); - domain->post(std::move(task)); - domain->invoke([] {}); + ASSERT_TRUE(prepared); + EXPECT_EQ(domain->post(std::move(prepared.task)), Render_Domain::Error::none); + EXPECT_TRUE(domain->invoke([] {})); EXPECT_TRUE(executed.load(std::memory_order_acquire)); } TEST(RenderDomain, AbandonedPreparedTasksReleaseReservedCapacity) { auto domain = Render_Domain::acquire(0x7ffffff9U); for (std::size_t index = 0; index < 128; ++index) { - auto task = domain->prepare([] {}); + auto prepared = domain->prepare([] {}); + ASSERT_TRUE(prepared); } - domain->invoke([] {}); + EXPECT_TRUE(domain->invoke([] {})); } TEST(RenderDomain, NestedInvokeExecutesInlineOnTheAffinityThread) { auto domain = Render_Domain::acquire(0x7ffffff8U); - const int value = domain->invoke([domain] { - return domain->invoke([] { return 42; }); + const auto value = domain->invoke([domain] { + const auto nested = domain->invoke([] { return 42; }); + if (!nested) + throw std::logic_error("nested render-domain invoke unexpectedly stopped"); + return *nested.value; }); - EXPECT_EQ(value, 42); + ASSERT_TRUE(value); + EXPECT_EQ(*value.value, 42); } TEST(RenderDomain, ReportsBoundedAdmissionStatistics) { auto domain = Render_Domain::acquire(0x7ffffff7U); @@ -50,11 +55,14 @@ TEST(RenderDomain, ReportsBoundedAdmissionStatistics) { 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([] {}); + EXPECT_EQ(domain->post([] { throw std::runtime_error("unexpected render-domain failure"); }), + Render_Domain::Error::none); + EXPECT_TRUE(domain->invoke([] {})); const auto after = domain->statistics().unhandled_exception_count; EXPECT_EQ(after, before + 1U); - EXPECT_EQ(domain->invoke([] { return 17; }), 17); + const auto value = domain->invoke([] { return 17; }); + ASSERT_TRUE(value); + EXPECT_EQ(*value.value, 17); } TEST(RenderDomain, FinalOwnerMayBeReleasedOnAffinityThread) { @@ -62,10 +70,10 @@ TEST(RenderDomain, FinalOwnerMayBeReleasedOnAffinityThread) { std::weak_ptr weak = domain; std::promise released; auto finished = released.get_future(); - domain->post([owned = domain, &released]() mutable { + EXPECT_EQ(domain->post([owned = domain, &released]() mutable { owned.reset(); released.set_value(); - }); + }), Render_Domain::Error::none); domain.reset(); EXPECT_EQ(finished.wait_for(std::chrono::seconds(1)), std::future_status::ready); diff --git a/web_server/CMakeLists.txt b/web_server/CMakeLists.txt index 229706a..f7acce5 100644 --- a/web_server/CMakeLists.txt +++ b/web_server/CMakeLists.txt @@ -199,11 +199,9 @@ if (RENDERIVE_BUILD_TESTS) continue() endif () file(RELATIVE_PATH Renderive_Web_test_name "${Renderive_Web_test_dir}" "${Renderive_Web_test_source}") - string(MD5 Renderive_Web_test_hash "${Renderive_Web_test_name}") - string(SUBSTRING "${Renderive_Web_test_hash}" 0 8 Renderive_Web_test_hash) - get_filename_component(Renderive_Web_test_name "${Renderive_Web_test_source}" NAME_WE) + string(REGEX REPLACE "\\.[^.]+$" "" Renderive_Web_test_name "${Renderive_Web_test_name}") string(MAKE_C_IDENTIFIER "${Renderive_Web_test_name}" Renderive_Web_test_name) - set(Renderive_Web_test_target "Renderive_Web_${Renderive_Web_test_name}_${Renderive_Web_test_hash}") + set(Renderive_Web_test_target "Renderive_Web_${Renderive_Web_test_name}") add_executable("${Renderive_Web_test_target}" "${Renderive_Web_test_source}") target_link_libraries("${Renderive_Web_test_target}" PRIVATE Renderive_Web Adminive::Nlohmann GTest::gtest_main) renderive_stage_render_3D_runtime("${Renderive_Web_test_target}") diff --git a/web_server/app/Gallery_Session_Control_Adminive.h b/web_server/app/Gallery_Session_Control_Adminive.h index 343d095..b7fa5d2 100644 --- a/web_server/app/Gallery_Session_Control_Adminive.h +++ b/web_server/app/Gallery_Session_Control_Adminive.h @@ -90,8 +90,9 @@ struct Type_Descriptor> { [](const T& value) { return value.scene_.max_render_fps() > 0.0; }, [dirty](T& value, bool enabled) { if (enabled) { - if (value.scene_.max_render_fps() <= 0.0) - value.scene_.set_max_render_fps(30.0); + if (value.scene_.max_render_fps() <= 0.0 && + value.scene_.set_max_render_fps(30.0) != renderive::Plot_Control_Error::none) + return; } else { value.scene_.clear_max_render_fps(); } @@ -104,8 +105,9 @@ struct Type_Descriptor> { return fps > 0.0 ? fps : 30.0; }, [dirty](T& value, double fps) { - if (value.scene_.max_render_fps() > 0.0) - value.scene_.set_max_render_fps(fps); + if (value.scene_.max_render_fps() > 0.0 && + value.scene_.set_max_render_fps(fps) != renderive::Plot_Control_Error::none) + return; dirty(value); }), callback_property( diff --git a/web_server/app/Web_Plot_Session.cpp b/web_server/app/Web_Plot_Session.cpp index 1ce2666..4a7c153 100644 --- a/web_server/app/Web_Plot_Session.cpp +++ b/web_server/app/Web_Plot_Session.cpp @@ -64,7 +64,7 @@ struct Web_Plot_Session::Impl { Impl() { plot.init(); plot.set_background_color({3, 7, 18, 255}); - plot.set_max_render_fps(30.0); + static_cast(plot.set_max_render_fps(30.0)); plot.set_viewport_size({960, 600}); const auto root = plot.root_renderable(); constexpr Color axis_color{93, 116, 151, 255}; diff --git a/web_server/app/render_2D/Gallery_Scene2D.cpp b/web_server/app/render_2D/Gallery_Scene2D.cpp index f48b5da..42b1f9f 100644 --- a/web_server/app/render_2D/Gallery_Scene2D.cpp +++ b/web_server/app/render_2D/Gallery_Scene2D.cpp @@ -190,7 +190,7 @@ public: register_controls(); set_performance_plot_name(plot_, case_id_ + "/" + gallery_enum_id(frame_mode_)); if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) - plot_.set_max_render_fps(30.0); + static_cast(plot_.set_max_render_fps(30.0)); plot_.activate_view(); update_model(); (void)plot_.render_frame(true); @@ -368,8 +368,13 @@ public: const auto state = plot_.render_scene().capture_state(); if (state.enabled()) return "A performance capture session is already active"; - const auto session_id = plot_.render_scene().capture_next_frame(); - last_action_result_ = "capture session=" + std::to_string(session_id); + const auto capture = plot_.render_scene().capture_next_frame(); + if (!capture) { + if (capture.error == Capture_Error::session_active) + return "A performance capture session is already active"; + throw std::logic_error("validated capture request rejected"); + } + last_action_result_ = "capture session=" + std::to_string(capture.session_id); return "Capture next render frame requested"; } if (request.id == "capture_frames") { @@ -381,8 +386,13 @@ public: const auto state = plot_.render_scene().capture_state(); if (state.enabled()) return "A performance capture session is already active"; - const auto session_id = plot_.render_scene().capture_frames(count); - last_action_result_ = "capture session=" + std::to_string(session_id) + + const auto capture = plot_.render_scene().capture_frames(count); + if (!capture) { + if (capture.error == Capture_Error::session_active) + return "A performance capture session is already active"; + throw std::logic_error("validated capture request rejected"); + } + last_action_result_ = "capture session=" + std::to_string(capture.session_id) + ", frames=" + std::to_string(count); return "Consecutive render frame capture requested"; } @@ -914,7 +924,7 @@ private: plot_.clear_consumer_feedback(); return; } - plot_.set_consumer_feedback({interval_ns}); + static_cast(plot_.set_consumer_feedback({interval_ns})); } void record_performance(std::chrono::steady_clock::time_point started, bool rendered) { const auto finished = std::chrono::steady_clock::now(); diff --git a/web_server/app/render_3D/Gallery_Scene3D.cpp b/web_server/app/render_3D/Gallery_Scene3D.cpp index b0e01a2..9a68df5 100644 --- a/web_server/app/render_3D/Gallery_Scene3D.cpp +++ b/web_server/app/render_3D/Gallery_Scene3D.cpp @@ -340,9 +340,14 @@ public: auto& scene = scene_.render_scene(); if (scene.capture_state().enabled()) return "A performance capture session is already active"; - const auto session_id = scene.capture_next_frame(); + const auto capture = scene.capture_next_frame(); + if (!capture) { + if (capture.error == Capture_Error::session_active) + return "A performance capture session is already active"; + throw std::logic_error("validated capture request rejected"); + } last_action_result_ = - "capture session=" + std::to_string(session_id); + "capture session=" + std::to_string(capture.session_id); return "Next 3D frame capture requested"; } if (request.id == "capture_frames") { @@ -356,9 +361,14 @@ public: auto& scene = scene_.render_scene(); if (scene.capture_state().enabled()) return "A performance capture session is already active"; - const auto session_id = scene.capture_frames(count); + const auto capture = scene.capture_frames(count); + if (!capture) { + if (capture.error == Capture_Error::session_active) + return "A performance capture session is already active"; + throw std::logic_error("validated capture request rejected"); + } last_action_result_ = "capture session=" + - std::to_string(session_id) + + std::to_string(capture.session_id) + " frames=" + std::to_string(count); return "Consecutive 3D frame capture requested"; }