diff --git a/Kernel/src/renderive/render_graph/Render_Plan.cpp b/Kernel/src/renderive/render_graph/Render_Plan.cpp index 9d6bcc5..2d279b8 100644 --- a/Kernel/src/renderive/render_graph/Render_Plan.cpp +++ b/Kernel/src/renderive/render_graph/Render_Plan.cpp @@ -104,26 +104,20 @@ bool Render_Graph_Builder::reaches(std::size_t from, std::size_t target) const { std::shared_ptr Render_Plan_History::publish(Render_Graph graph) { normalize(graph); std::lock_guard lock(mutex_); - const auto existing = std::find_if(plans_.begin(), plans_.end(), [&graph](const auto& plan) { - return same_topology(plan->graph, graph); - }); - if (existing != plans_.end()) { - current_ = *existing; - return current_; - } + if (!plans_.empty() && same_topology(plans_.back()->graph, graph)) + return plans_.back(); if (next_version_ == 0 || next_version_ == std::numeric_limits::max()) throw std::overflow_error("render plan version exhausted"); auto plan = std::make_shared(); plan->version = next_version_++; plan->graph = std::move(graph); plans_.push_back(plan); - current_ = plan; return plan; } std::shared_ptr Render_Plan_History::current() const { std::lock_guard lock(mutex_); - return current_; + return plans_.empty() ? nullptr : plans_.back(); } std::shared_ptr Render_Plan_History::find(Render_Plan_Version version) const { diff --git a/Kernel/src/renderive/render_graph/Render_Plan.hpp b/Kernel/src/renderive/render_graph/Render_Plan.hpp index 1a61490..db7f499 100644 --- a/Kernel/src/renderive/render_graph/Render_Plan.hpp +++ b/Kernel/src/renderive/render_graph/Render_Plan.hpp @@ -68,7 +68,6 @@ public: private: mutable std::mutex mutex_; std::vector> plans_; - std::shared_ptr current_; Render_Plan_Version next_version_{1}; }; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index 5003921..31bf81c 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -89,8 +88,6 @@ struct Scene_Base::Compiled_Render_Plan { Renderable_Id id{}; std::shared_ptr graph; std::vector dependency_parent_ids; - bool prepare_required{}; - bool paint_required{}; }; std::vector renderables; std::vector display_order; @@ -978,9 +975,7 @@ std::shared_ptr Scene_Base::compile_render_pla const auto& state = snapshot.renderables[index]; if (key.id != state.renderable_id || key.graph != state.render_graph || - key.dependency_parent_ids != state.dependency_parent_ids || - key.prepare_required != state.prepare_required || - key.paint_required != state.paint_required) + key.dependency_parent_ids != state.dependency_parent_ids) return false; } return true; @@ -992,8 +987,7 @@ std::shared_ptr Scene_Base::compile_render_pla compiled.renderables.reserve(snapshot.renderables.size()); for (const auto& state : snapshot.renderables) { compiled.renderables.push_back({ - state.renderable_id, state.render_graph, state.dependency_parent_ids, - state.prepare_required, state.paint_required}); + state.renderable_id, state.render_graph, state.dependency_parent_ids}); } compiled.display_order = snapshot.display_order; }; @@ -1027,16 +1021,10 @@ std::shared_ptr Scene_Base::compile_render_pla const auto& state = snapshot.renderables[item.snapshot_index]; if (!state.render_graph) throw std::logic_error("frame renderable has no graph snapshot"); - std::unordered_set selected; for (const auto& node : state.render_graph->graph.nodes) { if (node.kind == Render_Node_Kind::composite) throw std::logic_error( "renderable graph must not define composite nodes"); - const bool include = node.kind == Render_Node_Kind::prepare - ? state.prepare_required - : state.paint_required; - if (!include) - continue; graph.nodes.push_back(node); const auto& render_function = state.render_graph->functions.at(node.execution_index); @@ -1044,30 +1032,22 @@ std::shared_ptr Scene_Base::compile_render_pla functions.emplace(node.node_id, Execution_Binding{item.snapshot_index, Execution_Function{function}}); }, render_function); - selected.insert(node.node_id); - } - for (const auto& edge : state.render_graph->graph.edges) { - if (selected.contains(edge.from) && selected.contains(edge.to)) - append_edge(edge.from, edge.to); } + for (const auto& edge : state.render_graph->graph.edges) + append_edge(edge.from, edge.to); const auto stage_boundary = [&](Render_Node_Kind kind, bool roots) { std::vector result; - if ((kind == Render_Node_Kind::prepare && - !state.prepare_required) || - (kind == Render_Node_Kind::paint && !state.paint_required)) - return result; for (const auto& node : state.render_graph->graph.nodes) { - if (node.kind != kind || !selected.contains(node.node_id)) + if (node.kind != kind) continue; const bool connected = std::any_of( state.render_graph->graph.edges.begin(), state.render_graph->graph.edges.end(), [&](const Render_Edge& edge) { - const Render_Node_Id adjacent = - roots ? edge.from : edge.to; + const Render_Node_Id adjacent = roots ? edge.from : edge.to; const bool matches = roots ? edge.to == node.node_id : edge.from == node.node_id; - if (!matches || !selected.contains(adjacent)) + if (!matches) return false; return std::any_of( state.render_graph->graph.nodes.begin(), @@ -1169,12 +1149,9 @@ std::shared_ptr Scene_Base::compile_render_pla throw std::logic_error("render plan node has no execution binding"); execution_bindings[node.execution_index] = std::move(function->second); } - if (const auto existing = compiled_render_plans_.find(plan->version); - existing != compiled_render_plans_.end()) { - auto& compiled = *existing->second; - update_key(compiled); - compiled.execution_bindings = std::move(execution_bindings); - compiled_render_plan_ = existing->second; + if (compiled_render_plan_ && compiled_render_plan_->plan == plan) { + update_key(*compiled_render_plan_); + compiled_render_plan_->execution_bindings = std::move(execution_bindings); return compiled_render_plan_; } auto compiled = std::make_shared(); @@ -1183,7 +1160,6 @@ std::shared_ptr Scene_Base::compile_render_pla compiled->execution_bindings = std::move(execution_bindings); compiled->runtime = std::make_unique< renderive::render_graph::detail::Render_Graph_Runtime>(*compiled->plan); - compiled_render_plans_.emplace(compiled->plan->version, compiled); compiled_render_plan_ = compiled; return compiled; } @@ -1234,6 +1210,8 @@ void Scene_Base::execute_render_graph(Render_Task& task) { case Render_Node_Kind::prepare: { const auto& state = snapshot.renderables.at( binding.renderable_index.value()); + if (!state.prepare_required) + return Node_Execution_Result::completed(); std::get(binding.function)( Prepare_Render_Context{snapshot, state, metrics, diagnostics}); @@ -1242,6 +1220,8 @@ void Scene_Base::execute_render_graph(Render_Task& task) { case Render_Node_Kind::paint: { const auto& state = snapshot.renderables.at( binding.renderable_index.value()); + if (!state.paint_required) + return Node_Execution_Result::completed(); if (!state.paint_buffer_) throw std::logic_error( "paint node has no frame color cache"); diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index e5eca9a..6b61779 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -296,8 +296,6 @@ private: const Render_Node_Id composite_begin_node_id_; const Render_Node_Id scene_render_node_id_; Render_Plan_History render_plan_history_; - std::unordered_map> - compiled_render_plans_; std::shared_ptr compiled_render_plan_; Capture_Controller capture_controller_; Capture_Repository capture_repository_; diff --git a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp index d5c9670..67e2cd2 100644 --- a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp @@ -54,11 +54,12 @@ TEST(render_dag_test, one_graph_assigns_dense_execution_slots_and_versions_only_ changed_graph.edges.push_back({100, 101}); const auto changed = history.publish(std::move(changed_graph)); EXPECT_GT(changed->version, first->version); - const auto reused = history.publish(make_parallel_graph()); - EXPECT_EQ(reused, first); - EXPECT_EQ(history.current(), first); + const auto restored = history.publish(make_parallel_graph()); + EXPECT_GT(restored->version, changed->version); + EXPECT_EQ(history.current(), restored); EXPECT_EQ(history.find(first->version), first); EXPECT_EQ(history.find(changed->version), changed); + EXPECT_EQ(history.find(restored->version), restored); } TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_from_frame_slots) { diff --git a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp index 2ff1cff..626e934 100644 --- a/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp +++ b/Kernel/tests/renderive/scene/Render_Plan_Execution_Test.cpp @@ -191,7 +191,7 @@ TEST(render_plan_execution_test, dependent_prepare_does_not_wait_for_dependency_ } #endif -TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independent_and_prune_nodes) { +TEST(render_plan_execution_test, prepare_and_paint_cache_validity_do_not_recompile_render_plan) { Scene2D_Context<> scene; auto renderable = renderive_Owner::make(true); attach_initial(scene, renderable); @@ -199,6 +199,8 @@ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independen scene.wait_for_render(); EXPECT_TRUE(renderable->prepare_cache_valid_for_test()); EXPECT_TRUE(renderable->paint_cache_valid_for_test()); + EXPECT_EQ(renderable->prepare_count.load(), 1); + EXPECT_EQ(renderable->paint_count.load(), 1); const auto first_plan = scene.render_plan_snapshot(); const Render_Node_Id prepare_id = node(*first_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id; @@ -208,12 +210,13 @@ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independen scene.render(); scene.wait_for_render(); const auto cached_plan = scene.render_plan_snapshot(); - EXPECT_GT(cached_plan->version, first_plan->version); - EXPECT_EQ(std::count_if(cached_plan->graph.nodes.begin(), cached_plan->graph.nodes.end(), - [&](const Render_Node& value) { - return value.owner_id == renderable->renderable_id() && - value.kind != Render_Node_Kind::composite; - }), 0); + EXPECT_EQ(cached_plan, first_plan); + EXPECT_EQ(renderable->prepare_count.load(), 1); + EXPECT_EQ(renderable->paint_count.load(), 1); + EXPECT_EQ(node(*cached_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id, + prepare_id); + EXPECT_EQ(node(*cached_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id, + paint_id); renderable->invalidate_paint_for_test(); EXPECT_TRUE(renderable->prepare_cache_valid_for_test()); @@ -221,14 +224,13 @@ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independen scene.render(); scene.wait_for_render(); const auto paint_plan = scene.render_plan_snapshot(); - EXPECT_GT(paint_plan->version, cached_plan->version); + EXPECT_EQ(paint_plan, first_plan); + EXPECT_EQ(renderable->prepare_count.load(), 1); + EXPECT_EQ(renderable->paint_count.load(), 2); + EXPECT_EQ(node(*paint_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id, + prepare_id); EXPECT_EQ(node(*paint_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id, paint_id); - EXPECT_EQ(std::count_if(paint_plan->graph.nodes.begin(), paint_plan->graph.nodes.end(), - [&](const Render_Node& value) { - return value.owner_id == renderable->renderable_id() && - value.kind == Render_Node_Kind::prepare; - }), 0); renderable->invalidate_prepare_for_test(); EXPECT_FALSE(renderable->prepare_cache_valid_for_test()); @@ -236,7 +238,9 @@ TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independen scene.render(); scene.wait_for_render(); const auto rebuilt_plan = scene.render_plan_snapshot(); - EXPECT_GT(rebuilt_plan->version, paint_plan->version); + EXPECT_EQ(rebuilt_plan, first_plan); + EXPECT_EQ(renderable->prepare_count.load(), 2); + EXPECT_EQ(renderable->paint_count.load(), 3); EXPECT_EQ(node(*rebuilt_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id, prepare_id); EXPECT_EQ(node(*rebuilt_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id, diff --git a/cmake/RenderivePackage.cmake b/cmake/RenderivePackage.cmake index ee5ecd6..856c821 100644 --- a/cmake/RenderivePackage.cmake +++ b/cmake/RenderivePackage.cmake @@ -5,6 +5,7 @@ set(renderive_package_build_output set(renderive_package_excludes "^data/.*$" "^performance/.*$" + "^renderive_package/.*$" "^third_party/datoviz/.*$" "^cmake-build-[^/]*/.*$" "^(.*/)?node_modules/.*$" diff --git a/render_3D/render_3D/Point_Scene.cpp b/render_3D/render_3D/Point_Scene.cpp index 07db557..758219c 100644 --- a/render_3D/render_3D/Point_Scene.cpp +++ b/render_3D/render_3D/Point_Scene.cpp @@ -125,7 +125,7 @@ struct Basic_Point_Scene final Basic_Point_Scene(const Scene_Options& options, std::shared_ptr visual) requires (!std::same_as) - : Kernel_Scene() { + : Kernel_Scene(), render_domain(detail::Render_Domain::acquire(options.gpu_index)) { initialize(options, std::move(visual)); } @@ -134,14 +134,14 @@ struct Basic_Point_Scene final requires std::same_as : Kernel_Scene(Observer_State<>{}, typename Strategy::Configuration{ .frequency_hz = options.maximum_frames_per_second, - .replace_pending_frame = true}) { + .replace_pending_frame = true}), + render_domain(detail::Render_Domain::acquire(options.gpu_index)) { initialize(options, std::move(visual)); } ~Basic_Point_Scene() override { this->shutdown(); - gpu_completion.shutdown(); - render_domain.invoke([this] { backend.reset(); }); + render_domain->invoke([this] { backend.reset(); }); } void initialize(const Scene_Options& options, @@ -162,7 +162,7 @@ struct Basic_Point_Scene final } const detail::Scene_State initial{ options.viewport, options.clear_color, options.visual_family}; - render_domain.invoke([this, &options, &initial] { + render_domain->invoke([this, &options, &initial] { backend = std::make_unique( options.gpu_index, options.validation_enabled, initial); }); @@ -186,7 +186,7 @@ struct Basic_Point_Scene final ::renderive::Keyboard_Modifier modifiers) override { const Extent viewport = this->Scene_State_Strategy::template get< &detail::Scene_State::viewport>(); - render_domain.invoke([this, type, x, y, button, modifiers, viewport] { + render_domain->invoke([this, type, x, y, button, modifiers, viewport] { backend->dispatch_pointer(type, x, y, button, modifiers, viewport); }); this->notify_model_dirty(); @@ -197,7 +197,7 @@ struct Basic_Point_Scene final ::renderive::Keyboard_Modifier modifiers) override { const Extent viewport = this->Scene_State_Strategy::template get< &detail::Scene_State::viewport>(); - render_domain.invoke( + render_domain->invoke( [this, x, y, delta_x, delta_y, modifiers, viewport] { backend->dispatch_wheel(x, y, delta_x, delta_y, modifiers, viewport); @@ -206,7 +206,7 @@ struct Basic_Point_Scene final } void dispatch_key(const ::renderive::Key_Event& event) override { - render_domain.invoke([this, event] { backend->dispatch_key(event); }); + render_domain->invoke([this, event] { backend->dispatch_key(event); }); this->notify_model_dirty(); } @@ -327,7 +327,7 @@ struct Basic_Point_Scene final auto async_frame = std::make_shared(); auto render_completion = std::make_shared< detail::Render_Domain::Prepared_Task>( - render_domain.prepare( + render_domain->prepare( [this, async_frame, metrics, diagnostics, source]() mutable { if (!async_frame->pending) { static_cast(source->fail( @@ -375,11 +375,11 @@ struct Basic_Point_Scene final })); auto completion = std::make_shared< detail::Gpu_Completion_Service::Reservation>( - gpu_completion.prepare( + detail::Gpu_Completion_Service::instance().prepare( [this, async_frame, render_completion]( Completion_Result result) noexcept { async_frame->completion = std::move(result); - render_domain.post(std::move(*render_completion)); + render_domain->post(std::move(*render_completion)); }, observe)); const auto queued_at = observe @@ -387,7 +387,7 @@ struct Basic_Point_Scene final : std::chrono::steady_clock::time_point{}; try { - render_domain.post( + render_domain->post( [this, scene_state, scene_revision, frame_sequence, prepared, source, async_frame, completion, observe, queued_at] { @@ -432,8 +432,7 @@ struct Basic_Point_Scene final std::shared_ptr visual; Renderable_Id point_id{}; - detail::Render_Domain render_domain; - detail::Gpu_Completion_Service gpu_completion; + std::shared_ptr render_domain; std::unique_ptr backend; mutable std::mutex frame_mutex; std::shared_ptr latest; diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp index a362499..8a1bbd1 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp @@ -1,15 +1,23 @@ #include "Gpu_Completion_Service.h" -#include +#include #include #include #include +#include namespace renderive::render_3d::detail { +Gpu_Completion_Service& Gpu_Completion_Service::instance() { + static Gpu_Completion_Service service; + return service; +} Gpu_Completion_Service::Gpu_Completion_Service() { pending_.set_capacity(default_capacity); thread_ = std::thread([this] { run(); }); } Gpu_Completion_Service::~Gpu_Completion_Service() { - shutdown(); + stopping_.store(true, std::memory_order_release); + wake(); + if (thread_.joinable()) + thread_.join(); } Gpu_Completion_Service::Reservation::Reservation(std::shared_ptr pending) noexcept : pending_(std::move(pending)) {} Gpu_Completion_Service::Reservation::~Reservation() { @@ -26,108 +34,131 @@ void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence) std::terminate(); pending->device = device; pending->fence = fence; + if (pending->observe) + pending->watched_at = std::chrono::steady_clock::now(); pending->status = Pending_Fence::Status::watched; } - pending->ready.notify_one(); + pending->service->wake(); } void Gpu_Completion_Service::Reservation::cancel() noexcept { if (!pending_) return; auto pending = std::exchange(pending_, {}); cancel_reserved(pending); + pending->service->wake(); } void Gpu_Completion_Service::cancel_reserved(const std::shared_ptr& pending) noexcept { if (!pending) return; - { - std::lock_guard lock(pending->mutex); - if (pending->status == Pending_Fence::Status::reserved) - pending->status = Pending_Fence::Status::canceled; - } - pending->ready.notify_one(); + std::lock_guard lock(pending->mutex); + if (pending->status == Pending_Fence::Status::reserved) + pending->status = Pending_Fence::Status::canceled; } Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(Completion completion, bool observe) { if (!completion) throw std::invalid_argument("GPU completion callback is empty"); + if (stopping_.load(std::memory_order_acquire)) + throw std::runtime_error("GPU completion service is stopping"); auto pending = std::make_shared(); pending->completion = std::move(completion); pending->observe = observe; - { - std::lock_guard lock(mutex_); - if (stopping_) - throw std::runtime_error("GPU completion service is stopping"); + pending->service = this; + slots_.acquire(); + try { pending_.push(pending); + } catch (...) { + slots_.release(); + throw; } + wake(); return Reservation(std::move(pending)); } -void Gpu_Completion_Service::shutdown() noexcept { - { - std::lock_guard lock(mutex_); - if (stopping_ && !thread_.joinable()) - return; - stopping_ = true; - cancel_reserved(active_); - std::shared_ptr pending; - while (pending_.try_pop(pending)) - cancel_reserved(pending); - pending_.push(std::shared_ptr{}); - } - if (thread_.joinable()) - thread_.join(); +void Gpu_Completion_Service::wake() noexcept { + wake_.notify_one(); } void Gpu_Completion_Service::run() noexcept { + std::vector> active; + active.reserve(static_cast(default_capacity)); for (;;) { - std::shared_ptr pending; - pending_.pop(pending); - if (!pending) - return; - { - std::lock_guard lock(mutex_); - if (stopping_) { - cancel_reserved(pending); + std::shared_ptr incoming; + while (pending_.try_pop(incoming)) + active.push_back(std::move(incoming)); + bool progressed{}; + for (auto iterator = active.begin(); iterator != active.end();) { + auto& pending = *iterator; + VkDevice device{VK_NULL_HANDLE}; + VkFence fence{VK_NULL_HANDLE}; + Completion completion; + std::chrono::steady_clock::time_point watched_at{}; + bool observe{}; + Pending_Fence::Status status; + { + std::lock_guard lock(pending->mutex); + status = pending->status; + if (status == Pending_Fence::Status::watched) { + device = pending->device; + fence = pending->fence; + watched_at = pending->watched_at; + observe = pending->observe; + } + } + if (status == Pending_Fence::Status::canceled) { + iterator = active.erase(iterator); + slots_.release(); + progressed = true; continue; } - active_ = pending; - } - VkDevice device{VK_NULL_HANDLE}; - VkFence fence{VK_NULL_HANDLE}; - Completion completion; - bool observe{}; - { - std::unique_lock lock(pending->mutex); - pending->ready.wait(lock, [&pending] { return pending->status != Pending_Fence::Status::reserved; }); - if (pending->status == Pending_Fence::Status::canceled) { - lock.unlock(); - std::lock_guard service_lock(mutex_); - active_.reset(); + if (status == Pending_Fence::Status::reserved) { + if (stopping_.load(std::memory_order_acquire)) { + cancel_reserved(pending); + iterator = active.erase(iterator); + slots_.release(); + progressed = true; + continue; + } + ++iterator; continue; } - device = pending->device; - fence = pending->fence; - completion = std::move(pending->completion); - observe = pending->observe; - } - const auto wait_started = observe ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - const VkResult result = vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX); - Result completion_result; - if (observe) { - const auto wait_duration = std::chrono::duration_cast(std::chrono::steady_clock::now() - wait_started).count(); - completion_result.wait_duration_ns = wait_duration > 0 ? static_cast(wait_duration) : 0; - } - if (result != VK_SUCCESS) { + const VkResult result = vkGetFenceStatus(device, fence); + if (result == VK_NOT_READY) { + ++iterator; + continue; + } + { + std::lock_guard lock(pending->mutex); + completion = std::move(pending->completion); + pending->status = Pending_Fence::Status::canceled; + } + Result completion_result; + if (observe) { + const auto duration = std::chrono::duration_cast(std::chrono::steady_clock::now() - watched_at).count(); + completion_result.wait_duration_ns = duration > 0 ? static_cast(duration) : 0; + } + if (result != VK_SUCCESS) { + try { + throw std::runtime_error("GPU fence wait failed with Vulkan result " + std::to_string(static_cast(result))); + } catch (...) { + completion_result.error = std::current_exception(); + } + } try { - throw std::runtime_error("GPU fence wait failed with Vulkan result " + std::to_string(static_cast(result))); + completion(std::move(completion_result)); } catch (...) { - completion_result.error = std::current_exception(); } + iterator = active.erase(iterator); + slots_.release(); + progressed = true; } - try { - completion(std::move(completion_result)); - } catch (...) { - } - { - std::lock_guard lock(mutex_); - active_.reset(); + if (stopping_.load(std::memory_order_acquire) && active.empty() && pending_.empty()) + return; + if (!progressed) { + std::unique_lock lock(wait_mutex_); + if (active.empty()) + wake_.wait(lock, [this] { + return stopping_.load(std::memory_order_acquire) || !pending_.empty(); + }); + else + wake_.wait_for(lock, poll_interval); } } } diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.h b/render_3D/render_3D/detail/Gpu_Completion_Service.h index 5d810bc..ff8f4b5 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.h +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.h @@ -1,11 +1,15 @@ #pragma once #include +#include +#include #include +#include #include #include #include #include #include +#include #include #include namespace renderive::render_3d::detail { @@ -32,12 +36,10 @@ public: std::shared_ptr pending_; friend class Gpu_Completion_Service; }; - Gpu_Completion_Service(); - ~Gpu_Completion_Service(); + static Gpu_Completion_Service& instance(); Gpu_Completion_Service(const Gpu_Completion_Service&) = delete; Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete; [[nodiscard]] Reservation prepare(Completion completion, bool observe); - void shutdown() noexcept; private: struct Pending_Fence { enum class Status { @@ -46,20 +48,26 @@ private: canceled }; std::mutex mutex; - std::condition_variable ready; VkDevice device{VK_NULL_HANDLE}; VkFence fence{VK_NULL_HANDLE}; Completion completion; + std::chrono::steady_clock::time_point watched_at{}; + Gpu_Completion_Service* service{}; Status status{Status::reserved}; bool observe{}; }; + Gpu_Completion_Service(); + ~Gpu_Completion_Service(); static void cancel_reserved(const std::shared_ptr& pending) noexcept; + void wake() noexcept; void run() noexcept; - static constexpr std::size_t default_capacity = 64; - std::mutex mutex_; + static constexpr std::ptrdiff_t default_capacity = 1024; + static constexpr auto poll_interval = std::chrono::microseconds(200); + std::counting_semaphore slots_{default_capacity}; oneapi::tbb::concurrent_bounded_queue> pending_; - std::shared_ptr active_; + std::mutex wait_mutex_; + std::condition_variable wake_; + std::atomic_bool stopping_{}; std::thread thread_; - bool stopping_{}; }; } diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp new file mode 100644 index 0000000..1531d18 --- /dev/null +++ b/render_3D/render_3D/detail/Render_Domain.cpp @@ -0,0 +1,81 @@ +#include "Render_Domain.h" +#include +namespace renderive::render_3d::detail { +namespace { +struct Render_Domain_Registry { + std::mutex mutex; + std::unordered_map> domains; +}; +Render_Domain_Registry& registry() { + static Render_Domain_Registry value; + return value; +} +} +Render_Domain::Prepared_Task::~Prepared_Task() { + release(); +} +Render_Domain::Prepared_Task::Prepared_Task(Prepared_Task&& other) noexcept + : domain_(std::exchange(other.domain_, nullptr)), task_(std::move(other.task_)) {} +Render_Domain::Prepared_Task& Render_Domain::Prepared_Task::operator=(Prepared_Task&& other) noexcept { + if (this == &other) + return *this; + release(); + domain_ = std::exchange(other.domain_, nullptr); + task_ = std::move(other.task_); + return *this; +} +void Render_Domain::Prepared_Task::release() noexcept { + if (!domain_) + return; + task_.reset(); + domain_->slots_.release(); + domain_ = nullptr; +} +std::shared_ptr Render_Domain::acquire(std::uint32_t gpu_index) { + auto& storage = registry(); + std::lock_guard lock(storage.mutex); + if (const auto found = storage.domains.find(gpu_index); found != storage.domains.end()) + return found->second; + auto domain = std::shared_ptr(new Render_Domain()); + storage.domains.emplace(gpu_index, domain); + return domain; +} +Render_Domain::Render_Domain() { + tasks_.set_capacity(default_capacity); + thread_ = std::thread([this] { run(); }); +} +Render_Domain::~Render_Domain() { + { + std::lock_guard lock(mutex_); + stopping_ = true; + } + slots_.acquire(); + if (!tasks_.try_push(std::unique_ptr{})) + std::terminate(); + if (thread_.joinable()) + thread_.join(); +} +void Render_Domain::post(std::function function) { + post(prepare(std::move(function))); +} +void Render_Domain::post(Prepared_Task task) noexcept { + if (!task.task_ || task.domain_ != this) + std::terminate(); + std::lock_guard lock(mutex_); + if (stopping_) + std::terminate(); + if (!tasks_.try_push(std::move(task.task_))) + std::terminate(); + task.domain_ = nullptr; +} +void Render_Domain::run() { + for (;;) { + std::unique_ptr task; + tasks_.pop(task); + slots_.release(); + if (!task) + return; + task->function(); + } +} +} diff --git a/render_3D/render_3D/detail/Render_Domain.h b/render_3D/render_3D/detail/Render_Domain.h index 0a48fb3..ea702f8 100644 --- a/render_3D/render_3D/detail/Render_Domain.h +++ b/render_3D/render_3D/detail/Render_Domain.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include #include +#include #include #include #include @@ -18,51 +20,36 @@ public: class Prepared_Task final { public: Prepared_Task() = default; - ~Prepared_Task() = default; + ~Prepared_Task(); Prepared_Task(const Prepared_Task&) = delete; Prepared_Task& operator=(const Prepared_Task&) = delete; - Prepared_Task(Prepared_Task&&) noexcept = default; - Prepared_Task& operator=(Prepared_Task&&) noexcept = default; + Prepared_Task(Prepared_Task&& other) noexcept; + Prepared_Task& operator=(Prepared_Task&& other) noexcept; private: - explicit Prepared_Task(std::unique_ptr task) noexcept : task_(std::move(task)) {} + Prepared_Task(Render_Domain* domain, std::unique_ptr task) noexcept + : domain_(domain), task_(std::move(task)) {} + void release() noexcept; + Render_Domain* domain_{}; std::unique_ptr task_; friend class Render_Domain; }; - Render_Domain() { - tasks_.set_capacity(default_capacity); - thread_ = std::thread([this] { run(); }); - } - ~Render_Domain() { - { - std::lock_guard lock(mutex_); - stopping_ = true; - tasks_.push(std::unique_ptr{}); - } - if (thread_.joinable()) - thread_.join(); - } + static std::shared_ptr acquire(std::uint32_t gpu_index); + ~Render_Domain(); Render_Domain(const Render_Domain&) = delete; Render_Domain& operator=(const Render_Domain&) = delete; [[nodiscard]] Prepared_Task prepare(std::function function) { if (!function) throw std::invalid_argument("render domain task is empty"); - return Prepared_Task(std::make_unique(std::move(function))); - } - void post(std::function function) { - auto task = prepare(std::move(function)); - std::lock_guard lock(mutex_); - if (stopping_) - throw std::runtime_error("Point_Scene render domain is stopping"); - tasks_.push(std::move(task.task_)); - } - void post(Prepared_Task task) noexcept { - if (!task.task_) - std::terminate(); - std::lock_guard lock(mutex_); - if (stopping_) - std::terminate(); - tasks_.push(std::move(task.task_)); + slots_.acquire(); + try { + return Prepared_Task(this, std::make_unique(std::move(function))); + } catch (...) { + slots_.release(); + throw; + } } + void post(std::function function); + void post(Prepared_Task task) noexcept; template auto invoke(Function&& function) -> std::invoke_result_t { using Result = std::invoke_result_t; @@ -75,16 +62,10 @@ public: return result.get(); } private: - void run() { - for (;;) { - std::unique_ptr task; - tasks_.pop(task); - if (!task) - return; - task->function(); - } - } - static constexpr std::size_t default_capacity = 64; + Render_Domain(); + void run(); + static constexpr std::ptrdiff_t default_capacity = 64; + std::counting_semaphore slots_{default_capacity}; oneapi::tbb::concurrent_bounded_queue> tasks_; std::mutex mutex_; bool stopping_{}; diff --git a/render_3D/tests/Gpu_Completion_Service_Tests.cpp b/render_3D/tests/Gpu_Completion_Service_Tests.cpp index 8d7dc20..8bcdad3 100644 --- a/render_3D/tests/Gpu_Completion_Service_Tests.cpp +++ b/render_3D/tests/Gpu_Completion_Service_Tests.cpp @@ -1,63 +1,48 @@ #include "render_3D/detail/Gpu_Completion_Service.h" #include "render_3D/detail/Render_Domain.h" - #include - #include -#include #include - namespace renderive::render_3d::detail { namespace { - -static_assert(noexcept( - std::declval().watch( - VK_NULL_HANDLE, VK_NULL_HANDLE))); -static_assert(noexcept( - std::declval().post( - std::declval()))); - -TEST(GpuCompletionService, AbandonedReservationCancelsBeforeFenceWait) { - Gpu_Completion_Service service; +static_assert(noexcept(std::declval().watch(VK_NULL_HANDLE, VK_NULL_HANDLE))); +static_assert(noexcept(std::declval().post(std::declval()))); +TEST(GpuCompletionService, UsesOneProcessWideService) { + EXPECT_EQ(&Gpu_Completion_Service::instance(), &Gpu_Completion_Service::instance()); +} +TEST(GpuCompletionService, AbandonedReservationDoesNotComplete) { std::atomic completion_count{}; { - auto reservation = service.prepare( + auto reservation = Gpu_Completion_Service::instance().prepare( [&](Gpu_Completion_Service::Result) { completion_count.fetch_add(1, std::memory_order_relaxed); - }, - false); + }, false); } - service.shutdown(); EXPECT_EQ(completion_count.load(std::memory_order_relaxed), 0); } - -TEST(GpuCompletionService, PrepareRejectsAStoppedServiceBeforeSubmission) { - Gpu_Completion_Service service; - service.shutdown(); - EXPECT_THROW({ - auto reservation = service.prepare( - [](Gpu_Completion_Service::Result) {}, false); - static_cast(reservation); - }, std::runtime_error); +TEST(RenderDomain, SharesDomainPerGpuIndex) { + auto first = Render_Domain::acquire(0); + auto second = Render_Domain::acquire(0); + auto other = Render_Domain::acquire(1); + EXPECT_EQ(first, second); + EXPECT_NE(first, other); } - -TEST(GpuCompletionService, ShutdownCancelsAnUnarmedReservation) { - Gpu_Completion_Service service; - auto reservation = service.prepare( - [](Gpu_Completion_Service::Result) {}, false); - service.shutdown(); -} - TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) { - Render_Domain domain; + auto domain = Render_Domain::acquire(0); std::atomic executed{}; - auto task = domain.prepare([&] { + auto task = domain->prepare([&] { executed.store(true, std::memory_order_release); }); - domain.post(std::move(task)); - domain.invoke([] {}); + domain->post(std::move(task)); + domain->invoke([] {}); EXPECT_TRUE(executed.load(std::memory_order_acquire)); } - -} // namespace -} // namespace renderive::render_3d::detail +TEST(RenderDomain, AbandonedPreparedTasksReleaseReservedCapacity) { + auto domain = Render_Domain::acquire(2); + for (std::size_t index = 0; index < 128; ++index) { + auto task = domain->prepare([] {}); + } + domain->invoke([] {}); +} +} +} diff --git a/webapp_gallery/src/inspector/performance_panel.tsx b/webapp_gallery/src/inspector/performance_panel.tsx index af79708..1ed8d65 100644 --- a/webapp_gallery/src/inspector/performance_panel.tsx +++ b/webapp_gallery/src/inspector/performance_panel.tsx @@ -1,2 +1,2 @@ import {Stack,Typography} from "@mui/material";import type {Gallery_Telemetry} from "../protocol/gallery_types";import {Json_Viewer} from "../common/json_viewer"; -export function Performance_Panel({telemetry}:{telemetry:Gallery_Telemetry}) {return 性能滑动窗口;} +export function Performance_Panel({telemetry}:{telemetry:Gallery_Telemetry}) {return 性能滑动窗口;} diff --git a/webapp_gallery/src/plot/performance_strip.tsx b/webapp_gallery/src/plot/performance_strip.tsx index 28524d0..83fcdd7 100644 --- a/webapp_gallery/src/plot/performance_strip.tsx +++ b/webapp_gallery/src/plot/performance_strip.tsx @@ -1,3 +1,3 @@ import {Box,Typography} from "@mui/material";import type {Gallery_Telemetry} from "../protocol/gallery_types";import {value_at_path} from "../protocol/format"; -const METRICS:Array<[string,string,string]>=[["后端 FPS","performance.measured_fps"," FPS"],["像素 FPS","performance.pixel_response_fps"," FPS"],["渲染","performance.last_render_ms"," ms"],["编码","performance.last_pixel_encode_ms"," ms"]]; +const METRICS:Array<[string,string,string]>=[["后端 FPS","performance.measured_fps"," FPS"],["像素 FPS","performance.pixel_response_fps"," FPS"],["帧完成延迟","performance.last_render_ms"," ms"],["编码","performance.last_pixel_encode_ms"," ms"]]; export function Performance_Strip({telemetry}:{telemetry:Gallery_Telemetry}) {return {METRICS.map(([label,path,suffix])=>{label}{Number(value_at_path(telemetry,path)||0).toFixed(1)}{suffix})};}