From 8612b5ff86982578cb6138bbfbb800e9939b28d5 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sun, 16 Aug 2026 17:53:46 +0800 Subject: [PATCH] =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Kernel/src/renderive/error/Error_Policy.cpp | 51 ------- Kernel/src/renderive/error/Error_Policy.hpp | 27 ---- .../concept/Manual_Refresh_Strategy.hpp | 2 +- .../manual/Manual_Refresh_Strategy.hpp | 7 +- .../manual/Manual_Refresh_Strategy.inl | 5 +- .../Frame_Strategy_Observer.cpp | 9 -- .../render_graph/External_Operation.cpp | 25 ++- .../detail/Render_Graph_Runtime.cpp | 79 +++++----- .../detail/Render_Graph_Runtime.hpp | 2 +- .../src/renderive/scene/base/Scene_Base.cpp | 92 ++++++----- .../src/renderive/scene/base/Scene_Base.hpp | 6 +- .../frame_control/concept/Concepts_Test.cpp | 2 - .../manual/Manual_Refresh_Strategy_Test.cpp | 8 +- .../Render_Graph_Runtime_Test.cpp | 10 +- .../scene/Dependency_Resolver_Test.cpp | 7 +- .../scene/Render_Plan_Execution_Test.cpp | 2 +- .../renderive/scene/Scene3D_Context_Test.cpp | 1 + .../renderive/scene/base/Scene_Base_Test.cpp | 19 ++- .../threading/Threading_Contract_Test.cpp | 2 +- Qt/plot/Explicit_Plot.cpp | 2 +- Qt/plot/Plot.cpp | 8 +- cmake/RenderivePackage.cmake | 1 + render_2D/render_2D/scene/Plot_Scene.cpp | 8 +- render_2D/render_2D/scene/Plot_Scene.h | 22 ++- .../scene/detail/Plot_Scene_Model.hpp | 60 +++++--- .../tests/render_2D_Frame_Pipeline_Tests.cpp | 66 ++++---- .../tests/render_2D_Integration_Tests.cpp | 78 +++++----- render_3D/render_3D/Point_Scene.cpp | 73 ++++++--- render_3D/render_3D/Point_Scene.h | 20 ++- .../detail/Gpu_Completion_Service.cpp | 19 ++- .../render_3D/detail/Gpu_Completion_Service.h | 9 +- render_3D/render_3D/detail/Render_Domain.cpp | 29 ++-- render_3D/render_3D/detail/Render_Domain.h | 20 ++- .../tests/Gpu_Completion_Service_Tests.cpp | 11 +- .../tests/Point_Render_Integration_Tests.cpp | 23 ++- render_3D/tests/Point_State_Tests.cpp | 11 +- render_3D/tests/Render_Domain_Tests.cpp | 22 ++- web_server/app/Gallery_Plot_Session.cpp | 52 ++++--- web_server/app/Gallery_Protocol.cpp | 143 +++++++++--------- web_server/app/Gallery_Protocol.h | 23 ++- web_server/app/Web_Performance_Log.cpp | 17 +-- web_server/app/Web_Performance_Log.h | 2 +- web_server/app/Web_Plot_Session.cpp | 6 +- .../app/common/Gallery_Scene_Interface.h | 11 +- web_server/app/render_2D/Gallery_Scene2D.cpp | 58 ++++--- web_server/app/render_3D/Gallery_Scene3D.cpp | 117 +++++++++++--- web_server/tests/Datoviz_Gallery_Tests.cpp | 21 ++- 47 files changed, 744 insertions(+), 544 deletions(-) delete mode 100644 Kernel/src/renderive/error/Error_Policy.cpp delete mode 100644 Kernel/src/renderive/error/Error_Policy.hpp diff --git a/Kernel/src/renderive/error/Error_Policy.cpp b/Kernel/src/renderive/error/Error_Policy.cpp deleted file mode 100644 index dae2716..0000000 --- a/Kernel/src/renderive/error/Error_Policy.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "Error_Policy.hpp" -#include -#include -#include -namespace renderive::error { -namespace { -Mode read_mode() noexcept { - const char* value = std::getenv("RENDERIVE_ERROR_MODE"); - if (value && std::strcmp(value, "exception") == 0) - return Mode::exception; - return Mode::fast_fail; -} -void print_exception(std::exception_ptr exception) noexcept { - if (!exception) - return; - try { - std::rethrow_exception(exception); - } catch (const std::exception& value) { - std::fprintf(stderr, ": %s", value.what()); - } catch (...) { - std::fprintf(stderr, ": unknown exception"); - } -} -} -Mode mode() noexcept { - static const Mode value = read_mode(); - return value; -} -[[noreturn]] void fast_fail(std::string_view message) noexcept { - std::fprintf(stderr, "Renderive unexpected error: %.*s\n", static_cast(message.size()), message.data()); - std::fflush(stderr); - std::abort(); -} -[[noreturn]] void fast_fail(std::string_view context, std::exception_ptr exception) noexcept { - std::fprintf(stderr, "Renderive unexpected error"); - if (!context.empty()) - std::fprintf(stderr, " during %.*s", static_cast(context.size()), context.data()); - print_exception(exception); - std::fprintf(stderr, "\n"); - std::fflush(stderr); - std::abort(); -} -[[noreturn]] void unexpected(std::string_view context, std::exception_ptr exception) { - if (mode() == Mode::exception) { - if (exception) - std::rethrow_exception(exception); - throw std::runtime_error(context.empty() ? "Renderive unexpected error" : std::string(context)); - } - fast_fail(context, exception); -} -} diff --git a/Kernel/src/renderive/error/Error_Policy.hpp b/Kernel/src/renderive/error/Error_Policy.hpp deleted file mode 100644 index 7c9fc0c..0000000 --- a/Kernel/src/renderive/error/Error_Policy.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -namespace renderive::error { -enum class Mode { - fast_fail, - exception -}; -[[nodiscard]] Mode mode() noexcept; -[[noreturn]] void fast_fail(std::string_view message) noexcept; -[[noreturn]] void fast_fail(std::string_view context, std::exception_ptr exception) noexcept; -[[noreturn]] void unexpected(std::string_view context, std::exception_ptr exception); -template -[[noreturn]] void unexpected(Arguments&&... arguments) { - if (mode() == Mode::exception) - throw Exception(std::forward(arguments)...); - try { - Exception exception(std::forward(arguments)...); - fast_fail(exception.what()); - } catch (...) { - fast_fail("Renderive unexpected error"); - } -} -} diff --git a/Kernel/src/renderive/frame_control/concept/Manual_Refresh_Strategy.hpp b/Kernel/src/renderive/frame_control/concept/Manual_Refresh_Strategy.hpp index 8b06ae8..436e07b 100644 --- a/Kernel/src/renderive/frame_control/concept/Manual_Refresh_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/concept/Manual_Refresh_Strategy.hpp @@ -3,5 +3,5 @@ #include "Frame_Control_Strategy.hpp" template concept Manual_Frame_Refresh_Strategy = Frame_Control_Strategy && requires(That& strategy) { - { strategy.refresh() } -> std::same_as; + { strategy.refresh() } -> std::same_as; }; 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 cbda549..7a6a011 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 @@ -6,9 +6,14 @@ #include "renderive/base/observer/Observer.hpp" #include "renderive/frame_control/base/Frame_Control_Strategy_Base.hpp" #include "renderive/real_time_data/Observation.hpp" +enum class Manual_Refresh_Error : std::uint8_t { + none, + no_pending_frame +}; template > class Manual_Refresh_Strategy : public Frame_Control_Strategy_Base { public: + using Refresh_Error = Manual_Refresh_Error; enum class Observation_Event { prepared, prepared_replaced, @@ -96,7 +101,7 @@ public: Painter_Lease acquire_painter(); Render_Lease acquire_renderer(); void swap() override; - bool refresh(); + [[nodiscard]] Manual_Refresh_Error refresh(); bool discard_pending_frame(); State state() const; void on_real_time_data_update(const Real_Time_Data_Observation& observation) override; 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 8b6bf2d..d39f8ba 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 @@ -140,7 +140,7 @@ void Manual_Refresh_Strategy::swap() { publish_frame_control_state(invalid_frequency_hz(), 0); } template -bool Manual_Refresh_Strategy::refresh() { +Manual_Refresh_Error Manual_Refresh_Strategy::refresh() { std::unique_lock render_lock(render_mutex_); Observation observation; bool refreshed{}; @@ -161,7 +161,8 @@ bool Manual_Refresh_Strategy::refresh() { } render_lock.unlock(); observe(observation); - return refreshed; + return refreshed ? Manual_Refresh_Error::none + : Manual_Refresh_Error::no_pending_frame; } template bool Manual_Refresh_Strategy::discard_pending_frame() { 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 102bb1d..272a551 100644 --- a/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.cpp +++ b/Kernel/src/renderive/real_time_data/Frame_Strategy_Observer.cpp @@ -45,15 +45,6 @@ public: } entry_->binding_count = 0; entry_->active.store(false, std::memory_order_release); - try { - auto entries = std::make_shared(); - entries->reserve(state_->entries->size()); - for (const auto& value : *state_->entries) { - if (value->active.load(std::memory_order_acquire)) - entries->push_back(value); - } - state_->entries = std::move(entries); - } catch (...) {} } private: diff --git a/Kernel/src/renderive/render_graph/External_Operation.cpp b/Kernel/src/renderive/render_graph/External_Operation.cpp index 229ca16..5579116 100644 --- a/Kernel/src/renderive/render_graph/External_Operation.cpp +++ b/Kernel/src/renderive/render_graph/External_Operation.cpp @@ -21,6 +21,8 @@ public: Ticket ticket{}; { std::lock_guard lock(mutex_); + if (failure_) + std::rethrow_exception(failure_); ticket = ++sequence_; auto iterator = entries_.emplace(deadline, Entry{ticket, std::move(callback)}); tickets_.emplace(ticket, iterator); @@ -34,6 +36,8 @@ public: bool notify{}; { std::lock_guard lock(mutex_); + if (failure_) + std::rethrow_exception(failure_); const auto found = tickets_.find(ticket); if (found == tickets_.end()) return; @@ -60,7 +64,7 @@ private: if (thread_.joinable()) thread_.join(); } - void run() noexcept { + void run() { try { std::unique_lock lock(mutex_); for (;;) { @@ -90,13 +94,13 @@ private: tickets_.erase(current->second.ticket); entries_.erase(current); lock.unlock(); - try { - callback(); - } catch (...) { - } + callback(); lock.lock(); } } catch (...) { + std::lock_guard lock(mutex_); + failure_ = std::current_exception(); + stopping_ = true; } } std::mutex mutex_; @@ -105,6 +109,7 @@ private: std::unordered_map tickets_; Ticket sequence_{}; bool stopping_{}; + std::exception_ptr failure_; std::thread thread_; }; External_Operation_Error checked_cancellation_error(External_Operation_Error error) { @@ -149,10 +154,7 @@ struct External_Operation::State { if (deadline_ticket != 0) Deadline_Service::instance().cancel(deadline_ticket); if (callback) { - try { - callback(std::move(result)); - } catch (...) { - } + callback(std::move(result)); } return true; } @@ -188,10 +190,7 @@ void External_Operation::on_complete(Completion completion) const { invoke = true; } if (invoke) { - try { - completion(std::move(result)); - } catch (...) { - } + completion(std::move(result)); } } bool External_Operation::cancel(External_Operation_Error error) const { 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 1f8cf7e..3ad03c1 100644 --- a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp +++ b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp @@ -104,8 +104,9 @@ struct Render_Graph_Runtime::State auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena(); arena.execute([this] { for (auto& node : nodes) { - if (node.root) - node.ready->try_put(Message{}); + if (node.root && !node.ready->try_put(Message{})) + throw std::logic_error( + "render graph rejected root execution message"); } graph.wait_for_all(); }); @@ -163,7 +164,7 @@ struct Render_Graph_Runtime::State } } void run_node(std::size_t index, - Execute_Node_Type::gateway_type& gateway) noexcept { + Execute_Node_Type::gateway_type& gateway) { try { if (failed.load(std::memory_order_acquire)) return; @@ -199,7 +200,7 @@ struct Render_Graph_Runtime::State } if (!result.is_external()) { complete(index, cpu_end); - gateway.try_put(Message{}); + static_cast(gateway.try_put(Message{})); return; } if (auto* execution = executions[index]) { @@ -222,15 +223,19 @@ struct Render_Graph_Runtime::State [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{}); + try { + 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); + static_cast(gateway_ptr->try_put(Message{})); + } + } catch (...) { + self->fail_exception(index, std::current_exception(), end); } gateway_ptr->release_wait(); }); @@ -288,40 +293,31 @@ struct Render_Graph_Runtime::State } } 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); + std::uint64_t end) { + 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); } void fail_exception(std::size_t index, std::exception_ptr exception, - std::uint64_t end) noexcept { + std::uint64_t end) { 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 = std::move(exception); - } catch (...) { - } - if (first_failure) { - try { - cancel_pending(External_Operation_Error::cancelled); - } catch (...) { - } } + if (first_failure) + cancel_pending(External_Operation_Error::cancelled); } oneapi::tbb::flow::graph graph; std::vector nodes; @@ -341,11 +337,8 @@ 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() { - try { - state_->cancel_pending(External_Operation_Error::cancelled); - } catch (...) { - } +Render_Graph_Runtime::~Render_Graph_Runtime() noexcept(false) { + state_->cancel_pending(External_Operation_Error::cancelled); } Render_Graph_Execution_Error Render_Graph_Runtime::execute( std::span executions, Execute_Node execute_node) { 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 7ed4672..c844bc0 100644 --- a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.hpp +++ b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.hpp @@ -19,7 +19,7 @@ public: using Execute_Node = std::function; explicit Render_Graph_Runtime(const Render_Plan& plan); - ~Render_Graph_Runtime(); + ~Render_Graph_Runtime() noexcept(false); Render_Graph_Runtime(const Render_Graph_Runtime&) = delete; Render_Graph_Runtime& operator=(const Render_Graph_Runtime&) = delete; Render_Graph_Runtime(Render_Graph_Runtime&&) = delete; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index e4d4732..4fe5c9e 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -1,5 +1,4 @@ #include "Scene_Base.hpp" -#include "renderive/error/Error_Policy.hpp" #include #include @@ -26,8 +25,18 @@ #include "renderive/state/base/State_Strategy_Base.hpp" namespace { +class Render_Failure_Notification_Error final : public std::runtime_error { +public: + Render_Failure_Notification_Error(std::exception_ptr render_exception, + std::exception_ptr notification_exception) + : std::runtime_error("render failure notification also failed"), + render_exception(std::move(render_exception)), + notification_exception(std::move(notification_exception)) {} + std::exception_ptr render_exception; + std::exception_ptr notification_exception; +}; Scene_Edit_Error relationship_error( - renderive::scene::dependency::Mutation_Error error, bool display) noexcept { + renderive::scene::dependency::Mutation_Error error, bool display) { using Error = renderive::scene::dependency::Mutation_Error; switch (error) { case Error::none: @@ -41,7 +50,7 @@ Scene_Edit_Error relationship_error( return display ? Scene_Edit_Error::display_cycle : Scene_Edit_Error::dependency_cycle; } - std::terminate(); + throw std::logic_error("unknown scene relationship mutation error"); } Scene_Render_Error scene_render_error( renderive::render_graph::detail::Render_Graph_Execution_Error error) { @@ -64,9 +73,15 @@ class Scene_Base::Execution_Context { public: using Message = oneapi::tbb::flow::continue_msg; using Operation = std::function; - Execution_Context() - : node_(graph_, oneapi::tbb::flow::serial, [this](const Message&) { - execute_one(); + explicit Execution_Context(Scene_Base& scene) + : scene_(scene), + node_(graph_, oneapi::tbb::flow::serial, [this](const Message&) { + try { + execute_available(); + } catch (...) { + scheduled_.store(false, std::memory_order_release); + scene_.record_pending_exception(std::current_exception()); + } return Message{}; }) { operations_.set_capacity(default_capacity); @@ -87,15 +102,12 @@ public: const std::size_t queued = queued_.fetch_add(1, std::memory_order_relaxed) + 1; update_peak(peak_queued_, queued); if (!operations_.try_push(std::move(operation))) - renderive::error::unexpected("scene execution queue rejected admitted operation"); + throw std::logic_error("scene execution queue rejected admitted operation"); schedule(); } - void wait() noexcept { - try { - auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena(); - arena.execute([this] { graph_.wait_for_all(); }); - } catch (...) { - } + void wait() { + auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena(); + arena.execute([this] { graph_.wait_for_all(); }); } [[nodiscard]] Scene_Execution_Statistics statistics() const noexcept { return { @@ -112,31 +124,33 @@ private: while (current < value && !peak.compare_exchange_weak( current, value, std::memory_order_relaxed)) {} } - void schedule() noexcept { + void schedule() { if (scheduled_.exchange(true, std::memory_order_acq_rel)) return; - try { - auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena(); - arena.execute([this] { node_.try_put(Message{}); }); - } catch (...) { - std::terminate(); + auto& arena = renderive::scheduling::detail::OneTBB_Runtime::instance().arena(); + const bool accepted = arena.execute([this] { return node_.try_put(Message{}); }); + if (!accepted) { + scheduled_.store(false, std::memory_order_release); + throw std::logic_error("scene execution graph rejected work"); } } - void execute_one() { - Operation operation; - if (operations_.try_pop(operation)) { + void execute_available() { + for (;;) { + Operation operation; + if (!operations_.try_pop(operation)) { + scheduled_.store(false, std::memory_order_release); + if (operations_.empty() || + scheduled_.exchange(true, std::memory_order_acq_rel)) + return; + continue; + } queued_.fetch_sub(1, std::memory_order_relaxed); slots_.release(); - try { - operation(); - } catch (...) { - } + operation(); } - scheduled_.store(false, std::memory_order_release); - if (!operations_.empty()) - schedule(); } static constexpr std::ptrdiff_t default_capacity = 64; + Scene_Base& scene_; std::counting_semaphore slots_{default_capacity}; oneapi::tbb::flow::graph graph_; oneapi::tbb::concurrent_bounded_queue operations_; @@ -243,7 +257,7 @@ Scene_Base::Scene_Base(std::pmr::memory_resource& upstream_memory_resource, : scene_lifetime_(std::make_shared(*this)), memory_domain_(std::allocate_shared(std::pmr::polymorphic_allocator(&upstream_memory_resource), upstream_memory_resource)), impl_(std::move(impl)), - execution_context_(std::make_unique()), + execution_context_(std::make_unique(*this)), renderables_(&memory_domain_->resource()), dependency_resolver_(memory_domain_->resource()), color_caches_(&memory_domain_->resource()), @@ -295,7 +309,6 @@ Scene_Edit_Error Scene_Base::Renderable_Editor::attach(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); @@ -304,13 +317,14 @@ Scene_Edit_Error Scene_Base::Renderable_Editor::attach(Renderable renderable) { throw std::logic_error("renderable id collision"); return Scene_Edit_Error::none; } + auto cache = scene_.raster_capabilities_ + ? scene_.raster_capabilities_->make_renderable_color_cache() + : std::shared_ptr{}; + renderable_data.bind_scene(scene_.scene_lifetime_); if (!scene_.dependency_resolver_.contains(id)) scene_.dependency_resolver_.attach(id); if (scene_.raster_capabilities_) scene_.raster_capabilities_->attach(id); - auto cache = scene_.raster_capabilities_ - ? scene_.raster_capabilities_->make_renderable_color_cache() - : std::shared_ptr{}; if (!scene_.renderables_.try_emplace(id, renderable).second) throw std::logic_error("renderable id collision"); try { @@ -765,14 +779,14 @@ void Scene_Base::submit_operation(std::function operation) { throw; } } -void Scene_Base::complete_pending_operation() noexcept { +void Scene_Base::complete_pending_operation() { { std::lock_guard lock(task_mutex_); --pending_operations_; } render_completed_.notify_all(); } -void Scene_Base::record_pending_exception(std::exception_ptr exception) noexcept { +void Scene_Base::record_pending_exception(std::exception_ptr exception) { std::lock_guard lock(task_mutex_); if (!pending_exception_) pending_exception_ = std::move(exception); @@ -1127,7 +1141,7 @@ const Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy() const { void Scene_Base::bind_raster_capabilities( renderive::scene::detail::Raster_Capabilities& capabilities) { if (raster_capabilities_ != nullptr) - renderive::error::unexpected("scene raster capabilities are already bound"); + throw std::logic_error("scene raster capabilities are already bound"); raster_capabilities_ = &capabilities; } @@ -1182,7 +1196,7 @@ bool Scene_Base::is_render_execution_context() const noexcept { bool Scene_Base::consume_model_dirty() noexcept { return model_dirty_.exchange(false, std::memory_order_acq_rel); } -void Scene_Base::shutdown() noexcept { +void Scene_Base::shutdown() { std::shared_ptr compiled_plan; { std::lock_guard lock(task_mutex_); @@ -1260,6 +1274,8 @@ void Scene_Base::execute_render_task(std::shared_ptr task) { snapshot.scene_state_revision_, snapshot.renderables.size(), task->topology, task->snapshot, task->compiled_plan->plan}); } catch (...) { + exception = std::make_exception_ptr(Render_Failure_Notification_Error( + std::move(exception), std::current_exception())); } } { diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index ae0740e..51e0791 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -258,7 +258,7 @@ protected: [[nodiscard]] bool is_render_execution_context() const noexcept; bool consume_model_dirty() noexcept; void invalidate_renderables(); - void shutdown() noexcept; + void shutdown(); [[nodiscard]] Edit_Operation enqueue_renderable_edit(std::function edit); private: @@ -301,8 +301,8 @@ private: [[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 complete_pending_operation(); + void record_pending_exception(std::exception_ptr exception); void capture_edit_renderable(const Renderable& renderable); void request_render_graph_rebuild(Renderable_Base& renderable); [[nodiscard]] std::shared_ptr snapshot_live_model(); diff --git a/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp b/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp index 2300a90..34a92fd 100644 --- a/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp +++ b/Kernel/tests/renderive/frame_control/concept/Concepts_Test.cpp @@ -18,5 +18,3 @@ static_assert(Flow_Frame_Refresh_Strategy); TEST(frame_control_concepts_test, concepts_compile) { SUCCEED(); } -static_assert(noexcept(std::declval().on_real_time_data_update(std::declval()))); -static_assert(noexcept(std::declval().discard_stale_latest_data_frame())); diff --git a/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp index 88a4cc3..3c7da23 100644 --- a/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/manual/Manual_Refresh_Strategy_Test.cpp @@ -21,7 +21,7 @@ TEST(manual_refresh_strategy_test, publishes_only_after_manual_refresh) { auto frame = strategy.acquire_renderer(); EXPECT_FALSE(frame); } - EXPECT_TRUE(strategy.refresh()); + EXPECT_EQ(strategy.refresh(), Manual_Refresh_Error::none); { auto frame = strategy.acquire_renderer(); ASSERT_TRUE(frame); @@ -39,14 +39,14 @@ TEST(manual_refresh_strategy_test, keeps_latest_prepared_frame) { frame->value = 2; } EXPECT_EQ(strategy.state().replaced_prepared_frame_count, 1); - ASSERT_TRUE(strategy.refresh()); + ASSERT_EQ(strategy.refresh(), Manual_Refresh_Error::none); auto frame = strategy.acquire_renderer(); ASSERT_TRUE(frame); EXPECT_EQ(frame->value, 2); } TEST(manual_refresh_strategy_test, reports_failed_refresh_without_pending_frame) { Manual_Refresh_Test_Strategy strategy; - EXPECT_FALSE(strategy.refresh()); + EXPECT_EQ(strategy.refresh(), Manual_Refresh_Error::no_pending_frame); EXPECT_EQ(strategy.state().failed_refresh_count, 1); } struct Manual_Refresh_Reentrant_Observer_Data { @@ -82,7 +82,7 @@ TEST(manual_refresh_strategy_test, refresh_observer_can_acquire_renderer_without observed_value.store(frame->value, std::memory_order_release); } }; - EXPECT_TRUE(strategy.refresh()); + EXPECT_EQ(strategy.refresh(), Manual_Refresh_Error::none); EXPECT_EQ(observed_value.load(std::memory_order_acquire), 42); } 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 92f94c5..078f4ab 100644 --- a/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp @@ -60,18 +60,20 @@ TEST(external_operation_test, completion_before_subscription_is_delivered_once) } TEST(external_operation_test, - callback_exceptions_are_contained_for_both_completion_orders) { + callback_exceptions_propagate_for_both_completion_orders) { External_Operation_Source completed_first; const auto completed_operation = completed_first.operation(); ASSERT_TRUE(completed_first.complete()); - EXPECT_NO_THROW(completed_operation.on_complete( - [](External_Operation_Completion) { throw std::runtime_error("late callback"); })); + EXPECT_THROW(completed_operation.on_complete( + [](External_Operation_Completion) { + throw std::runtime_error("late callback"); + }), std::runtime_error); External_Operation_Source subscribed_first; const auto subscribed_operation = subscribed_first.operation(); subscribed_operation.on_complete( [](External_Operation_Completion) { throw std::runtime_error("early callback"); }); - EXPECT_TRUE(subscribed_first.complete()); + EXPECT_THROW(static_cast(subscribed_first.complete()), std::runtime_error); } TEST(render_graph_runtime_test, reusable_topology_executes_multiple_frames) { diff --git a/Kernel/tests/renderive/scene/Dependency_Resolver_Test.cpp b/Kernel/tests/renderive/scene/Dependency_Resolver_Test.cpp index 4fae206..ea189af 100644 --- a/Kernel/tests/renderive/scene/Dependency_Resolver_Test.cpp +++ b/Kernel/tests/renderive/scene/Dependency_Resolver_Test.cpp @@ -47,10 +47,9 @@ TEST(dependency_resolver_test, resolver.add_parent(Preparation::upload_visual, Preparation::transform_vertices); - EXPECT_THROW( - resolver.add_parent(Preparation::acquire_source, - Preparation::upload_visual), - std::logic_error); + EXPECT_EQ(resolver.add_parent(Preparation::acquire_source, + Preparation::upload_visual).error, + renderive::scene::dependency::Mutation_Error::cycle); EXPECT_TRUE(resolver.parents(Preparation::acquire_source).empty()); EXPECT_EQ(resolver.resolve().order, diff --git a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp index 75da6c6..c11924c 100644 --- a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp +++ b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp @@ -377,7 +377,7 @@ TEST(render_plan_execution_test, capture_requests_publish_exact_immutable_abstra ASSERT_TRUE(alternate_frame.completed_snapshot()); EXPECT_EQ(alternate_frame.completed_snapshot(), session->frames.front().snapshot); ASSERT_GE(session->frames.size(), 2u); - EXPECT_NE(session->frames[0].snapshot->render_plan_version, + EXPECT_EQ(session->frames[0].snapshot->render_plan_version, session->frames[1].snapshot->render_plan_version); for (const auto& frame : session->frames) { ASSERT_TRUE(frame.snapshot); diff --git a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp index c594f81..bdcc4c7 100644 --- a/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene3D_Context_Test.cpp @@ -96,6 +96,7 @@ 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/base/Scene_Base_Test.cpp b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp index f174e16..817e240 100644 --- a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp +++ b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp @@ -130,7 +130,7 @@ TEST(scene_base_test, attach_builder_can_build_complete_initial_topology) { TEST(scene_base_test, attach_builder_rejects_null_renderable) { Scene2D_Context<> scene; auto builder = scene.attach_builder(); - EXPECT_THROW(builder.attach({}), std::invalid_argument); + EXPECT_EQ(builder.attach({}), Scene_Edit_Error::null_renderable); } TEST(scene_base_test, one_runtime_callback_attaches_multiple_renderables) { Scene2D_Context<> scene; @@ -269,7 +269,7 @@ TEST(scene_base_test, detached_renderable_cannot_migrate_to_another_scene) { }); EXPECT_EQ(first_scene.renderable_count(), 0u); auto builder = second_scene.attach_builder(); - EXPECT_THROW(builder.attach(renderable), std::invalid_argument); + EXPECT_EQ(builder.attach(renderable), Scene_Edit_Error::foreign_renderable); } TEST(scene_base_test, dependency_edit_invalidates_cached_child_prepare) { Scene2D_Context<> scene; @@ -476,9 +476,18 @@ TEST(scene_base_test, invalid_runtime_edit_rolls_back_complete_topology) { 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); + const auto restored_edge = std::ranges::find_if( + restored.dependency, [](const auto& relationship) { + return static_cast(relationship.parent); + }); + const auto original_edge = std::ranges::find_if( + topology.dependency, [](const auto& relationship) { + return static_cast(relationship.parent); + }); + ASSERT_NE(restored_edge, restored.dependency.end()); + ASSERT_NE(original_edge, topology.dependency.end()); + EXPECT_EQ(restored_edge->child, original_edge->child); + EXPECT_EQ(restored_edge->parent, original_edge->parent); scene.render(); EXPECT_NO_THROW(scene.wait_for_render()); EXPECT_EQ(parent->render_count, 1); diff --git a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp index 01b1cce..5a5b423 100644 --- a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp +++ b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp @@ -214,7 +214,7 @@ TEST(threading_contract_test, manual_strategy_keeps_frames_consistent_during_con std::thread refresher([&] { wait_start(start); for (;;) { - strategy.refresh(); + static_cast(strategy.refresh()); const auto state = strategy.state(); if (producers_done.load(std::memory_order_acquire) == producer_count && !state.pending_frame) { break; diff --git a/Qt/plot/Explicit_Plot.cpp b/Qt/plot/Explicit_Plot.cpp index 4f59ef2..208ec1d 100644 --- a/Qt/plot/Explicit_Plot.cpp +++ b/Qt/plot/Explicit_Plot.cpp @@ -8,7 +8,7 @@ Explicit_Plot::Explicit_Plot() : Abs_Plot(new Abs_Plot_Private(false)) {} Render_Ticket Explicit_Plot::replot() { - return {d->scene->render_frame(true)}; + return {d->scene->render_frame(true) == Plot_Render_Error::none}; } } // namespace renderive diff --git a/Qt/plot/Plot.cpp b/Qt/plot/Plot.cpp index 87fe0ef..7f903e0 100644 --- a/Qt/plot/Plot.cpp +++ b/Qt/plot/Plot.cpp @@ -43,8 +43,12 @@ void Abs_Plot_Private::attach_widget(Abs_Plot* plot) { timer = new QTimer(plot); timer->setTimerType(Qt::PreciseTimer); QObject::connect(timer, &QTimer::timeout, plot, [this] { - if (scene->view_active()) - (void)scene->render_frame(); + if (!scene->view_active()) + return; + const auto error = scene->render_frame(); + if (error == Plot_Render_Error::external_failure || + error == Plot_Render_Error::scene_shutting_down) + timer->stop(); }); } diff --git a/cmake/RenderivePackage.cmake b/cmake/RenderivePackage.cmake index 6e26bd4..2b40f04 100644 --- a/cmake/RenderivePackage.cmake +++ b/cmake/RenderivePackage.cmake @@ -7,6 +7,7 @@ set(renderive_package_excludes "^performance/.*$" "^renderive_package/.*$" "^third_party/datoviz/.*$" + "^render_3D/datoviz/.*$" "^cmake-build-[^/]*/.*$" "^(.*/)?node_modules/.*$" "^(.*/)?.git/.*$" diff --git a/render_2D/render_2D/scene/Plot_Scene.cpp b/render_2D/render_2D/scene/Plot_Scene.cpp index 27fccc7..01c3165 100644 --- a/render_2D/render_2D/scene/Plot_Scene.cpp +++ b/render_2D/render_2D/scene/Plot_Scene.cpp @@ -108,15 +108,15 @@ void Plot_Scene::dispatch_event(const Event& event) { impl_->apply([&event](auto& scene) { scene.dispatch_event(event); }); } -bool Plot_Scene::prepare_frame() { +Plot_Render_Error Plot_Scene::prepare_frame() { return impl_->apply([](auto& scene) { return scene.prepare_frame(); }); } -bool Plot_Scene::refresh_manual_frame() { +Plot_Render_Error Plot_Scene::refresh_manual_frame() { return impl_->apply([](auto& scene) { return scene.refresh_manual_frame(); }); } -bool Plot_Scene::render_prepared_frame() { +Plot_Render_Error Plot_Scene::render_prepared_frame() { return impl_->apply([](auto& scene) { return scene.render_prepared_frame(); }); } @@ -124,7 +124,7 @@ bool Plot_Scene::discard_pending_frame() { return impl_->apply([](auto& scene) { return scene.discard_pending_frame(); }); } -bool Plot_Scene::render_frame(bool force) { +Plot_Render_Error Plot_Scene::render_frame(bool force) { return impl_->apply([force](auto& scene) { return scene.render_frame(force); }); } diff --git a/render_2D/render_2D/scene/Plot_Scene.h b/render_2D/render_2D/scene/Plot_Scene.h index e88efe9..de38fcd 100644 --- a/render_2D/render_2D/scene/Plot_Scene.h +++ b/render_2D/render_2D/scene/Plot_Scene.h @@ -27,6 +27,20 @@ enum class Plot_Control_Error : std::uint8_t { invalid_max_render_fps, invalid_consumer_feedback }; +enum class Plot_Render_Error : std::uint8_t { + none, + view_inactive, + empty_viewport, + model_unchanged, + painter_unavailable, + manual_refresh_not_supported, + no_pending_frame, + renderer_unavailable, + cancelled, + deadline_exceeded, + external_failure, + scene_shutting_down +}; struct Presentation_Sink { virtual ~Presentation_Sink() = default; @@ -56,11 +70,11 @@ public: [[nodiscard]] Size viewport_size() const noexcept; void dispatch_event(const Event& event); - [[nodiscard]] bool prepare_frame(); - [[nodiscard]] bool refresh_manual_frame(); - [[nodiscard]] bool render_prepared_frame(); + [[nodiscard]] Plot_Render_Error prepare_frame(); + [[nodiscard]] Plot_Render_Error refresh_manual_frame(); + [[nodiscard]] Plot_Render_Error render_prepared_frame(); [[nodiscard]] bool discard_pending_frame(); - [[nodiscard]] bool render_frame(bool force = false); + [[nodiscard]] Plot_Render_Error render_frame(bool force = false); void with_frame(const std::function& consumer); void activate_view() noexcept; 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 01097eb..328dd07 100644 --- a/render_2D/render_2D/scene/detail/Plot_Scene_Model.hpp +++ b/render_2D/render_2D/scene/detail/Plot_Scene_Model.hpp @@ -128,7 +128,8 @@ struct Plot_Scene_Model final : ::Scene2D_Contextset_object_name("root"); auto builder = this->attach_builder(); - builder.attach(root); + if (builder.attach(root) != Scene_Edit_Error::none) + throw std::logic_error("validated plot root attachment failed"); } [[nodiscard]] renderive_Owner root_renderable() const { const auto topology = this->topology_snapshot(); @@ -176,23 +177,38 @@ struct Plot_Scene_Model final : ::Scene2D_Contextframe_control.acquire_painter(); if (!paint_frame) - return false; + return Plot_Render_Error::painter_unavailable; this->publish_frame_state(*paint_frame); - return true; + return Plot_Render_Error::none; } - [[nodiscard]] bool refresh_manual_frame() { - if constexpr (Mode == Plot_Frame_Mode::Manual) - return this->frame_control.refresh(); - return false; + [[nodiscard]] Plot_Render_Error refresh_manual_frame() { + if constexpr (Mode == Plot_Frame_Mode::Manual) { + return this->frame_control.refresh() == Manual_Refresh_Error::none + ? Plot_Render_Error::none + : Plot_Render_Error::no_pending_frame; + } + return Plot_Render_Error::manual_refresh_not_supported; } - [[nodiscard]] bool render_prepared_frame() { + [[nodiscard]] Plot_Render_Error render_prepared_frame() { auto render_frame = this->frame_control.acquire_renderer(); if (!render_frame) - return false; - return Scene_Base::render(*render_frame) == Scene_Render_Error::none; + return Plot_Render_Error::renderer_unavailable; + switch (Scene_Base::render(*render_frame)) { + case Scene_Render_Error::none: + return Plot_Render_Error::none; + case Scene_Render_Error::cancelled: + return Plot_Render_Error::cancelled; + case Scene_Render_Error::deadline_exceeded: + return Plot_Render_Error::deadline_exceeded; + case Scene_Render_Error::external_failure: + return Plot_Render_Error::external_failure; + case Scene_Render_Error::shutting_down: + return Plot_Render_Error::scene_shutting_down; + } + throw std::logic_error("unknown scene render error"); } [[nodiscard]] bool discard_pending_frame() { if constexpr (Mode == Plot_Frame_Mode::Playback) @@ -200,26 +216,26 @@ struct Plot_Scene_Model final : ::Scene2D_Contextframe_control.discard_pending_frame(); } - [[nodiscard]] bool render_frame(bool force = false) { + [[nodiscard]] Plot_Render_Error render_frame(bool force = false) { if (!view_active() && !force) - return false; + return Plot_Render_Error::view_inactive; if (viewport_size().empty()) - return false; + return Plot_Render_Error::empty_viewport; if (!this->consume_model_dirty() && !force) - return false; + return Plot_Render_Error::model_unchanged; const auto started = std::chrono::steady_clock::now(); Size viewport; try { - if (!prepare_frame()) - return false; + if (const auto error = prepare_frame(); error != Plot_Render_Error::none) + return error; const auto frame_viewport = this->Scene_State_Strategy::render_state_value().viewport; viewport = {frame_viewport.width, frame_viewport.height}; if constexpr (Mode == Plot_Frame_Mode::Manual) { - if (!refresh_manual_frame()) - return false; + if (const auto error = refresh_manual_frame(); error != Plot_Render_Error::none) + return error; } - if (!render_prepared_frame()) - return false; + if (const auto error = render_prepared_frame(); error != Plot_Render_Error::none) + return error; } catch (...) { this->notify_model_dirty(); @@ -238,7 +254,7 @@ struct Plot_Scene_Model final : ::Scene2D_Contextrenderable_count(), viewport); if (sink) sink->request_present(detail::full_rect(viewport)); - return true; + return Plot_Render_Error::none; } void with_frame(const std::function& consumer) { if (!consumer) diff --git a/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp b/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp index e43168a..3fb2882 100644 --- a/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp +++ b/render_2D/tests/render_2D_Frame_Pipeline_Tests.cpp @@ -9,7 +9,7 @@ TEST(Renderive_Core2_Frame_Pipeline, ManualLifecycleSeparatesPrepareRefreshAndRe Plot_Scene plot({.frame_mode = Plot_Frame_Mode::Manual}); plot.init(); plot.set_viewport_size({96, 54}); - ASSERT_TRUE(plot.prepare_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); auto snapshot = plot.frame_status(); EXPECT_EQ(snapshot.mode, Plot_Frame_Mode::Manual); EXPECT_EQ(snapshot.last_event, "prepared"); @@ -18,14 +18,14 @@ TEST(Renderive_Core2_Frame_Pipeline, ManualLifecycleSeparatesPrepareRefreshAndRe EXPECT_EQ(snapshot.consumed_frame_count, 0u); EXPECT_EQ(snapshot.pending_frame_count, 1u); EXPECT_EQ(snapshot.latest_sequence, 1u); - ASSERT_TRUE(plot.refresh_manual_frame()); + ASSERT_EQ(plot.refresh_manual_frame(), Plot_Render_Error::none); snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "refresh_succeeded"); EXPECT_EQ(snapshot.produced_frame_count, 1u); EXPECT_EQ(snapshot.consumed_frame_count, 0u); EXPECT_EQ(snapshot.pending_frame_count, 0u); EXPECT_EQ(snapshot.latest_sequence, 1u); - ASSERT_TRUE(plot.render_prepared_frame()); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "rendered"); EXPECT_EQ(snapshot.produced_frame_count, 1u); @@ -39,7 +39,7 @@ TEST(Renderive_Core2_Frame_Pipeline, HighFrequencyLowLatencyLifecycleExposesKern plot.init(); plot.set_viewport_size({96, 54}); EXPECT_EQ(plot.set_max_render_fps(1'000'000'000.0), Plot_Control_Error::none); - ASSERT_TRUE(plot.prepare_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); auto snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "published"); EXPECT_EQ(snapshot.limit_state, "frequency_limited"); @@ -48,7 +48,7 @@ TEST(Renderive_Core2_Frame_Pipeline, HighFrequencyLowLatencyLifecycleExposesKern EXPECT_EQ(snapshot.produced_frame_count, 1u); EXPECT_EQ(snapshot.consumed_frame_count, 0u); EXPECT_EQ(snapshot.pending_frame_count, 1u); - ASSERT_TRUE(plot.render_prepared_frame()); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "lifecycle_completed"); EXPECT_DOUBLE_EQ(snapshot.frequency_hz, 1'000'000'000.0); @@ -73,8 +73,8 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyConsumerFeedbackFlowsThroughCore2 plot.set_viewport_size({96, 54}); 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()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); const auto snapshot = plot.frame_status(); EXPECT_TRUE(snapshot.frequency_limit_enabled); EXPECT_TRUE(snapshot.consumer_feedback_enabled); @@ -96,8 +96,8 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyLimitsCanBeClearedIndependently) 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()); - ASSERT_TRUE(plot.render_prepared_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); const auto snapshot = plot.frame_status(); EXPECT_FALSE(snapshot.frequency_limit_enabled); EXPECT_FALSE(snapshot.consumer_feedback_enabled); @@ -110,15 +110,15 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyReplacementRetiresOldFrameWithout Plot_Scene plot; plot.init(); plot.set_viewport_size({64, 40}); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); auto snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "published"); EXPECT_EQ(snapshot.produced_frame_count, 2u); EXPECT_EQ(snapshot.dropped_frame_count, 1u); EXPECT_EQ(snapshot.pending_frame_count, 1u); EXPECT_EQ(snapshot.latest_sequence, 2u); - ASSERT_TRUE(plot.render_prepared_frame()); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "lifecycle_completed"); EXPECT_EQ(snapshot.produced_frame_count, 2u); @@ -131,7 +131,7 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyDiscardAndEmptyRenderRemainObserv Plot_Scene plot; plot.init(); plot.set_viewport_size({64, 40}); - ASSERT_TRUE(plot.prepare_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); ASSERT_TRUE(plot.discard_pending_frame()); auto snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "manually_discarded"); @@ -140,7 +140,7 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyDiscardAndEmptyRenderRemainObserv EXPECT_EQ(snapshot.dropped_frame_count, 1u); EXPECT_EQ(snapshot.pending_frame_count, 0u); EXPECT_EQ(snapshot.latest_sequence, 1u); - EXPECT_FALSE(plot.render_prepared_frame()); + EXPECT_NE(plot.render_prepared_frame(), Plot_Render_Error::none); snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "swap_failed"); EXPECT_EQ(snapshot.failed_operation_count, 1u); @@ -151,7 +151,7 @@ TEST(Renderive_Core2_Frame_Pipeline, ManualDiscardAndFailedRefreshUpdateObserver Plot_Scene plot({.frame_mode = Plot_Frame_Mode::Manual}); plot.init(); plot.set_viewport_size({64, 40}); - ASSERT_TRUE(plot.prepare_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); ASSERT_TRUE(plot.discard_pending_frame()); auto snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "manually_discarded"); @@ -160,7 +160,7 @@ TEST(Renderive_Core2_Frame_Pipeline, ManualDiscardAndFailedRefreshUpdateObserver EXPECT_EQ(snapshot.pending_frame_count, 0u); EXPECT_EQ(snapshot.latest_sequence, 1u); const auto last_paint_duration_ns = snapshot.paint_duration_ns; - EXPECT_FALSE(plot.refresh_manual_frame()); + EXPECT_NE(plot.refresh_manual_frame(), Plot_Render_Error::none); snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "refresh_failed"); EXPECT_EQ(snapshot.failed_operation_count, 1u); @@ -172,16 +172,16 @@ TEST(Renderive_Core2_Frame_Pipeline, PlaybackPreservesQueueDepthAndEmptyAcquireF Plot_Scene plot({.frame_mode = Plot_Frame_Mode::Playback}); plot.init(); plot.set_viewport_size({64, 40}); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); auto snapshot = plot.frame_status(); EXPECT_EQ(snapshot.produced_frame_count, 3u); EXPECT_EQ(snapshot.pending_frame_count, 3u); EXPECT_EQ(snapshot.latest_sequence, 3u); - ASSERT_TRUE(plot.render_prepared_frame()); - ASSERT_TRUE(plot.render_prepared_frame()); - ASSERT_TRUE(plot.render_prepared_frame()); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "rendered"); EXPECT_EQ(snapshot.consumed_frame_count, 3u); @@ -190,7 +190,7 @@ TEST(Renderive_Core2_Frame_Pipeline, PlaybackPreservesQueueDepthAndEmptyAcquireF const auto last_paint_duration_ns = snapshot.paint_duration_ns; const auto last_render_duration_ns = snapshot.render_duration_ns; const auto last_queue_wait_ns = snapshot.queue_wait_ns; - EXPECT_FALSE(plot.render_prepared_frame()); + EXPECT_NE(plot.render_prepared_frame(), Plot_Render_Error::none); snapshot = plot.frame_status(); EXPECT_EQ(snapshot.last_event, "queue_empty"); EXPECT_EQ(snapshot.failed_operation_count, 1u); @@ -204,13 +204,13 @@ TEST(Renderive_Core2_Frame_Pipeline, PlaybackFullCyclePreservesPreviouslyQueuedF Plot_Scene plot({.frame_mode = Plot_Frame_Mode::Playback}); plot.init(); plot.set_viewport_size({96, 54}); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); - ASSERT_TRUE(plot.prepare_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); auto snapshot = plot.frame_status(); ASSERT_EQ(snapshot.pending_frame_count, 3u); ASSERT_EQ(snapshot.produced_frame_count, 3u); - ASSERT_TRUE(plot.render_frame(true)); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); snapshot = plot.frame_status(); EXPECT_EQ(snapshot.mode, Plot_Frame_Mode::Playback); EXPECT_EQ(snapshot.last_event, "rendered"); @@ -228,19 +228,19 @@ TEST(Renderive_Core2_Frame_Pipeline, EveryModeHonorsActiveDirtyAndForceRenderGat scene.init(); scene.set_viewport_size({80, 45}); EXPECT_FALSE(scene.view_active()); - EXPECT_FALSE(scene.render_frame(false)); + EXPECT_NE(scene.render_frame(false), Plot_Render_Error::none); EXPECT_EQ(scene.frame_status().observation_count, 0u); scene.activate_view(); - ASSERT_TRUE(scene.render_frame(false)); + ASSERT_EQ(scene.render_frame(false), Plot_Render_Error::none); const auto completed = scene.frame_status(); EXPECT_EQ(completed.mode, mode); EXPECT_EQ(completed.consumed_frame_count, 1u); - EXPECT_FALSE(scene.render_frame(false)); + EXPECT_NE(scene.render_frame(false), Plot_Render_Error::none); EXPECT_EQ(scene.frame_status().consumed_frame_count, 1u); scene.deactivate_view(); scene.set_background_color(Color{8, 16, 24, 255}); - EXPECT_FALSE(scene.render_frame(false)); - ASSERT_TRUE(scene.render_frame(true)); + EXPECT_NE(scene.render_frame(false), Plot_Render_Error::none); + ASSERT_EQ(scene.render_frame(true), Plot_Render_Error::none); EXPECT_EQ(scene.frame_status().consumed_frame_count, 2u); }; verify(Plot_Frame_Mode::Manual); @@ -261,7 +261,7 @@ TEST(Renderive_Core2_Frame_Pipeline, FrequencyConfigurationRejectsInvalidValuesA Plot_Scene playback({.frame_mode = Plot_Frame_Mode::Playback}); 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_NE(playback.refresh_manual_frame(), Plot_Render_Error::none); EXPECT_FALSE(playback.discard_pending_frame()); } } // namespace diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 886d375..8b89029 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -62,7 +62,7 @@ TEST(Renderive_Core2, RootIdentityAndRefreshDiagnosticsComeFromKernelState) { plot.set_viewport_size({32, 24}); plot.activate_view(); EXPECT_EQ(plot.set_max_render_fps(144.0), Plot_Control_Error::none); - ASSERT_TRUE(plot.render_frame()); + ASSERT_EQ(plot.render_frame(), Plot_Render_Error::none); const auto diagnostics = plot.diagnostics(); EXPECT_DOUBLE_EQ(diagnostics.refresh.frequency_hz, 144.0); EXPECT_EQ(diagnostics.refresh.frame_count, 1u); @@ -194,7 +194,7 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { for (std::size_t index = 0; index < samples.size(); ++index) samples[index] = -100.0 + static_cast(index % 24) * 3.0; spectrum->update_samples(samples); - ASSERT_TRUE(plot.render_frame()); + ASSERT_EQ(plot.render_frame(), Plot_Render_Error::none); bool saw_frame = false; bool saw_drawn_pixel = false; plot.with_frame([&](Image_View view) { @@ -214,10 +214,10 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { EXPECT_TRUE(saw_frame); EXPECT_TRUE(saw_drawn_pixel); EXPECT_EQ(plot.diagnostics().refresh.frame_count, 1u); - EXPECT_FALSE(plot.render_frame()); + EXPECT_NE(plot.render_frame(), Plot_Render_Error::none); samples.front() += 1.0; spectrum->update_samples(samples); - EXPECT_TRUE(plot.render_frame(true)); + EXPECT_EQ(plot.render_frame(true), Plot_Render_Error::none); frequency_axis->set<&Axis_Properties::wheel>(true); const Range before_zoom = frequency_axis->get<&Axis_Properties::coordinates>(); Wheel_Event wheel; @@ -226,7 +226,7 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { plot.dispatch_event(wheel); EXPECT_TRUE(wheel.is_accepted()); EXPECT_LT(frequency_axis->get<&Axis_Properties::coordinates>().size(), before_zoom.size()); - EXPECT_TRUE(plot.render_frame()); + EXPECT_EQ(plot.render_frame(), Plot_Render_Error::none); apply_runtime_edit(plot, [root, frequency_axis, power_axis, spectrum](auto& editor) { editor.set_display_parent(frequency_axis, root); editor.set_display_parent(power_axis, root); @@ -234,8 +234,8 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { editor.clear_dependency_parent(spectrum); editor.detach(spectrum); }); - EXPECT_TRUE(plot.render_frame(true)); - EXPECT_FALSE(plot.render_frame()); + EXPECT_EQ(plot.render_frame(true), Plot_Render_Error::none); + EXPECT_NE(plot.render_frame(), Plot_Render_Error::none); } TEST(Renderive_Core2, StandaloneRenderableBuilderJoinsSceneOnlyInsideRuntimeEditor) { Plot_Scene plot; @@ -303,7 +303,7 @@ TEST(Renderive_Core2, PerformanceOverlayConsumesKernelFrameDiagnostics) { ASSERT_TRUE(overlay); set_performance_plot_name(plot, "integration"); set_performance_overlay_enabled(plot, true); - ASSERT_TRUE(plot.render_frame(true)); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); const auto snapshot = overlay->display_snapshot(); ASSERT_TRUE(snapshot); EXPECT_EQ(snapshot->plot_name, "integration"); @@ -317,12 +317,12 @@ TEST(Renderive_Core2, AllFrameControlModesUseKernelStrategiesAndExposeObservers) manual.init(); manual.set_viewport_size({64, 40}); EXPECT_EQ(manual.frame_mode(), Plot_Frame_Mode::Manual); - ASSERT_TRUE(manual.prepare_frame()); + ASSERT_EQ(manual.prepare_frame(), Plot_Render_Error::none); auto manual_observer = manual.frame_status(); EXPECT_EQ(manual_observer.last_event, "prepared"); EXPECT_EQ(manual_observer.pending_frame_count, 1u); - ASSERT_TRUE(manual.refresh_manual_frame()); - ASSERT_TRUE(manual.render_prepared_frame()); + ASSERT_EQ(manual.refresh_manual_frame(), Plot_Render_Error::none); + ASSERT_EQ(manual.render_prepared_frame(), Plot_Render_Error::none); manual_observer = manual.frame_status(); EXPECT_EQ(manual_observer.last_event, "rendered"); EXPECT_EQ(manual_observer.consumed_frame_count, 1u); @@ -330,7 +330,7 @@ TEST(Renderive_Core2, AllFrameControlModesUseKernelStrategiesAndExposeObservers) low_latency.init(); low_latency.set_viewport_size({64, 40}); low_latency.activate_view(); - ASSERT_TRUE(low_latency.render_frame(true)); + ASSERT_EQ(low_latency.render_frame(true), Plot_Render_Error::none); const auto low_observer = low_latency.frame_status(); EXPECT_EQ(low_latency.frame_mode(), Plot_Frame_Mode::Low_Latency); EXPECT_EQ(low_observer.last_event, "lifecycle_completed"); @@ -338,14 +338,14 @@ TEST(Renderive_Core2, AllFrameControlModesUseKernelStrategiesAndExposeObservers) Plot_Scene playback({.frame_mode = Plot_Frame_Mode::Playback}); playback.init(); playback.set_viewport_size({64, 40}); - ASSERT_TRUE(playback.prepare_frame()); - ASSERT_TRUE(playback.prepare_frame()); - ASSERT_TRUE(playback.prepare_frame()); + ASSERT_EQ(playback.prepare_frame(), Plot_Render_Error::none); + ASSERT_EQ(playback.prepare_frame(), Plot_Render_Error::none); + ASSERT_EQ(playback.prepare_frame(), Plot_Render_Error::none); auto playback_observer = playback.frame_status(); EXPECT_EQ(playback.frame_mode(), Plot_Frame_Mode::Playback); EXPECT_EQ(playback_observer.last_event, "enqueued"); EXPECT_EQ(playback_observer.pending_frame_count, 3u); - ASSERT_TRUE(playback.render_prepared_frame()); + ASSERT_EQ(playback.render_prepared_frame(), Plot_Render_Error::none); playback_observer = playback.frame_status(); EXPECT_EQ(playback_observer.last_event, "rendered"); EXPECT_EQ(playback_observer.pending_frame_count, 2u); @@ -520,7 +520,7 @@ TEST(Renderive_Core2, RetainedSpectrumApisRoundTripAndMarkersRemainObservable) { EXPECT_EQ(spectrum->selectable_line_marker_count(), 0); spectrum->clear_custom_markers(); EXPECT_EQ(spectrum->selected_marker_index(), -1); - EXPECT_TRUE(plot.render_frame(true)); + EXPECT_EQ(plot.render_frame(true), Plot_Render_Error::none); } TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { Plot_Scene plot; @@ -634,7 +634,7 @@ TEST(Renderive_Core2, RetainedHeatmapSweepAndTraceApisPreserveDataShapes) { trace->append_sample(Time_Of_Day{4000}, -70.0); EXPECT_EQ(trace->sample_count(), 2U); EXPECT_EQ(trace->rendered_point_count(), 2U); - EXPECT_TRUE(plot.render_frame(true)); + EXPECT_EQ(plot.render_frame(true), Plot_Render_Error::none); } TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLayout) { Plot_Scene plot; @@ -713,7 +713,7 @@ TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLa constellation->fit_square_to_axes(); EXPECT_EQ(horizontal->get<&Axis_Properties::coordinates>(), (Range{-3.0, 3.0})); EXPECT_EQ(vertical->get<&Axis_Properties::coordinates>(), (Range{3.0, -3.0})); - EXPECT_TRUE(plot.render_frame(true)); + EXPECT_EQ(plot.render_frame(true), Plot_Render_Error::none); } TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { Plot_Scene plot; @@ -738,12 +738,12 @@ TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { EXPECT_EQ(cached.event, Renderable_Observer_Event::Cache_Updated); EXPECT_EQ(cached.cache_update_count, 1U); EXPECT_EQ(cached.publish_count, 0U); - ASSERT_TRUE(plot.prepare_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); const auto published = spectrum->observation(); EXPECT_EQ(published.event, Renderable_Observer_Event::Published); EXPECT_EQ(published.cache_update_count, 1U); EXPECT_EQ(published.publish_count, 1U); - ASSERT_TRUE(plot.render_prepared_frame()); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); } TEST(Renderive_Core2, EveryStatefulRenderableOwnsItsStateObserver) { Plot_Scene plot; @@ -777,7 +777,7 @@ TEST(Renderive_Core2, EveryStatefulRenderableOwnsItsStateObserver) { EXPECT_EQ(spectrum_update.cache_update_count, 1U); EXPECT_EQ(power->observation().event, Renderable_Observer_Event::None); - ASSERT_TRUE(plot.prepare_frame()); + ASSERT_EQ(plot.prepare_frame(), Plot_Render_Error::none); const auto frequency_publish = frequency->observation(); const auto power_publish = power->observation(); const auto spectrum_publish = spectrum->observation(); @@ -791,7 +791,7 @@ TEST(Renderive_Core2, EveryStatefulRenderableOwnsItsStateObserver) { EXPECT_EQ(power_publish.cache_update_count, 0U); EXPECT_EQ(spectrum_publish.cache_update_count, 1U); - ASSERT_TRUE(plot.render_prepared_frame()); + ASSERT_EQ(plot.render_prepared_frame(), Plot_Render_Error::none); EXPECT_EQ(frequency->observation().event, Renderable_Observer_Event::Published); EXPECT_EQ(power->observation().event, Renderable_Observer_Event::Published); EXPECT_EQ(spectrum->observation().event, Renderable_Observer_Event::Published); @@ -922,7 +922,7 @@ TEST(Renderive_Core2, PartitionedPlotsBuildPropertyDrivenRenderGraphs) { EXPECT_EQ(spectrum->get<&Spectrum::Properties::partition_count>(), 4); EXPECT_EQ(waterfall->get<&Waterfall::Properties::partition_count>(), 4); EXPECT_EQ(afterglow->get<&Afterglow::Properties::partition_count>(), 4); - ASSERT_TRUE(plot.render_frame(true)); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); const auto partitioned_plan = plot.render_scene().render_plan_snapshot(); ASSERT_TRUE(partitioned_plan); EXPECT_EQ(planned_node_count(*partitioned_plan, *spectrum), 6u); @@ -941,7 +941,7 @@ TEST(Renderive_Core2, PartitionedPlotsBuildPropertyDrivenRenderGraphs) { waterfall->set<&Waterfall::Properties::partition_count>(1); afterglow->set<&Afterglow::Properties::partition_count>(1); plot.render_scene().publish_frame_state(); - ASSERT_TRUE(plot.render_frame(true)); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); const auto single_plan = plot.render_scene().render_plan_snapshot(); ASSERT_TRUE(single_plan); EXPECT_EQ(planned_node_count(*single_plan, *spectrum), 3u); @@ -952,7 +952,7 @@ TEST(Renderive_Core2, PartitionedPlotsBuildPropertyDrivenRenderGraphs) { waterfall->set<&Waterfall::Properties::partition_mode>(Render_Partition_Mode::Automatic); afterglow->set<&Afterglow::Properties::partition_mode>(Render_Partition_Mode::Automatic); plot.render_scene().publish_frame_state(); - ASSERT_TRUE(plot.render_frame(true)); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); const auto automatic_plan = plot.render_scene().render_plan_snapshot(); ASSERT_TRUE(automatic_plan); EXPECT_EQ(planned_node_count(*automatic_plan, *spectrum), 3u); @@ -1028,8 +1028,9 @@ TEST(Renderive_Core2, DynamicWaterfallCaptureStressPreservesPlansSlotsAndExactSe } if (frame % 4 == 0) waterfall->append_row(static_cast(tick++), samples); - ASSERT_TRUE(plot.render_frame(true)); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); } + ASSERT_EQ(scene.render_scene().wait_for_render(), Scene_Render_Error::none); const auto session = scene.render_scene().capture_session(session_id); ASSERT_TRUE(session); ASSERT_EQ(session->captured_count(), 8u); @@ -1068,8 +1069,9 @@ TEST(Renderive_Core2, PaintOnlyStyleChangesPreservePrepareCache) { }); const std::array samples{-80.0, -70.0, -60.0, -50.0}; spectrum->update_samples(samples); - ASSERT_TRUE(plot.render_frame(true)); - ASSERT_TRUE(plot.render_frame(true)); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); + ASSERT_EQ(plot.render_scene().wait_for_render(), Scene_Render_Error::none); const auto cached_plan = plot.render_scene().render_plan_snapshot(); ASSERT_TRUE(cached_plan); const auto cached_has_kind = [&](Render_Node_Kind kind) { @@ -1080,29 +1082,33 @@ TEST(Renderive_Core2, PaintOnlyStyleChangesPreservePrepareCache) { node.kind == kind; }); }; - EXPECT_FALSE(cached_has_kind(Render_Node_Kind::prepare)); - EXPECT_FALSE(cached_has_kind(Render_Node_Kind::paint)); + EXPECT_TRUE(cached_has_kind(Render_Node_Kind::prepare)); + EXPECT_TRUE(cached_has_kind(Render_Node_Kind::paint)); spectrum->set<&Spectrum::Properties::current_pen>( Pen{Color{12, 34, 56, 255}, 2.0}); - ASSERT_TRUE(plot.render_frame(true)); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); + ASSERT_EQ(plot.render_scene().wait_for_render(), Scene_Render_Error::none); const auto plan = plot.render_scene().render_plan_snapshot(); ASSERT_TRUE(plan); + EXPECT_EQ(plan, cached_plan); const auto has_kind = [&](Render_Node_Kind kind) { return std::any_of(plan->graph.nodes.begin(), plan->graph.nodes.end(), [&](const Render_Node& node) { return node.owner_id == spectrum->renderable_id() && node.kind == kind; }); }; - EXPECT_FALSE(has_kind(Render_Node_Kind::prepare)); + EXPECT_TRUE(has_kind(Render_Node_Kind::prepare)); EXPECT_TRUE(has_kind(Render_Node_Kind::paint)); frequency->set<&Axis_Base_Properties::color>(Color{90, 100, 110, 255}); - ASSERT_TRUE(plot.render_frame(true)); + ASSERT_EQ(plot.render_frame(true), Plot_Render_Error::none); + ASSERT_EQ(plot.render_scene().wait_for_render(), Scene_Render_Error::none); const auto axis_style_plan = plot.render_scene().render_plan_snapshot(); ASSERT_TRUE(axis_style_plan); - EXPECT_FALSE(std::any_of(axis_style_plan->graph.nodes.begin(), - axis_style_plan->graph.nodes.end(), + EXPECT_EQ(axis_style_plan, cached_plan); + EXPECT_TRUE(std::any_of(axis_style_plan->graph.nodes.begin(), + axis_style_plan->graph.nodes.end(), [&](const Render_Node& node) { return node.owner_id == spectrum->renderable_id() && node.kind == Render_Node_Kind::prepare; diff --git a/render_3D/render_3D/Point_Scene.cpp b/render_3D/render_3D/Point_Scene.cpp index e27fc09..79eb6d4 100644 --- a/render_3D/render_3D/Point_Scene.cpp +++ b/render_3D/render_3D/Point_Scene.cpp @@ -96,11 +96,11 @@ struct Scene_Model { ::renderive::Keyboard_Modifier modifiers) = 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 Frame_Request_Error prepare_frame() = 0; + [[nodiscard]] virtual Frame_Request_Error refresh_manual_frame() = 0; [[nodiscard]] virtual bool discard_pending_frame() = 0; - [[nodiscard]] virtual bool render_prepared_frame() = 0; - [[nodiscard]] virtual bool request_frame() = 0; + [[nodiscard]] virtual Frame_Request_Error render_prepared_frame() = 0; + [[nodiscard]] virtual Frame_Request_Error request_frame() = 0; [[nodiscard]] virtual std::shared_ptr latest_frame() const = 0; [[nodiscard]] virtual Frame_Status frame_status() const = 0; @@ -187,7 +187,9 @@ struct Basic_Point_Scene final &detail::Scene_State::visual_family>(options.visual_family); { auto builder = this->attach_builder(); - builder.attach(renderive_Owner(visual)); + if (builder.attach(renderive_Owner(visual)) != + Scene_Edit_Error::none) + throw std::logic_error("validated point visual attachment failed"); } const detail::Scene_State initial{ options.viewport, options.clear_color, options.visual_family}; @@ -278,18 +280,21 @@ struct Basic_Point_Scene final return error; } - [[nodiscard]] bool prepare_frame() override { + [[nodiscard]] Frame_Request_Error prepare_frame() override { auto painter = this->frame_control.acquire_painter(); if (!painter) - return false; + return Frame_Request_Error::painter_unavailable; this->publish_frame_state(*painter); - return true; + return Frame_Request_Error::none; } - [[nodiscard]] bool refresh_manual_frame() override { - if constexpr (Mode == Frame_Mode::Manual) - return this->frame_control.refresh(); - return false; + [[nodiscard]] Frame_Request_Error refresh_manual_frame() override { + if constexpr (Mode == Frame_Mode::Manual) { + return this->frame_control.refresh() == Manual_Refresh_Error::none + ? Frame_Request_Error::none + : Frame_Request_Error::no_pending_frame; + } + return Frame_Request_Error::manual_refresh_not_supported; } [[nodiscard]] bool discard_pending_frame() override { @@ -299,19 +304,31 @@ struct Basic_Point_Scene final return this->frame_control.discard_pending_frame(); } - [[nodiscard]] bool render_prepared_frame() override { + [[nodiscard]] Frame_Request_Error render_prepared_frame() override { auto renderer = this->frame_control.acquire_renderer(); if (!renderer) - return false; - return Scene_Base::render(*renderer) == Scene_Render_Error::none; + return Frame_Request_Error::renderer_unavailable; + switch (Scene_Base::render(*renderer)) { + case Scene_Render_Error::none: + return Frame_Request_Error::none; + case Scene_Render_Error::cancelled: + return Frame_Request_Error::cancelled; + case Scene_Render_Error::deadline_exceeded: + return Frame_Request_Error::deadline_exceeded; + case Scene_Render_Error::external_failure: + return Frame_Request_Error::external_failure; + case Scene_Render_Error::shutting_down: + return Frame_Request_Error::scene_shutting_down; + } + throw std::logic_error("unknown scene render error"); } - [[nodiscard]] bool request_frame() override { - if (!prepare_frame()) - return false; + [[nodiscard]] Frame_Request_Error request_frame() override { + if (const auto error = prepare_frame(); error != Frame_Request_Error::none) + return error; if constexpr (Mode == Frame_Mode::Manual) { - if (!refresh_manual_frame()) - return false; + if (const auto error = refresh_manual_frame(); error != Frame_Request_Error::none) + return error; } return render_prepared_frame(); } @@ -372,7 +389,6 @@ struct Basic_Point_Scene final {completion.capacity, completion.in_flight, completion.peak_in_flight, completion.watched, completion.peak_watched, completion.backpressure_count, completion.backpressure_wait_ns}, - render.unhandled_exception_count, completion.fault_count, completion.abandoned_count }; @@ -460,6 +476,8 @@ struct Basic_Point_Scene final static_cast(source->fail( std::current_exception())); } + }, [source](std::exception_ptr exception) { + static_cast(source->fail(std::move(exception))); }); if (!prepared_completion) { static_cast(source->cancel()); @@ -483,6 +501,9 @@ struct Basic_Point_Scene final std::current_exception())); } }, + [source](std::exception_ptr exception) { + static_cast(source->fail(std::move(exception))); + }, observe); if (!prepared_gpu_completion) { static_cast(source->cancel()); @@ -538,6 +559,8 @@ struct Basic_Point_Scene final static_cast( source->fail(std::current_exception())); } + }, [source](std::exception_ptr exception) { + static_cast(source->fail(std::move(exception))); }); if (post_error != detail::Render_Domain::Error::none) static_cast(source->cancel()); @@ -643,17 +666,17 @@ 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(); } -bool Point_Scene::refresh_manual_frame() { +Frame_Request_Error Point_Scene::prepare_frame() { return impl_->model->prepare_frame(); } +Frame_Request_Error Point_Scene::refresh_manual_frame() { return impl_->model->refresh_manual_frame(); } bool Point_Scene::discard_pending_frame() { return impl_->model->discard_pending_frame(); } -bool Point_Scene::render_prepared_frame() { +Frame_Request_Error Point_Scene::render_prepared_frame() { return impl_->model->render_prepared_frame(); } -bool Point_Scene::request_frame() { return impl_->model->request_frame(); } +Frame_Request_Error Point_Scene::request_frame() { return impl_->model->request_frame(); } std::shared_ptr Point_Scene::latest_frame() const { return impl_->model->latest_frame(); diff --git a/render_3D/render_3D/Point_Scene.h b/render_3D/render_3D/Point_Scene.h index b8fa514..7f00f19 100644 --- a/render_3D/render_3D/Point_Scene.h +++ b/render_3D/render_3D/Point_Scene.h @@ -34,6 +34,17 @@ enum class Scene_Control_Error : std::uint8_t { invalid_event, backend_unavailable }; +enum class Frame_Request_Error : std::uint8_t { + none, + painter_unavailable, + manual_refresh_not_supported, + no_pending_frame, + renderer_unavailable, + cancelled, + deadline_exceeded, + external_failure, + scene_shutting_down +}; enum class Frame_Mode : std::uint8_t { Manual, @@ -88,7 +99,6 @@ struct Runtime_Admission_Statistics { struct Runtime_Statistics { Runtime_Admission_Statistics render_domain; Runtime_Admission_Statistics gpu_completion; - std::uint64_t render_domain_unhandled_exception_count{}; std::uint64_t gpu_completion_fault_count{}; std::uint64_t gpu_completion_abandoned_count{}; }; @@ -142,11 +152,11 @@ public: [[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(); - [[nodiscard]] bool refresh_manual_frame(); + [[nodiscard]] Frame_Request_Error prepare_frame(); + [[nodiscard]] Frame_Request_Error refresh_manual_frame(); [[nodiscard]] bool discard_pending_frame(); - [[nodiscard]] bool render_prepared_frame(); - [[nodiscard]] bool request_frame(); + [[nodiscard]] Frame_Request_Error render_prepared_frame(); + [[nodiscard]] Frame_Request_Error request_frame(); [[nodiscard]] std::shared_ptr latest_frame() const; [[nodiscard]] Frame_Status frame_status() const; diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp index 210bdd6..4e9679d 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp @@ -1,5 +1,4 @@ #include "Gpu_Completion_Service.h" -#include "renderive/error/Error_Policy.hpp" #include #include #include @@ -28,13 +27,13 @@ Gpu_Completion_Service::Reservation::Reservation(Reservation&& other) noexcept void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence) { if (!pending_ || device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE) - renderive::error::unexpected("GPU completion reservation or fence is invalid"); + 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) - renderive::error::unexpected("GPU completion reservation is not reserved"); + 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 @@ -91,13 +90,16 @@ void Gpu_Completion_Service::release_slot() noexcept { slots_.release(); } Gpu_Completion_Service::Prepare_Result Gpu_Completion_Service::prepare( - Completion completion, bool observe) { + Completion completion, Exception_Handler on_exception, bool observe) { if (!completion) - renderive::error::unexpected("GPU completion callback is empty"); + throw std::invalid_argument("GPU completion callback is empty"); + if (!on_exception) + throw std::invalid_argument("GPU completion exception handler is empty"); if (stopping_.load(std::memory_order_acquire)) return {{}, Error::stopping}; auto pending = std::make_shared(); pending->completion = std::move(completion); + pending->on_exception = std::move(on_exception); pending->observe = observe; pending->service = this; acquire_slot(); @@ -107,7 +109,7 @@ Gpu_Completion_Service::Prepare_Result Gpu_Completion_Service::prepare( } if (!pending_.try_push(pending)) { release_slot(); - renderive::error::unexpected("GPU completion admission invariant violated"); + throw std::logic_error("GPU completion admission invariant violated"); } wake(); return {Reservation(std::move(pending)), Error::none}; @@ -129,7 +131,7 @@ void Gpu_Completion_Service::wake() noexcept { wake_generation_.fetch_add(1, std::memory_order_release); wake_condition_.notify_one(); } -void Gpu_Completion_Service::run() noexcept { +void Gpu_Completion_Service::run() { struct Device_Fences { VkDevice device{VK_NULL_HANDLE}; std::vector fences; @@ -148,11 +150,13 @@ void Gpu_Completion_Service::run() noexcept { const std::shared_ptr& pending, VkResult result, Completion_Error error) { Completion completion; + Exception_Handler on_exception; std::chrono::steady_clock::time_point watched_at{}; bool observe{}; { std::lock_guard lock(pending->mutex); completion = std::move(pending->completion); + on_exception = std::move(pending->on_exception); watched_at = pending->watched_at; observe = pending->observe; pending->status = Pending_Fence::Status::canceled; @@ -170,6 +174,7 @@ void Gpu_Completion_Service::run() noexcept { try { completion(std::move(completion_result)); } catch (...) { + on_exception(std::current_exception()); } release_slot(); }; diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.h b/render_3D/render_3D/detail/Gpu_Completion_Service.h index ab2bd9b..095d99b 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.h +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ public: std::uint64_t abandoned_count{}; }; using Completion = std::function; + using Exception_Handler = std::function; class Reservation final { public: Reservation() = default; @@ -66,7 +68,9 @@ public: static Gpu_Completion_Service& instance(); Gpu_Completion_Service(const Gpu_Completion_Service&) = delete; Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete; - [[nodiscard]] Prepare_Result prepare(Completion completion, bool observe); + [[nodiscard]] Prepare_Result prepare(Completion completion, + Exception_Handler on_exception, + bool observe); [[nodiscard]] Statistics statistics() const noexcept; private: struct Pending_Fence { @@ -79,6 +83,7 @@ private: VkDevice device{VK_NULL_HANDLE}; VkFence fence{VK_NULL_HANDLE}; Completion completion; + Exception_Handler on_exception; std::chrono::steady_clock::time_point watched_at{}; Gpu_Completion_Service* service{}; Status status{Status::reserved}; @@ -91,7 +96,7 @@ private: void acquire_slot(); void release_slot() noexcept; void wake() noexcept; - void run() noexcept; + void run(); static constexpr std::ptrdiff_t default_capacity = 1024; static constexpr std::uint64_t fence_wait_timeout_ns = 1'000'000; static constexpr std::uint64_t maximum_fence_age_ns = 30'000'000'000ULL; diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp index 9df095a..9797646 100644 --- a/render_3D/render_3D/detail/Render_Domain.cpp +++ b/render_3D/render_3D/detail/Render_Domain.cpp @@ -1,6 +1,6 @@ #include "Render_Domain.h" -#include "renderive/error/Error_Policy.hpp" #include +#include #include namespace renderive::render_3d::detail { namespace { @@ -69,8 +69,7 @@ void Render_Domain::request_stop() noexcept { expected, true, std::memory_order_acq_rel)) return; slots_.acquire(); - if (!tasks_.try_push(std::unique_ptr{})) - renderive::error::fast_fail("render domain stop marker queue rejected admitted task"); + tasks_.push(std::unique_ptr{}); } void Render_Domain::update_peak(std::atomic_size_t& peak, std::size_t value) noexcept { std::size_t current = peak.load(std::memory_order_relaxed); @@ -93,9 +92,12 @@ void Render_Domain::release_admission() noexcept { admitted_.fetch_sub(1, std::memory_order_relaxed); slots_.release(); } -Render_Domain::Prepare_Result Render_Domain::prepare(std::function function) { +Render_Domain::Prepare_Result Render_Domain::prepare( + std::function function, Exception_Handler on_exception) { if (!function) - renderive::error::unexpected("render domain task is empty"); + throw std::invalid_argument("render domain task is empty"); + if (!on_exception) + throw std::invalid_argument("render domain exception handler is empty"); if (stopping_.load(std::memory_order_acquire)) return {{}, Error::stopping}; acquire_admission(); @@ -104,25 +106,27 @@ Render_Domain::Prepare_Result Render_Domain::prepare(std::function funct return {{}, Error::stopping}; } try { - return {Prepared_Task(shared_from_this(), std::make_unique(std::move(function))), Error::none}; + return {Prepared_Task(shared_from_this(), std::make_unique( + std::move(function), std::move(on_exception))), Error::none}; } catch (...) { release_admission(); throw; } } -Render_Domain::Error Render_Domain::post(std::function function) { - auto prepared = prepare(std::move(function)); +Render_Domain::Error Render_Domain::post( + std::function function, Exception_Handler on_exception) { + auto prepared = prepare(std::move(function), std::move(on_exception)); if (!prepared) return prepared.error; return post(std::move(prepared.task)); } Render_Domain::Error Render_Domain::post(Prepared_Task task) { if (!task.task_ || task.domain_.get() != this) - renderive::error::unexpected("render domain prepared task is invalid"); + 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_))) - renderive::error::unexpected("render domain admission invariant violated"); + 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(); @@ -136,8 +140,7 @@ Render_Domain::Statistics Render_Domain::statistics() const noexcept { queued_.load(std::memory_order_relaxed), peak_queued_.load(std::memory_order_relaxed), backpressure_count_.load(std::memory_order_relaxed), - backpressure_wait_ns_.load(std::memory_order_relaxed), - unhandled_exception_count_.load(std::memory_order_relaxed) + backpressure_wait_ns_.load(std::memory_order_relaxed) }; } void Render_Domain::run() { @@ -155,7 +158,7 @@ void Render_Domain::run() { try { task->function(); } catch (...) { - unhandled_exception_count_.fetch_add(1, std::memory_order_relaxed); + task->on_exception(std::current_exception()); } task.reset(); if (destroy_on_exit_.load(std::memory_order_acquire)) { diff --git a/render_3D/render_3D/detail/Render_Domain.h b/render_3D/render_3D/detail/Render_Domain.h index 8396069..4fde745 100644 --- a/render_3D/render_3D/detail/Render_Domain.h +++ b/render_3D/render_3D/detail/Render_Domain.h @@ -17,8 +17,12 @@ namespace renderive::render_3d::detail { class Render_Domain final : public std::enable_shared_from_this { struct Task { - explicit Task(std::function value) : function(std::move(value)) {} + Task(std::function value, + std::function exception_handler) + : function(std::move(value)), + on_exception(std::move(exception_handler)) {} std::function function; + std::function on_exception; }; public: enum class Error : std::uint8_t { @@ -33,7 +37,6 @@ public: std::size_t peak_queued{}; std::uint64_t backpressure_count{}; std::uint64_t backpressure_wait_ns{}; - std::uint64_t unhandled_exception_count{}; }; class Prepared_Task final { public: @@ -62,8 +65,11 @@ public: ~Render_Domain(); Render_Domain(const Render_Domain&) = delete; Render_Domain& operator=(const Render_Domain&) = delete; - [[nodiscard]] Prepare_Result prepare(std::function function); - [[nodiscard]] Error post(std::function function); + using Exception_Handler = std::function; + [[nodiscard]] Prepare_Result prepare(std::function function, + Exception_Handler on_exception); + [[nodiscard]] Error post(std::function function, + Exception_Handler on_exception); [[nodiscard]] Error post(Prepared_Task task); template struct Invoke_Result { @@ -89,7 +95,10 @@ public: } auto task = std::make_shared>(std::forward(function)); auto result = task->get_future(); - output.error = post([task] { (*task)(); }); + output.error = post([task] { (*task)(); }, + [](std::exception_ptr exception) { + std::rethrow_exception(exception); + }); if (output.error != Error::none) return output; if constexpr (std::is_void_v) { @@ -119,7 +128,6 @@ private: std::atomic_size_t peak_queued_{}; std::atomic_uint64_t backpressure_count_{}; std::atomic_uint64_t backpressure_wait_ns_{}; - std::atomic_uint64_t unhandled_exception_count_{}; std::atomic_bool stopping_{}; std::atomic_bool destroy_on_exit_{}; std::thread thread_; diff --git a/render_3D/tests/Gpu_Completion_Service_Tests.cpp b/render_3D/tests/Gpu_Completion_Service_Tests.cpp index 6272895..d791c34 100644 --- a/render_3D/tests/Gpu_Completion_Service_Tests.cpp +++ b/render_3D/tests/Gpu_Completion_Service_Tests.cpp @@ -15,16 +15,23 @@ TEST(GpuCompletionService, AbandonedReservationDoesNotComplete) { auto prepared = Gpu_Completion_Service::instance().prepare( [&](Gpu_Completion_Service::Result) { completion_count.fetch_add(1, std::memory_order_relaxed); - }, false); + }, [](std::exception_ptr) {}, false); ASSERT_TRUE(prepared); } EXPECT_EQ(completion_count.load(std::memory_order_relaxed), 0); } TEST(GpuCompletionService, CanceledReservationWakesIdleService) { auto& service = Gpu_Completion_Service::instance(); + const auto previous_deadline = + std::chrono::steady_clock::now() + std::chrono::milliseconds(100); + while (service.statistics().in_flight != 0 && + std::chrono::steady_clock::now() < previous_deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); const auto baseline = service.statistics().in_flight; + ASSERT_EQ(baseline, 0U); { - auto prepared = service.prepare([](Gpu_Completion_Service::Result) {}, false); + auto prepared = service.prepare([](Gpu_Completion_Service::Result) {}, + [](std::exception_ptr) {}, false); ASSERT_TRUE(prepared); EXPECT_EQ(service.statistics().in_flight, baseline + 1); } diff --git a/render_3D/tests/Point_Render_Integration_Tests.cpp b/render_3D/tests/Point_Render_Integration_Tests.cpp index 785656a..11737eb 100644 --- a/render_3D/tests/Point_Render_Integration_Tests.cpp +++ b/render_3D/tests/Point_Render_Integration_Tests.cpp @@ -59,7 +59,9 @@ std::size_t colored_pixel_count(const Pixel_Frame& frame) { TEST(PointRenderIntegration, RendersRealRgbaAndResizes) { try { auto demo = make_test_scene(Scene_Options{.viewport = {320, 200}}); - ASSERT_TRUE(demo.scene->request_frame()); + ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Error::none); + ASSERT_EQ(demo.scene->render_scene().wait_for_render(), + Scene_Render_Error::none); auto first = demo.scene->latest_frame(); ASSERT_NE(first, nullptr); EXPECT_EQ(first->extent, (Extent{320, 200})); @@ -67,7 +69,9 @@ TEST(PointRenderIntegration, RendersRealRgbaAndResizes) { EXPECT_GT(colored_pixel_count(*first), 100U); demo.scene->resize({480, 270}); - ASSERT_TRUE(demo.scene->request_frame()); + ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Error::none); + ASSERT_EQ(demo.scene->render_scene().wait_for_render(), + Scene_Render_Error::none); auto resized = demo.scene->latest_frame(); ASSERT_NE(resized, nullptr); EXPECT_EQ(resized->extent, (Extent{480, 270})); @@ -81,7 +85,9 @@ TEST(PointRenderIntegration, RendersRealRgbaAndResizes) { TEST(PointRenderIntegration, KernelEventsReachDatovizArcballOnItsDomain) { try { auto demo = make_test_scene(Scene_Options{.viewport = {320, 200}}); - ASSERT_TRUE(demo.scene->request_frame()); + ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Error::none); + ASSERT_EQ(demo.scene->render_scene().wait_for_render(), + Scene_Render_Error::none); const auto before = demo.scene->latest_frame()->rgba8; ::renderive::Basic_Pointer_Event press( @@ -102,7 +108,9 @@ TEST(PointRenderIntegration, KernelEventsReachDatovizArcballOnItsDomain) { release.button = ::renderive::Mouse_Button::Left; demo.scene->dispatch(release); - ASSERT_TRUE(demo.scene->request_frame()); + ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Error::none); + ASSERT_EQ(demo.scene->render_scene().wait_for_render(), + Scene_Render_Error::none); const auto after_drag = demo.scene->latest_frame(); ASSERT_NE(after_drag, nullptr); EXPECT_NE(after_drag->rgba8, before); @@ -113,7 +121,9 @@ TEST(PointRenderIntegration, KernelEventsReachDatovizArcballOnItsDomain) { wheel.angle_delta_y = -216.0; demo.scene->dispatch(wheel); - ASSERT_TRUE(demo.scene->request_frame()); + ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Error::none); + ASSERT_EQ(demo.scene->render_scene().wait_for_render(), + Scene_Render_Error::none); const auto after_wheel = demo.scene->latest_frame(); ASSERT_NE(after_wheel, nullptr); EXPECT_GT(colored_pixel_count(*after_wheel), 100U); @@ -131,7 +141,8 @@ TEST(PointRenderIntegration, 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()); + ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Error::none); + ASSERT_EQ(kernel_scene.wait_for_render(), Scene_Render_Error::none); const auto session = kernel_scene.capture_session(capture); ASSERT_TRUE(session); diff --git a/render_3D/tests/Point_State_Tests.cpp b/render_3D/tests/Point_State_Tests.cpp index 0c2ec5f..871ffa5 100644 --- a/render_3D/tests/Point_State_Tests.cpp +++ b/render_3D/tests/Point_State_Tests.cpp @@ -28,7 +28,9 @@ struct Test_Scene final Extent{320, 180}); point_id = visual->renderable_id(); auto builder = attach_builder(); - builder.attach(renderive_Owner(visual)); + if (builder.attach(renderive_Owner(visual)) != + Scene_Edit_Error::none) + throw std::logic_error("test visual attachment failed"); } ~Test_Scene() override { shutdown(); } @@ -41,7 +43,8 @@ struct Test_Scene final } auto renderer = frame_control.acquire_renderer(); ASSERT_TRUE(renderer); - Scene_Base::render(*renderer); + ASSERT_EQ(Scene_Base::render(*renderer), Scene_Render_Error::none); + ASSERT_EQ(wait_for_render(), Scene_Render_Error::none); } Node_Execution_Result render_scene( @@ -127,8 +130,8 @@ TEST(PointState, RejectsInvalidStateAndPayloadAtTheOwningBoundary) { invalid.style.stroke_width_px = -1.0F; EXPECT_THROW(visual->set<&Point_State::style>(invalid.style), std::invalid_argument); - EXPECT_THROW(visual->update_points(std::vector{{{}, {}, 0.0F}}), - std::invalid_argument); + EXPECT_EQ(visual->update_points(std::vector{{{}, {}, 0.0F}}), + Visual_Data_Error::invalid_data); } TEST(PointRenderPlan, OrdersParallelPreparationBeforeSingleSceneRenderNode) { diff --git a/render_3D/tests/Render_Domain_Tests.cpp b/render_3D/tests/Render_Domain_Tests.cpp index aeb3fa4..c185523 100644 --- a/render_3D/tests/Render_Domain_Tests.cpp +++ b/render_3D/tests/Render_Domain_Tests.cpp @@ -20,7 +20,7 @@ TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) { std::atomic executed{}; auto prepared = domain->prepare([&] { executed.store(true, std::memory_order_release); - }); + }, [](std::exception_ptr) {}); ASSERT_TRUE(prepared); EXPECT_EQ(domain->post(std::move(prepared.task)), Render_Domain::Error::none); EXPECT_TRUE(domain->invoke([] {})); @@ -29,7 +29,7 @@ TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) { TEST(RenderDomain, AbandonedPreparedTasksReleaseReservedCapacity) { auto domain = Render_Domain::acquire(0x7ffffff9U); for (std::size_t index = 0; index < 128; ++index) { - auto prepared = domain->prepare([] {}); + auto prepared = domain->prepare([] {}, [](std::exception_ptr) {}); ASSERT_TRUE(prepared); } EXPECT_TRUE(domain->invoke([] {})); @@ -52,14 +52,20 @@ TEST(RenderDomain, ReportsBoundedAdmissionStatistics) { EXPECT_TRUE(statistics.admitted <= statistics.capacity); EXPECT_TRUE(statistics.queued <= statistics.capacity); } -TEST(RenderDomain, ContainsUnhandledFireAndForgetExceptionsAndKeepsRunning) { +TEST(RenderDomain, DeliversFireAndForgetExceptionsAndKeepsRunning) { auto domain = Render_Domain::acquire(0x7ffffff6U); - const auto before = domain->statistics().unhandled_exception_count; - EXPECT_EQ(domain->post([] { throw std::runtime_error("unexpected render-domain failure"); }), + std::promise delivered; + auto exception = delivered.get_future(); + EXPECT_EQ(domain->post( + [] { throw std::runtime_error("unexpected render-domain failure"); }, + [&delivered](std::exception_ptr value) { + delivered.set_value(std::move(value)); + }), Render_Domain::Error::none); + ASSERT_EQ(exception.wait_for(std::chrono::seconds(1)), + std::future_status::ready); + EXPECT_THROW(std::rethrow_exception(exception.get()), std::runtime_error); EXPECT_TRUE(domain->invoke([] {})); - const auto after = domain->statistics().unhandled_exception_count; - EXPECT_EQ(after, before + 1U); const auto value = domain->invoke([] { return 17; }); ASSERT_TRUE(value); EXPECT_EQ(*value.value, 17); @@ -73,7 +79,7 @@ TEST(RenderDomain, FinalOwnerMayBeReleasedOnAffinityThread) { EXPECT_EQ(domain->post([owned = domain, &released]() mutable { owned.reset(); released.set_value(); - }), Render_Domain::Error::none); + }, [](std::exception_ptr) {}), Render_Domain::Error::none); domain.reset(); EXPECT_EQ(finished.wait_for(std::chrono::seconds(1)), std::future_status::ready); diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index 4fbc467..7b818d9 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -212,27 +213,23 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_thisinvalidateTimer(automatic_timer); automatic_timer = trantor::InvalidTimerId; } - void arm_automatic_render_locked() noexcept { + void arm_automatic_render_locked() { if (!automatic_low_latency) return; disarm_automatic_render(); if (!scene || !scene->can_render_automatically() || render_task_pending) return; - try { - const auto interval = std::chrono::nanoseconds( - std::max(1, scene->kernel_refresh_interval_ns())); - const auto deadline = last_render_started ? *last_render_started + interval : Clock::now(); - const auto delay = std::max(Clock::duration::zero(), deadline - Clock::now()); - const std::uint64_t revision = automatic_timer_revision; - automatic_timer = automatic_render_loop()->runAfter( - std::chrono::duration(delay).count(), - [session = weak_from_this(), revision] { - if (auto value = session.lock()) - value->automatic_render_due(revision); - }); - } catch (...) { - disarm_automatic_render(); - } + const auto interval = std::chrono::nanoseconds( + std::max(1, scene->kernel_refresh_interval_ns())); + const auto deadline = last_render_started ? *last_render_started + interval : Clock::now(); + const auto delay = std::max(Clock::duration::zero(), deadline - Clock::now()); + const std::uint64_t revision = automatic_timer_revision; + automatic_timer = automatic_render_loop()->runAfter( + std::chrono::duration(delay).count(), + [session = weak_from_this(), revision] { + if (auto value = session.lock()) + value->automatic_render_due(revision); + }); } void automatic_render_due(std::uint64_t revision) { std::uint64_t generation{}; @@ -255,23 +252,26 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_thiscan_render_automatically()) { - (void)scene->render_latest_frame(); - last_render_started = started; + const auto error = scene->render_latest_frame(); + if (error == Gallery_Render_Error::none) + last_render_started = started; } render_task_pending = false; arm_automatic_render_locked(); } catch (...) { std::lock_guard lock(mutex); render_task_pending = false; - arm_automatic_render_locked(); + automatic_render_exception = std::current_exception(); + disarm_automatic_render(); } } static bool affects_render_schedule(const Web_Event& event) { @@ -299,6 +299,7 @@ struct Gallery_Plot_Session::Impl : std::enable_shared_from_this handle(const Web_Event& event) { + { + std::exception_ptr exception; + { + std::lock_guard lock(mutex); + exception = std::exchange(automatic_render_exception, {}); + } + if (exception) + std::rethrow_exception(exception); + } if (std::holds_alternative(event)) return handle_frame_request(); const bool reschedule = affects_render_schedule(event); diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index 0d98e82..e90a4d4 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -644,15 +644,7 @@ std::string Gallery_Protocol::case_json_from_controls( result["type"] = manual_refresh ? "refresh_state" : "case_state"; result["case"] = case_contract(case_id); result["frame_mode"] = frame_mode_contract(frame_mode); - try { - result["controls"] = Json::parse(controls_json.begin(), controls_json.end()); - } catch (const std::exception&) { - result["controls"] = { - {"resources", Json::array()}, - {"observers", Json::array()}, - {"render_plan", Json::object()} - }; - } + result["controls"] = Json::parse(controls_json.begin(), controls_json.end()); result["actions"] = { {"descriptor", adminive::to_descriptor_json()}, {"view", adminive::to_view_json( @@ -662,11 +654,7 @@ std::string Gallery_Protocol::case_json_from_controls( for (const auto& action : gallery_detail::registered_actions(case_id, frame_mode)) result["actions"]["data"].push_back( adminive::to_frontend_json(action)); - try { - result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); - } catch (const std::exception&) { - result["telemetry"] = Json::object(); - } + result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); if (!notice.empty()) result["notice"] = notice; return result.dump(); @@ -690,77 +678,92 @@ std::string Gallery_Protocol::observer_json(std::string_view case_id, result["type"] = "observer_state"; result["case_id"] = case_id; result["frame_mode"] = frame_mode_contract(frame_mode); - try { - result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); - } catch (const std::exception&) { - result["telemetry"] = Json::object(); - } + result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end()); return result.dump(); } -std::optional Gallery_Protocol::open_request(std::string_view message) { +Gallery_Request_Result Gallery_Protocol::open_request(std::string_view message) { + Json request; try { - const Json request = gallery_detail::parse_request(message); - if (!request.is_object() || request.value("category", "") != "event" || - request.value("type", "") != "gallery_open" || - !request.contains("case") || !request.at("case").is_string()) - return std::nullopt; - const std::string value = request.at("case").get(); - if (!is_case(value) || !request.contains("frame_mode") || - !request.at("frame_mode").is_string()) - return std::nullopt; - const auto mode = gallery_enum_cast( - request.at("frame_mode").get()); - return mode ? std::optional(Gallery_Open_Request{value, *mode}) - : std::nullopt; - } catch (const std::exception&) { - return std::nullopt; + request = gallery_detail::parse_request(message); + } catch (const Json::exception&) { + return {{}, Gallery_Request_Error::invalid_json}; } + if (!request.is_object() || !request.contains("category") || + !request.at("category").is_string() || + request.at("category").get_ref() != "event" || + !request.contains("type") || !request.at("type").is_string() || + request.at("type").get_ref() != "gallery_open") + return {{}, Gallery_Request_Error::wrong_message_type}; + if (!request.contains("case") || !request.contains("frame_mode")) + return {{}, Gallery_Request_Error::missing_field}; + if (!request.at("case").is_string() || !request.at("frame_mode").is_string()) + return {{}, Gallery_Request_Error::invalid_field}; + const std::string value = request.at("case").get(); + const auto mode = gallery_enum_cast( + request.at("frame_mode").get_ref()); + if (!is_case(value) || !mode) + return {{}, Gallery_Request_Error::invalid_field}; + return {Gallery_Open_Request{value, *mode}, Gallery_Request_Error::none}; } -std::optional Gallery_Protocol::action_request( +Gallery_Request_Result Gallery_Protocol::action_request( std::string_view message) { + Json request; try { - const Json request = gallery_detail::parse_request(message); - if (!request.is_object() || request.value("category", "") != "event" || - request.value("type", "") != "gallery_action" || - !request.contains("action") || !request.at("action").is_string()) - return std::nullopt; - Gallery_Action_Request result{request.at("action").get()}; - if (request.contains("argument")) { - const Json& argument = request.at("argument"); - if (argument.is_boolean()) - result.argument = argument.get(); - else if (argument.is_number()) { - const double value = argument.get(); - if (!std::isfinite(value)) - return std::nullopt; - result.argument = value; - } else if (argument.is_string()) - result.argument = argument.get(); - else - return std::nullopt; - } - return result; - } catch (const std::exception&) { - return std::nullopt; + request = gallery_detail::parse_request(message); + } catch (const Json::exception&) { + return {{}, Gallery_Request_Error::invalid_json}; } + if (!request.is_object() || !request.contains("category") || + !request.at("category").is_string() || + request.at("category").get_ref() != "event" || + !request.contains("type") || !request.at("type").is_string() || + request.at("type").get_ref() != "gallery_action") + return {{}, Gallery_Request_Error::wrong_message_type}; + if (!request.contains("action")) + return {{}, Gallery_Request_Error::missing_field}; + if (!request.at("action").is_string()) + return {{}, Gallery_Request_Error::invalid_field}; + Gallery_Action_Request result{request.at("action").get()}; + if (request.contains("argument")) { + const Json& argument = request.at("argument"); + if (argument.is_boolean()) + result.argument = argument.get(); + else if (argument.is_number()) { + const double value = argument.get(); + if (!std::isfinite(value)) + return {{}, Gallery_Request_Error::invalid_field}; + result.argument = value; + } else if (argument.is_string()) + result.argument = argument.get(); + else + return {{}, Gallery_Request_Error::invalid_field}; + } + return {std::move(result), Gallery_Request_Error::none}; } -std::optional Gallery_Protocol::control_patch_request( +Gallery_Request_Result Gallery_Protocol::control_patch_request( std::string_view message) { + Json request; try { - const Json request = gallery_detail::parse_request(message); - if (!request.is_object() || request.value("category", "") != "event" || - request.value("type", "") != "gallery_patch" || - !request.contains("target") || !request.at("target").is_string() || - !request.contains("patch") || !request.at("patch").is_object()) - return std::nullopt; - return Gallery_Control_Patch_Request{ - request.at("target").get(), request.at("patch").dump()}; - } catch (const std::exception&) { - return std::nullopt; + request = gallery_detail::parse_request(message); + } catch (const Json::exception&) { + return {{}, Gallery_Request_Error::invalid_json}; } + if (!request.is_object() || !request.contains("category") || + !request.at("category").is_string() || + request.at("category").get_ref() != "event" || + !request.contains("type") || !request.at("type").is_string() || + request.at("type").get_ref() != "gallery_patch") + return {{}, Gallery_Request_Error::wrong_message_type}; + if (!request.contains("target") || !request.contains("patch")) + return {{}, Gallery_Request_Error::missing_field}; + if (!request.at("target").is_string() || !request.at("patch").is_object()) + return {{}, Gallery_Request_Error::invalid_field}; + return {Gallery_Control_Patch_Request{ + request.at("target").get(), request.at("patch").dump()}, + Gallery_Request_Error::none}; } bool Gallery_Protocol::action_available(std::string_view case_id, diff --git a/web_server/app/Gallery_Protocol.h b/web_server/app/Gallery_Protocol.h index 9d0bec7..4aab61b 100644 --- a/web_server/app/Gallery_Protocol.h +++ b/web_server/app/Gallery_Protocol.h @@ -6,6 +6,23 @@ #include #include namespace renderive::web { +enum class Gallery_Request_Error { + none, + invalid_json, + wrong_message_type, + missing_field, + invalid_field +}; +template +struct Gallery_Request_Result { + std::optional value; + Gallery_Request_Error error{}; + [[nodiscard]] explicit operator bool() const noexcept { + return error == Gallery_Request_Error::none && value.has_value(); + } + [[nodiscard]] const Value* operator->() const noexcept { return &*value; } + [[nodiscard]] const Value& operator*() const noexcept { return *value; } +}; struct Gallery_Open_Request { std::string case_id; Gallery_Frame_Mode frame_mode = Gallery_Frame_Mode::Low_Latency; @@ -30,9 +47,9 @@ public: std::string_view notice, Gallery_Frame_Mode frame_mode, bool manual_refresh); - [[nodiscard]] static std::optional open_request(std::string_view message); - [[nodiscard]] static std::optional action_request(std::string_view message); - [[nodiscard]] static std::optional control_patch_request( + [[nodiscard]] static Gallery_Request_Result open_request(std::string_view message); + [[nodiscard]] static Gallery_Request_Result action_request(std::string_view message); + [[nodiscard]] static Gallery_Request_Result control_patch_request( std::string_view message); [[nodiscard]] static bool action_available(std::string_view case_id, Gallery_Frame_Mode frame_mode, std::string_view action_id); }; diff --git a/web_server/app/Web_Performance_Log.cpp b/web_server/app/Web_Performance_Log.cpp index da35d52..e4a9126 100644 --- a/web_server/app/Web_Performance_Log.cpp +++ b/web_server/app/Web_Performance_Log.cpp @@ -29,17 +29,14 @@ void initialize_web_performance_log(const std::filesystem::path& log_directory) performance_logger->flush_on(spdlog::level::info); } -void write_web_performance_log(std::string_view json_line) noexcept { - try { - std::shared_ptr logger; - { - std::lock_guard lock(log_mutex); - logger = performance_logger; - } - if (logger) - logger->info("{}", json_line); - } catch (...) { +void write_web_performance_log(std::string_view json_line) { + std::shared_ptr logger; + { + std::lock_guard lock(log_mutex); + logger = performance_logger; } + if (logger) + logger->info("{}", json_line); } std::filesystem::path web_performance_log_path() { diff --git a/web_server/app/Web_Performance_Log.h b/web_server/app/Web_Performance_Log.h index 32d75eb..39920b3 100644 --- a/web_server/app/Web_Performance_Log.h +++ b/web_server/app/Web_Performance_Log.h @@ -6,7 +6,7 @@ namespace renderive::web { void initialize_web_performance_log(const std::filesystem::path& log_directory); -void write_web_performance_log(std::string_view json_line) noexcept; +void write_web_performance_log(std::string_view json_line); [[nodiscard]] std::filesystem::path web_performance_log_path(); } // namespace renderive::web diff --git a/web_server/app/Web_Plot_Session.cpp b/web_server/app/Web_Plot_Session.cpp index 4a7c153..aa2a3af 100644 --- a/web_server/app/Web_Plot_Session.cpp +++ b/web_server/app/Web_Plot_Session.cpp @@ -159,7 +159,9 @@ struct Web_Plot_Session::Impl { apply_layout(plot.viewport_size()); plot.activate_view(); update_model(); - (void)plot.render_frame(true); + const auto initial_render = plot.render_frame(true); + if (initial_render != Plot_Render_Error::none) + plot.request_redraw(); } ~Impl() { plot.deactivate_view(); @@ -266,7 +268,7 @@ struct Web_Plot_Session::Impl { if (!plot.view_active()) return std::nullopt; update_model(); - if (!plot.render_frame(true)) + if (plot.render_frame(true) != Plot_Render_Error::none) return std::nullopt; std::string pixels; const Color background = plot.background_color(); diff --git a/web_server/app/common/Gallery_Scene_Interface.h b/web_server/app/common/Gallery_Scene_Interface.h index a313cc1..566c8da 100644 --- a/web_server/app/common/Gallery_Scene_Interface.h +++ b/web_server/app/common/Gallery_Scene_Interface.h @@ -11,6 +11,15 @@ #include #include namespace renderive::web { +enum class Gallery_Render_Error : std::uint8_t { + none, + inactive, + frame_unavailable, + cancelled, + deadline_exceeded, + backend_failure, + shutting_down +}; class Gallery_Scene_Interface { public: virtual ~Gallery_Scene_Interface() = default; @@ -24,7 +33,7 @@ public: [[nodiscard]] virtual adminive::Update_Result apply_patch(std::string_view target, const nlohmann::json& patch) = 0; virtual void resize(int width, int height) = 0; virtual void dispatch(const Gallery_Input_Event& event) = 0; - virtual bool render_latest_frame() = 0; + [[nodiscard]] virtual Gallery_Render_Error render_latest_frame() = 0; [[nodiscard]] virtual std::optional encode_latest_pixels() = 0; virtual void record_pixel_response(std::chrono::steady_clock::time_point request_started, std::chrono::steady_clock::time_point encode_started, std::chrono::steady_clock::time_point encode_finished, std::size_t pixel_bytes) = 0; [[nodiscard]] virtual std::string action(const Gallery_Action_Request& request, bool& recognized) = 0; diff --git a/web_server/app/render_2D/Gallery_Scene2D.cpp b/web_server/app/render_2D/Gallery_Scene2D.cpp index 42b1f9f..154c346 100644 --- a/web_server/app/render_2D/Gallery_Scene2D.cpp +++ b/web_server/app/render_2D/Gallery_Scene2D.cpp @@ -28,6 +28,24 @@ #include namespace renderive::web { namespace { +Gallery_Render_Error gallery_render_error(Plot_Render_Error error) { + switch (error) { + case Plot_Render_Error::none: + return Gallery_Render_Error::none; + case Plot_Render_Error::view_inactive: + return Gallery_Render_Error::inactive; + case Plot_Render_Error::cancelled: + return Gallery_Render_Error::cancelled; + case Plot_Render_Error::deadline_exceeded: + return Gallery_Render_Error::deadline_exceeded; + case Plot_Render_Error::external_failure: + return Gallery_Render_Error::backend_failure; + case Plot_Render_Error::scene_shutting_down: + return Gallery_Render_Error::shutting_down; + default: + return Gallery_Render_Error::frame_unavailable; + } +} template void update_axis_state(const renderive_Owner& axis, Update&& update) { if (const auto numeric = renderive_dynamic_owner_cast(axis)) { @@ -171,9 +189,10 @@ public: const auto make_group = [&](std::string name) { auto group = detail::make_renderable_group(true); group->set_object_name(std::move(name)); - attach.attach(group); - attach.add_display_parent(group, root_); - attach.add_dependency_parent(group, root_); + if (attach.attach(group) != Scene_Edit_Error::none || + attach.add_display_parent(group, root_) != Scene_Edit_Error::none || + attach.add_dependency_parent(group, root_) != Scene_Edit_Error::none) + throw std::logic_error("validated gallery group attachment failed"); return group; }; axes_node_ = make_group("坐标轴层"); @@ -193,7 +212,8 @@ public: static_cast(plot_.set_max_render_fps(30.0)); plot_.activate_view(); update_model(); - (void)plot_.render_frame(true); + if (plot_.render_frame(true) != Plot_Render_Error::none) + plot_.request_redraw(); } ~Gallery_Scene() { plot_.deactivate_view(); @@ -328,22 +348,23 @@ public: } }, event); } - bool render_latest_frame() { + Gallery_Render_Error render_latest_frame() override { if (!plot_.view_active()) - return false; + return Gallery_Render_Error::inactive; update_model(); const auto started = std::chrono::steady_clock::now(); - const bool rendered = plot_.render_frame(true); + const auto error = plot_.render_frame(true); + const bool rendered = error == Plot_Render_Error::none; record_performance(started, rendered); if (rendered) rendered_since_last_pixel_ = true; - return rendered; + return gallery_render_error(error); } [[nodiscard]] std::optional encode_latest_pixels() { if (!plot_.view_active()) return std::nullopt; if (!rendered_since_last_pixel_ && !can_render_automatically() && - !render_latest_frame()) + render_latest_frame() != Gallery_Render_Error::none) return std::nullopt; rendered_since_last_pixel_ = false; std::string pixels; @@ -398,7 +419,7 @@ public: } if (request.id == "mode_prepare" || request.id == "mode_enqueue") { update_model(); - const bool prepared = plot_.prepare_frame(); + const bool prepared = plot_.prepare_frame() == Plot_Render_Error::none; last_action_result_ = prepared ? "frame prepared" : "prepare rejected"; return prepared ? "Kernel 帧已准备/入队" : "Kernel 拒绝准备帧"; } @@ -410,19 +431,19 @@ public: int prepared{}; for (int index = 0; index < count; ++index) { update_model(); - prepared += plot_.prepare_frame() ? 1 : 0; + prepared += plot_.prepare_frame() == Plot_Render_Error::none ? 1 : 0; } last_action_result_ = "enqueued=" + std::to_string(prepared); return "回放帧已批量压入 Flow 队列"; } if (request.id == "mode_refresh") { - const bool refreshed = plot_.refresh_manual_frame(); + const bool refreshed = plot_.refresh_manual_frame() == Plot_Render_Error::none; last_action_result_ = refreshed ? "manual refresh succeeded" : "manual refresh failed"; return refreshed ? "Manual 待处理帧已提交刷新" : "当前没有可刷新的 Manual 帧"; } if (request.id == "mode_render" || request.id == "mode_dequeue") { const auto started = std::chrono::steady_clock::now(); - const bool rendered = plot_.render_prepared_frame(); + const bool rendered = plot_.render_prepared_frame() == Plot_Render_Error::none; record_performance(started, rendered); rendered_since_last_pixel_ = rendered; last_action_result_ = rendered ? "prepared frame rendered" : "no prepared frame"; @@ -436,7 +457,7 @@ public: if (request.id == "mode_cycle") { update_model(); const auto started = std::chrono::steady_clock::now(); - const bool rendered = plot_.render_frame(true); + const bool rendered = plot_.render_frame(true) == Plot_Render_Error::none; record_performance(started, rendered); rendered_since_last_pixel_ = rendered; last_action_result_ = rendered ? "full frame cycle rendered" : "frame cycle skipped"; @@ -894,7 +915,8 @@ private: consumer_manual_interval_ns_ = frequency_to_ns(feedback_policy_.manual_fps); consumer_feedback_source_ = "none"; if (!feedback_policy_.enabled) { - plot_.clear_consumer_feedback(); + if (plot_.clear_consumer_feedback() != Plot_Control_Error::none) + throw std::logic_error("low-latency feedback clearing failed"); consumer_feedback_source_ = "disabled"; return; } @@ -921,10 +943,12 @@ private: select_source(feedback_policy_.manual, consumer_manual_interval_ns_, "manual"); if (interval_ns == 0) { - plot_.clear_consumer_feedback(); + if (plot_.clear_consumer_feedback() != Plot_Control_Error::none) + throw std::logic_error("low-latency feedback clearing failed"); return; } - static_cast(plot_.set_consumer_feedback({interval_ns})); + if (plot_.set_consumer_feedback({interval_ns}) != Plot_Control_Error::none) + throw std::logic_error("validated low-latency feedback was rejected"); } 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 9a68df5..56923ab 100644 --- a/web_server/app/render_3D/Gallery_Scene3D.cpp +++ b/web_server/app/render_3D/Gallery_Scene3D.cpp @@ -64,6 +64,52 @@ struct Type_Descriptor { namespace renderive::web { namespace { using namespace renderive::render_3d; +Gallery_Render_Error gallery_render_error(Frame_Request_Error error) { + switch (error) { + case Frame_Request_Error::none: + return Gallery_Render_Error::none; + case Frame_Request_Error::cancelled: + return Gallery_Render_Error::cancelled; + case Frame_Request_Error::deadline_exceeded: + return Gallery_Render_Error::deadline_exceeded; + case Frame_Request_Error::external_failure: + return Gallery_Render_Error::backend_failure; + case Frame_Request_Error::scene_shutting_down: + return Gallery_Render_Error::shutting_down; + default: + return Gallery_Render_Error::frame_unavailable; + } +} +Gallery_Render_Error gallery_render_error(Scene_Render_Error error) { + switch (error) { + case Scene_Render_Error::none: + return Gallery_Render_Error::none; + case Scene_Render_Error::cancelled: + return Gallery_Render_Error::cancelled; + case Scene_Render_Error::deadline_exceeded: + return Gallery_Render_Error::deadline_exceeded; + case Scene_Render_Error::external_failure: + return Gallery_Render_Error::backend_failure; + case Scene_Render_Error::shutting_down: + return Gallery_Render_Error::shutting_down; + } + throw std::logic_error("unknown scene render error"); +} +Frame_Request_Error frame_request_error(Scene_Render_Error error) { + switch (error) { + case Scene_Render_Error::none: + return Frame_Request_Error::none; + case Scene_Render_Error::cancelled: + return Frame_Request_Error::cancelled; + case Scene_Render_Error::deadline_exceeded: + return Frame_Request_Error::deadline_exceeded; + case Scene_Render_Error::external_failure: + return Frame_Request_Error::external_failure; + case Scene_Render_Error::shutting_down: + return Frame_Request_Error::scene_shutting_down; + } + throw std::logic_error("unknown scene render error"); +} struct Gallery_Point { double x{}; @@ -251,8 +297,12 @@ public: void resize(int width, int height) override { if (width <= 0 || height <= 0) return; - scene_.resize({static_cast(width), - static_cast(height)}); + const auto error = scene_.resize({static_cast(width), + static_cast(height)}); + if (error != ::renderive::render_3d::Scene_Control_Error::none) { + last_event_ = "resize_rejected"; + return; + } last_event_ = "resized"; } @@ -273,7 +323,9 @@ public: input.buttons = pointer->buttons; input.modifiers = static_cast<::renderive::Keyboard_Modifier>( pointer->modifiers); - scene_.dispatch(input); + if (scene_.dispatch(input) != + ::renderive::render_3d::Scene_Control_Error::none) + last_event_ = "pointer_rejected"; return; } if (const auto* wheel = std::get_if(&event)) { @@ -286,7 +338,9 @@ public: input.angle_delta_y = wheel->angle_delta_y; input.modifiers = static_cast<::renderive::Keyboard_Modifier>( wheel->modifiers); - scene_.dispatch(input); + if (scene_.dispatch(input) != + ::renderive::render_3d::Scene_Control_Error::none) + last_event_ = "wheel_rejected"; return; } const auto& keyboard = std::get(event); @@ -299,14 +353,17 @@ public: input.modifiers = static_cast<::renderive::Keyboard_Modifier>( keyboard.modifiers); input.auto_repeat = keyboard.auto_repeat; - scene_.dispatch(input); + if (scene_.dispatch(input) != + ::renderive::render_3d::Scene_Control_Error::none) + last_event_ = "key_rejected"; } - bool render_latest_frame() override { + Gallery_Render_Error render_latest_frame() override { if (!can_render_automatically()) - return false; + return Gallery_Render_Error::inactive; advance_blue_point(0.025F); - return render_request(); + const auto request_error = render_request(); + return gallery_render_error(request_error); } [[nodiscard]] std::optional encode_latest_pixels() override { @@ -377,17 +434,17 @@ public: blue_offset_ = std::min(0.25F, blue_offset_ + 0.0125F); publish_points(); } - const bool prepared = scene_.prepare_frame(); + const bool prepared = scene_.prepare_frame() == Frame_Request_Error::none; last_event_ = prepared ? "frame_prepared" : "prepare_rejected"; return prepared ? "3D frame prepared" : "3D frame prepare rejected"; } if (request.id == "mode_refresh") { - const bool refreshed = scene_.refresh_manual_frame(); + const bool refreshed = scene_.refresh_manual_frame() == Frame_Request_Error::none; last_event_ = refreshed ? "manual_refreshed" : "manual_refresh_failed"; return refreshed ? "Manual 3D frame refreshed" : "No manual frame to refresh"; } if (request.id == "mode_render" || request.id == "mode_dequeue") { - const bool rendered = render_prepared(); + const bool rendered = render_prepared() == Frame_Request_Error::none; last_event_ = rendered ? "frame_rendered" : "render_rejected"; return rendered ? "3D frame rendered" : "No 3D frame to render"; } @@ -490,9 +547,7 @@ public: {"queued", runtime.render_domain.queued}, {"peak_queued", runtime.render_domain.peak_queued}, {"backpressure_count", runtime.render_domain.backpressure_count}, - {"backpressure_wait_ns", runtime.render_domain.backpressure_wait_ns}, - {"unhandled_exception_count", - runtime.render_domain_unhandled_exception_count}}}, + {"backpressure_wait_ns", runtime.render_domain.backpressure_wait_ns}}}, {"gpu_completion", {{"capacity", runtime.gpu_completion.capacity}, {"active", runtime.gpu_completion.active}, @@ -560,7 +615,9 @@ private: } if (extra_attached_) points.push_back({{0.0F, 0.62F, 0.0F}, {255, 230, 20, 255}, 58.0F}); - visual_->update_points(std::move(points)); + if (visual_->update_points(std::move(points)) != + Visual_Data_Error::none) + throw std::logic_error("generated gallery point data is invalid"); } void advance_blue_point(float step) { @@ -571,38 +628,50 @@ private: } void render_initial_frame() { + Frame_Request_Error error{Frame_Request_Error::none}; if (frame_mode_ == Gallery_Frame_Mode::Manual) { - if (scene_.prepare_frame() && scene_.refresh_manual_frame()) - render_prepared(); + if (scene_.prepare_frame() == Frame_Request_Error::none && + scene_.refresh_manual_frame() == Frame_Request_Error::none) + error = render_prepared(); + else + error = Frame_Request_Error::no_pending_frame; } else { - render_request(); + error = render_request(); } + if (error != Frame_Request_Error::none) + last_action_result_ = "initial frame is not available"; } - bool render_request() { + Frame_Request_Error render_request() { const auto started = std::chrono::steady_clock::now(); ++render_attempt_count_; - const bool rendered = scene_.request_frame(); + auto error = scene_.request_frame(); + if (error == Frame_Request_Error::none) + error = frame_request_error(scene_.render_scene().wait_for_render()); + const bool rendered = error == Frame_Request_Error::none; last_render_ms_ = std::chrono::duration( std::chrono::steady_clock::now() - started) .count(); successful_render_count_ += rendered ? 1U : 0U; if (rendered) record_sample(render_history_, std::chrono::steady_clock::now()); - return rendered; + return error; } - bool render_prepared() { + Frame_Request_Error render_prepared() { const auto started = std::chrono::steady_clock::now(); ++render_attempt_count_; - const bool rendered = scene_.render_prepared_frame(); + auto error = scene_.render_prepared_frame(); + if (error == Frame_Request_Error::none) + error = frame_request_error(scene_.render_scene().wait_for_render()); + const bool rendered = error == Frame_Request_Error::none; last_render_ms_ = std::chrono::duration( std::chrono::steady_clock::now() - started) .count(); successful_render_count_ += rendered ? 1U : 0U; if (rendered) record_sample(render_history_, std::chrono::steady_clock::now()); - return rendered; + return error; } std::uint64_t session_id_{}; diff --git a/web_server/tests/Datoviz_Gallery_Tests.cpp b/web_server/tests/Datoviz_Gallery_Tests.cpp index 3433fd9..8106f76 100644 --- a/web_server/tests/Datoviz_Gallery_Tests.cpp +++ b/web_server/tests/Datoviz_Gallery_Tests.cpp @@ -352,7 +352,10 @@ TEST(RenderiveWebDatovizGallery, AddedAndChurnedVisualsAreActuallyRemoved) { ASSERT_EQ(invoke_action(session, "remove_mesh").at("type"), "case_state"); manual_render_cycle(session); const std::string removed = request_pixels(session); - EXPECT_EQ(removed, baseline); + ASSERT_GE(removed.size(), pixel_frame_header_size); + ASSERT_GE(baseline.size(), pixel_frame_header_size); + EXPECT_EQ(removed.substr(pixel_frame_header_size), + baseline.substr(pixel_frame_header_size)); const auto churned = invoke_action(session, "mesh_churn"); ASSERT_EQ(churned.at("type"), "case_state"); @@ -360,7 +363,8 @@ TEST(RenderiveWebDatovizGallery, AddedAndChurnedVisualsAreActuallyRemoved) { "mesh_churned"); const std::string after_churn = request_pixels(session); expect_actual_datoviz_pixels(after_churn, 560, 320); - EXPECT_EQ(after_churn, baseline); + EXPECT_EQ(after_churn.substr(pixel_frame_header_size), + baseline.substr(pixel_frame_header_size)); } TEST(RenderiveWebDatovizGallery, TextureAndTextActionsReachTheThreeDimensionalBackend) { @@ -372,17 +376,24 @@ TEST(RenderiveWebDatovizGallery, TextureAndTextActionsReachTheThreeDimensionalBa ASSERT_EQ(invoke_action(session, "toggle_texture").at("type"), "case_state"); manual_render_cycle(session); const std::string without_texture = request_pixels(session); - EXPECT_NE(without_texture, baseline); + ASSERT_GE(without_texture.size(), pixel_frame_header_size); + ASSERT_GE(baseline.size(), pixel_frame_header_size); + EXPECT_NE(without_texture.substr(pixel_frame_header_size), + baseline.substr(pixel_frame_header_size)); ASSERT_EQ(invoke_action(session, "toggle_texture").at("type"), "case_state"); manual_render_cycle(session); const std::string restored_texture = request_pixels(session); - EXPECT_EQ(restored_texture, baseline); + ASSERT_GE(restored_texture.size(), pixel_frame_header_size); + EXPECT_EQ(restored_texture.substr(pixel_frame_header_size), + baseline.substr(pixel_frame_header_size)); ASSERT_EQ(invoke_action(session, "change_text").at("type"), "case_state"); manual_render_cycle(session); const std::string changed_text = request_pixels(session); - EXPECT_NE(changed_text, restored_texture); + ASSERT_GE(changed_text.size(), pixel_frame_header_size); + EXPECT_NE(changed_text.substr(pixel_frame_header_size), + restored_texture.substr(pixel_frame_header_size)); } } // namespace