From 26bbc29a16e860c95ded2c5d8396bfebf48d07d2 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sat, 15 Aug 2026 19:58:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Kernel/src/renderive/capture/Capture.cpp | 20 +- Kernel/src/renderive/capture/Capture.hpp | 4 + .../renderive/render_graph/Render_Plan.cpp | 2 + .../renderive/render_graph/Render_Plan.hpp | 1 + .../src/renderive/scene/base/Scene_Base.cpp | 49 +++- .../src/renderive/scene/base/Scene_Base.hpp | 10 + Kernel/src/renderive/scheduling/Scheduler.hpp | 2 + .../scheduling/detail/OneTBB_Runtime.cpp | 3 + .../render_graph/Render_DAG_Test.cpp | 48 +++- Qt/CMakeLists.txt | 11 +- render_2D/CMakeLists.txt | 3 +- render_3D/CMakeLists.txt | 21 +- render_3D/datoviz/CMakeLists.txt | 17 +- render_3D/datoviz/cmake/DatovizTests.cmake | 2 +- render_3D/render_3D/Point_Scene.cpp | 17 ++ render_3D/render_3D/Point_Scene.h | 16 ++ .../detail/Gpu_Completion_Service.cpp | 85 +++++-- .../render_3D/detail/Gpu_Completion_Service.h | 22 +- render_3D/render_3D/detail/Render_Domain.cpp | 86 +++++-- render_3D/render_3D/detail/Render_Domain.h | 55 +++-- .../tests/Gpu_Completion_Service_Tests.cpp | 37 ++- render_3D/tests/Render_Domain_Tests.cpp | 47 ++++ web_server/app/Gallery_Capture_Json.h | 10 +- web_server/app/Gallery_Plot_Session.cpp | 216 +++++++++++------- web_server/app/Gallery_Plot_Session.h | 2 +- web_server/app/render_2D/Gallery_Scene2D.cpp | 8 + web_server/app/render_3D/Gallery_Scene3D.cpp | 25 ++ .../src/inspector/performance_panel.tsx | 2 +- webapp_gallery/src/protocol/gallery_types.ts | 2 +- 29 files changed, 622 insertions(+), 201 deletions(-) create mode 100644 render_3D/tests/Render_Domain_Tests.cpp diff --git a/Kernel/src/renderive/capture/Capture.cpp b/Kernel/src/renderive/capture/Capture.cpp index a0502a3..fb3f41d 100644 --- a/Kernel/src/renderive/capture/Capture.cpp +++ b/Kernel/src/renderive/capture/Capture.cpp @@ -85,18 +85,27 @@ void Capture_Repository::begin_session(Capture_Session_Id session_id, return session.session_id == session_id; })) throw std::invalid_argument("capture session already exists"); + while (sessions_.size() >= retained_session_count) { + const auto completed = std::find_if(sessions_.begin(), sessions_.end(), + [](const Capture_Session& session) { return !session.active(); }); + if (completed == sessions_.end()) + throw std::logic_error("capture repository retention is occupied by active sessions"); + sessions_.erase(completed); + } sessions_.push_back({session_id, requested_count, {}}); sessions_.back().frames.reserve(requested_count); } void Capture_Repository::publish(Capture_Session_Id session_id, std::shared_ptr snapshot, + std::shared_ptr render_plan, Frame_Analysis analysis) { - if (!snapshot) - throw std::invalid_argument("captured frame snapshot is null"); + if (!snapshot || !render_plan) + throw std::invalid_argument("captured frame snapshot or render plan is null"); if (analysis.frame_id != snapshot->frame_id || - analysis.render_plan_version != snapshot->render_plan_version) - throw std::invalid_argument("captured frame analysis does not match snapshot"); + analysis.render_plan_version != snapshot->render_plan_version || + render_plan->version != snapshot->render_plan_version) + throw std::invalid_argument("captured frame analysis does not match snapshot or render plan"); std::lock_guard lock(mutex_); const auto iterator = std::find_if(sessions_.begin(), sessions_.end(), [session_id](const auto& session) { return session.session_id == session_id; }); @@ -104,7 +113,8 @@ void Capture_Repository::publish(Capture_Session_Id session_id, throw std::invalid_argument("capture session does not exist"); if (!iterator->active()) throw std::logic_error("capture session is complete"); - iterator->frames.push_back({std::move(snapshot), std::move(analysis)}); + iterator->frames.push_back({std::move(snapshot), std::move(render_plan), + std::move(analysis)}); } std::optional Capture_Repository::session( diff --git a/Kernel/src/renderive/capture/Capture.hpp b/Kernel/src/renderive/capture/Capture.hpp index 49393fd..ee4eca9 100644 --- a/Kernel/src/renderive/capture/Capture.hpp +++ b/Kernel/src/renderive/capture/Capture.hpp @@ -8,6 +8,7 @@ #include #include "Capture_Types.hpp" #include "renderive/render_graph/Frame_Analysis.hpp" +#include "renderive/render_graph/Render_Plan.hpp" class Capture_Controller { public: @@ -39,6 +40,7 @@ private: struct Captured_Frame { std::shared_ptr snapshot; + std::shared_ptr render_plan; Frame_Analysis analysis; }; @@ -59,10 +61,12 @@ public: void begin_session(Capture_Session_Id session_id, std::size_t requested_count); void publish(Capture_Session_Id session_id, std::shared_ptr snapshot, + std::shared_ptr render_plan, Frame_Analysis analysis); [[nodiscard]] std::optional session(Capture_Session_Id session_id) const; [[nodiscard]] std::vector sessions() const; private: + static constexpr std::size_t retained_session_count = 16; mutable std::mutex mutex_; std::vector sessions_; }; diff --git a/Kernel/src/renderive/render_graph/Render_Plan.cpp b/Kernel/src/renderive/render_graph/Render_Plan.cpp index 2d279b8..4d7d859 100644 --- a/Kernel/src/renderive/render_graph/Render_Plan.cpp +++ b/Kernel/src/renderive/render_graph/Render_Plan.cpp @@ -112,6 +112,8 @@ std::shared_ptr Render_Plan_History::publish(Render_Graph gra plan->version = next_version_++; plan->graph = std::move(graph); plans_.push_back(plan); + if (plans_.size() > retained_plan_count) + plans_.erase(plans_.begin()); return plan; } diff --git a/Kernel/src/renderive/render_graph/Render_Plan.hpp b/Kernel/src/renderive/render_graph/Render_Plan.hpp index db7f499..d60cddb 100644 --- a/Kernel/src/renderive/render_graph/Render_Plan.hpp +++ b/Kernel/src/renderive/render_graph/Render_Plan.hpp @@ -66,6 +66,7 @@ public: [[nodiscard]] std::shared_ptr current() const; [[nodiscard]] std::shared_ptr find(Render_Plan_Version version) const; private: + static constexpr std::size_t retained_plan_count = 64; mutable std::mutex mutex_; std::vector> plans_; 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 31bf81c..c0a118b 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -1,11 +1,13 @@ #include "Scene_Base.hpp" #include +#include #include #include #include #include #include +#include #include #include #include @@ -38,7 +40,19 @@ public: wait(); } void submit(Operation operation) { - operations_.push(std::move(operation)); + const auto started = std::chrono::steady_clock::now(); + if (!slots_.try_acquire()) { + backpressure_count_.fetch_add(1, std::memory_order_relaxed); + slots_.acquire(); + const auto waited = std::chrono::duration_cast( + std::chrono::steady_clock::now() - started).count(); + if (waited > 0) + backpressure_wait_ns_.fetch_add(static_cast(waited), std::memory_order_relaxed); + } + 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))) + std::terminate(); schedule(); } void wait() noexcept { @@ -48,7 +62,21 @@ public: } catch (...) { } } + [[nodiscard]] Scene_Execution_Statistics statistics() const noexcept { + return { + static_cast(default_capacity), + 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) + }; + } private: + static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept { + std::size_t current = peak.load(std::memory_order_relaxed); + while (current < value && !peak.compare_exchange_weak( + current, value, std::memory_order_relaxed)) {} + } void schedule() { if (scheduled_.exchange(true, std::memory_order_acq_rel)) return; @@ -57,19 +85,26 @@ private: } void execute_one() { Operation operation; - if (operations_.try_pop(operation)) + if (operations_.try_pop(operation)) { + queued_.fetch_sub(1, std::memory_order_relaxed); + slots_.release(); operation(); + } scheduled_.store(false, std::memory_order_release); if (!operations_.empty()) schedule(); } - static constexpr std::size_t default_capacity = 64; + static constexpr std::ptrdiff_t default_capacity = 64; + std::counting_semaphore slots_{default_capacity}; oneapi::tbb::flow::graph graph_; oneapi::tbb::concurrent_bounded_queue operations_; oneapi::tbb::flow::function_node node_; + std::atomic_size_t queued_{}; + std::atomic_size_t peak_queued_{}; + std::atomic_uint64_t backpressure_count_{}; + std::atomic_uint64_t backpressure_wait_ns_{}; std::atomic_bool scheduled_{}; }; - class Scene_Base::Render_Execution_Scope { public: explicit Render_Execution_Scope(Scene_Base& scene) noexcept @@ -799,6 +834,10 @@ std::vector Scene_Base::plan_version_statistics( return analyze_plan_versions(analyses); } +Scene_Execution_Statistics Scene_Base::execution_statistics() const noexcept { + return execution_context_->statistics(); +} + std::pmr::memory_resource& Scene_Base::memory_resource() const noexcept { return memory_domain_->resource(); } @@ -1273,7 +1312,7 @@ void Scene_Base::execute_render_graph(Render_Task& task) { } if (completed) { capture_repository_.publish( - capture_ticket.session_id, completed, + capture_ticket.session_id, completed, compiled.plan, analyze_frame(*compiled.plan, *completed)); } capture_controller_.finish_frame(capture_ticket, true); diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index 6b61779..6c5a22d 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,14 @@ public: const Scene_Render_Context& context) = 0; }; +struct Scene_Execution_Statistics { + std::size_t capacity{}; + std::size_t queued{}; + std::size_t peak_queued{}; + std::uint64_t backpressure_count{}; + std::uint64_t backpressure_wait_ns{}; +}; + class Scene_Base { private: struct Render_Completion; @@ -171,6 +180,7 @@ public: Capture_Session_Id session_id) const; [[nodiscard]] std::vector plan_version_statistics( Capture_Session_Id session_id) const; + [[nodiscard]] Scene_Execution_Statistics execution_statistics() const noexcept; [[nodiscard]] std::pmr::memory_resource& memory_resource() const noexcept; [[nodiscard]] std::pmr::memory_resource& upstream_memory_resource() const noexcept; diff --git a/Kernel/src/renderive/scheduling/Scheduler.hpp b/Kernel/src/renderive/scheduling/Scheduler.hpp index c6d4d78..0a15907 100644 --- a/Kernel/src/renderive/scheduling/Scheduler.hpp +++ b/Kernel/src/renderive/scheduling/Scheduler.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include namespace renderive::scheduling { struct Scheduler_Statistics { std::size_t concurrency{}; @@ -15,4 +16,5 @@ struct Scheduler_Statistics { }; [[nodiscard]] std::size_t scheduler_concurrency() noexcept; [[nodiscard]] Scheduler_Statistics scheduler_statistics() noexcept; +void enqueue_task(std::function task); } diff --git a/Kernel/src/renderive/scheduling/detail/OneTBB_Runtime.cpp b/Kernel/src/renderive/scheduling/detail/OneTBB_Runtime.cpp index 24b13e2..449d40c 100644 --- a/Kernel/src/renderive/scheduling/detail/OneTBB_Runtime.cpp +++ b/Kernel/src/renderive/scheduling/detail/OneTBB_Runtime.cpp @@ -95,4 +95,7 @@ std::size_t scheduler_concurrency() noexcept { Scheduler_Statistics scheduler_statistics() noexcept { return detail::OneTBB_Runtime::instance().statistics(); } +void enqueue_task(std::function task) { + detail::OneTBB_Runtime::instance().enqueue(std::move(task)); +} } diff --git a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp index 67e2cd2..d405fc7 100644 --- a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp @@ -138,7 +138,7 @@ TEST(render_dag_test, capture_controller_and_repository_capture_exact_requested_ execution->worker_id = static_cast(slot); execution->status = Node_Execution_Status::complete; } - repository.publish(session_id, snapshot, analyze_frame(*plan, *snapshot)); + repository.publish(session_id, snapshot, plan, analyze_frame(*plan, *snapshot)); } const auto session = repository.session(session_id); @@ -232,3 +232,49 @@ TEST(render_dag_test, concurrent_workers_reserve_exactly_one_capture_slot_each) EXPECT_EQ(captured.load(std::memory_order_relaxed), requested); EXPECT_FALSE(controller.state().enabled()); } + + +TEST(render_dag_test, render_plan_history_has_bounded_retention) { + Render_Plan_History history; + std::vector versions; + for (std::uint64_t index = 0; index < 70; ++index) { + Render_Graph_Builder builder; + builder.emplace(1000 + index, 2000 + index, "Node", Render_Node_Kind::prepare); + versions.push_back(history.publish(std::move(builder).finish())->version); + } + EXPECT_FALSE(history.find(versions.front())); + EXPECT_TRUE(history.find(versions.back())); + ASSERT_TRUE(history.current()); + EXPECT_EQ(history.current()->version, versions.back()); +} + +TEST(render_dag_test, capture_repository_retains_only_recent_completed_sessions) { + Capture_Repository repository; + Render_Graph_Builder builder; + builder.emplace(3000, 4000, "Node", Render_Node_Kind::prepare); + Render_Plan_History history; + const auto plan = history.publish(std::move(builder).finish()); + for (Capture_Session_Id session_id = 1; session_id <= 20; ++session_id) { + repository.begin_session(session_id, 1); + auto snapshot = std::make_shared(); + snapshot->frame_id = session_id; + snapshot->render_plan_version = plan->version; + snapshot->render_start_ns = session_id * 100; + snapshot->render_end_ns = snapshot->render_start_ns + 10; + snapshot->node_executions.resize(1); + auto& execution = snapshot->node_executions.front(); + execution.node_id = plan->graph.nodes.front().node_id; + execution.ready_time_ns = snapshot->render_start_ns; + execution.start_time_ns = snapshot->render_start_ns; + execution.cpu_end_time_ns = snapshot->render_end_ns; + execution.end_time_ns = snapshot->render_end_ns; + execution.status = Node_Execution_Status::complete; + repository.publish(session_id, snapshot, plan, analyze_frame(*plan, *snapshot)); + } + const auto sessions = repository.sessions(); + ASSERT_EQ(sessions.size(), 16U); + EXPECT_EQ(sessions.front(), 5U); + EXPECT_EQ(sessions.back(), 20U); + EXPECT_FALSE(repository.session(1)); + EXPECT_TRUE(repository.session(20)); +} diff --git a/Qt/CMakeLists.txt b/Qt/CMakeLists.txt index 75e297a..bd30fe2 100644 --- a/Qt/CMakeLists.txt +++ b/Qt/CMakeLists.txt @@ -21,9 +21,14 @@ if (NOT TARGET Renderive_render_2D) rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_Qt requires Renderive_render_2D") return() endif () -set(Renderive_Qt_source_dir "${CMAKE_CURRENT_LIST_DIR}") -append_glob_source(Renderive_Qt_sources "${Renderive_Qt_source_dir}") -list(FILTER Renderive_Qt_sources EXCLUDE REGEX "/tests/") +set(Renderive_Qt_source_dirs + "${CMAKE_CURRENT_LIST_DIR}/bridge" + "${CMAKE_CURRENT_LIST_DIR}/plot" +) +set(Renderive_Qt_sources) +foreach (Renderive_Qt_source_dir IN LISTS Renderive_Qt_source_dirs) + append_glob_source(Renderive_Qt_sources "${Renderive_Qt_source_dir}") +endforeach () add_library(Renderive_Qt STATIC ${Renderive_Qt_sources}) set_target_properties(Renderive_Qt PROPERTIES AUTOMOC ON) target_include_directories(Renderive_Qt PUBLIC diff --git a/render_2D/CMakeLists.txt b/render_2D/CMakeLists.txt index ded4ad4..8e74eec 100644 --- a/render_2D/CMakeLists.txt +++ b/render_2D/CMakeLists.txt @@ -19,9 +19,8 @@ if (NOT TARGET Renderive_Kernel) rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_render_2D requires Renderive_Kernel") return() endif () -set(Renderive_render_2D_source_dir "${CMAKE_CURRENT_LIST_DIR}") +set(Renderive_render_2D_source_dir "${CMAKE_CURRENT_LIST_DIR}/render_2D") append_glob_source(Renderive_render_2D_sources "${Renderive_render_2D_source_dir}") -list(FILTER Renderive_render_2D_sources EXCLUDE REGEX "/tests/") add_library(Renderive_render_2D STATIC ${Renderive_render_2D_sources}) target_include_directories(Renderive_render_2D PUBLIC "$" diff --git a/render_3D/CMakeLists.txt b/render_3D/CMakeLists.txt index a277db7..0b3c7d2 100644 --- a/render_3D/CMakeLists.txt +++ b/render_3D/CMakeLists.txt @@ -1,7 +1,6 @@ # 加载 datoviz的依赖库 include(${CMAKE_CURRENT_LIST_DIR}/cmake/export.cmake) set(Renderive_render_3D_dependencies - render_3D::PThreads4W render_3D::cglm render_3D::Vulkan-Hpp render_3D::VulkanMemoryAllocator @@ -13,6 +12,9 @@ set(Renderive_render_3D_dependencies render_3D::tinyxml2 global::zlib ) +if (WIN32) + list(APPEND Renderive_render_3D_dependencies render_3D::PThreads4W) +endif () if (RENDERIVE_BUILD_TESTS) list(APPEND Renderive_render_3D_dependencies global::GTest) endif () @@ -25,10 +27,13 @@ if (Renderive_render_3D_missing) endif () rcl_load_dependency_environment(${Renderive_render_3D_dependencies}) find_package(Vulkan REQUIRED) -find_package(PThreads4W QUIET) -if (NOT PThreads4W_FOUND) - rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_render_3D PThreads4W installation is incomplete. Rebuild render_3D::PThreads4W") - return() +find_package(Threads REQUIRED) +if (WIN32) + find_package(PThreads4W QUIET) + if (NOT PThreads4W_FOUND) + rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_render_3D PThreads4W installation is incomplete. Rebuild render_3D::PThreads4W") + return() + endif () endif () find_package(cglm CONFIG REQUIRED) find_package(volk CONFIG REQUIRED) @@ -89,10 +94,8 @@ if (NOT TARGET Renderive_Kernel) return() endif () add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/datoviz" "${CMAKE_CURRENT_BINARY_DIR}/datoviz") -set(Renderive_render_3D_source_dir "${CMAKE_CURRENT_LIST_DIR}") +set(Renderive_render_3D_source_dir "${CMAKE_CURRENT_LIST_DIR}/render_3D") append_glob_source(Renderive_render_3D_sources "${Renderive_render_3D_source_dir}") -list(FILTER Renderive_render_3D_sources EXCLUDE REGEX "/tests/") -list(FILTER Renderive_render_3D_sources EXCLUDE REGEX "/datoviz/") add_library(Renderive_render_3D STATIC ${Renderive_render_3D_sources}) target_include_directories(Renderive_render_3D PUBLIC "$" @@ -100,7 +103,7 @@ target_include_directories(Renderive_render_3D PUBLIC target_compile_features(Renderive_render_3D PUBLIC cxx_std_20) target_link_libraries(Renderive_render_3D PUBLIC Renderive_Kernel - PRIVATE ${Renderive_render_3D_datoviz_targets} tinyobjloader::tinyobjloader + PRIVATE ${Renderive_render_3D_datoviz_targets} tinyobjloader::tinyobjloader Threads::Threads ) if (MSVC) # Datoviz is embedded from OBJECT libraries in this static archive. Its Windows headers diff --git a/render_3D/datoviz/CMakeLists.txt b/render_3D/datoviz/CMakeLists.txt index 355e5e8..e8b2cbb 100644 --- a/render_3D/datoviz/CMakeLists.txt +++ b/render_3D/datoviz/CMakeLists.txt @@ -138,11 +138,20 @@ if(TARGET Vulkan::Headers) else() set(Renderive_render_3D_datoviz_vulkan_target Vulkan::Vulkan) endif() -# CORE/common:Datoviz 通用基础设施,线程实现沿用当前 PThreads4W 环境。 +if(WIN32) + if(NOT TARGET PThreads4W::PThreads4W) + message(FATAL_ERROR "Datoviz requires PThreads4W::PThreads4W on Windows") + endif() + set(Renderive_render_3D_datoviz_thread_target PThreads4W::PThreads4W) +else() + find_package(Threads REQUIRED) + set(Renderive_render_3D_datoviz_thread_target Threads::Threads) +endif() +# CORE/common:Datoviz 通用基础设施使用平台线程运行库。 Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_common_sources "${Renderive_render_3D_datoviz_src_dir}/common") add_library(Renderive_render_3D_datoviz_common OBJECT ${Renderive_render_3D_datoviz_common_sources}) target_include_directories(Renderive_render_3D_datoviz_common PUBLIC "${Renderive_render_3D_datoviz_include_dir}" INTERFACE "${Renderive_render_3D_datoviz_src_dir}/common") -target_link_libraries(Renderive_render_3D_datoviz_common PUBLIC PThreads4W::PThreads4W) +target_link_libraries(Renderive_render_3D_datoviz_common PUBLIC ${Renderive_render_3D_datoviz_thread_target}) Renderive_render_3D_datoviz_configure_target(Renderive_render_3D_datoviz_common) # CORE/fileio:保留 Datoviz fileio + fpng 的边界,压缩统一使用工程已经安装的 ZLIB。 Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_fileio_sources "${Renderive_render_3D_datoviz_src_dir}/fileio") @@ -162,13 +171,13 @@ Renderive_render_3D_datoviz_configure_target(Renderive_render_3D_datoviz_geom) Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_math_sources "${Renderive_render_3D_datoviz_src_dir}/math") add_library(Renderive_render_3D_datoviz_math OBJECT ${Renderive_render_3D_datoviz_math_sources}) target_include_directories(Renderive_render_3D_datoviz_math PUBLIC "${Renderive_render_3D_datoviz_include_dir}" "${Renderive_render_3D_datoviz_src_dir}/common") -target_link_libraries(Renderive_render_3D_datoviz_math PUBLIC cglm::cglm PThreads4W::PThreads4W) +target_link_libraries(Renderive_render_3D_datoviz_math PUBLIC cglm::cglm ${Renderive_render_3D_datoviz_thread_target}) Renderive_render_3D_datoviz_configure_target(Renderive_render_3D_datoviz_math) # CORE/thread:线程模块只依赖线程运行库,不引入 Datoviz 其他 runtime。 Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_thread_sources "${Renderive_render_3D_datoviz_src_dir}/thread") add_library(Renderive_render_3D_datoviz_thread OBJECT ${Renderive_render_3D_datoviz_thread_sources}) target_include_directories(Renderive_render_3D_datoviz_thread PUBLIC "${Renderive_render_3D_datoviz_include_dir}" "${Renderive_render_3D_datoviz_src_dir}/common") -target_link_libraries(Renderive_render_3D_datoviz_thread PUBLIC PThreads4W::PThreads4W) +target_link_libraries(Renderive_render_3D_datoviz_thread PUBLIC ${Renderive_render_3D_datoviz_thread_target}) Renderive_render_3D_datoviz_configure_target(Renderive_render_3D_datoviz_thread) # CONTROLLER/input:先保留 Datoviz 输入数据结构,不接 window/glfw。 Renderive_render_3D_datoviz_collect_sources(Renderive_render_3D_datoviz_input_sources "${Renderive_render_3D_datoviz_src_dir}/input") diff --git a/render_3D/datoviz/cmake/DatovizTests.cmake b/render_3D/datoviz/cmake/DatovizTests.cmake index 515f527..98cdb98 100644 --- a/render_3D/datoviz/cmake/DatovizTests.cmake +++ b/render_3D/datoviz/cmake/DatovizTests.cmake @@ -53,7 +53,7 @@ function(Renderive_render_3D_datoviz_add_test_runner target source_var component target_link_libraries(${target} PRIVATE ${ARGN} Renderive_render_3D_datoviz_testing - PThreads4W::PThreads4W) + ${Renderive_render_3D_datoviz_thread_target}) if(Renderive_render_3D_datoviz_msvc_system_library_dirs) target_link_directories(${target} PRIVATE ${Renderive_render_3D_datoviz_msvc_system_library_dirs}) diff --git a/render_3D/render_3D/Point_Scene.cpp b/render_3D/render_3D/Point_Scene.cpp index 758219c..e9db3de 100644 --- a/render_3D/render_3D/Point_Scene.cpp +++ b/render_3D/render_3D/Point_Scene.cpp @@ -106,6 +106,7 @@ struct Scene_Model { [[nodiscard]] virtual std::shared_ptr latest_frame() const = 0; [[nodiscard]] virtual Frame_Status frame_status() const = 0; + [[nodiscard]] virtual Runtime_Statistics runtime_statistics() const noexcept = 0; [[nodiscard]] virtual Scene_Base& scene() noexcept = 0; [[nodiscard]] virtual const Scene_Base& scene() const noexcept = 0; }; @@ -295,6 +296,18 @@ struct Basic_Point_Scene final return result; } + [[nodiscard]] Runtime_Statistics runtime_statistics() const noexcept override { + const auto render = render_domain->statistics(); + const auto completion = detail::Gpu_Completion_Service::instance().statistics(); + return { + {render.capacity, render.admitted, render.peak_admitted, render.queued, + render.peak_queued, render.backpressure_count, render.backpressure_wait_ns}, + {completion.capacity, completion.in_flight, completion.peak_in_flight, + completion.watched, completion.peak_watched, completion.backpressure_count, + completion.backpressure_wait_ns} + }; + } + [[nodiscard]] Scene_Base& scene() noexcept override { return *this; } [[nodiscard]] const Scene_Base& scene() const noexcept override { return *this; @@ -544,6 +557,10 @@ Frame_Status Point_Scene::frame_status() const { return impl_->model->frame_status(); } +Runtime_Statistics Point_Scene::runtime_statistics() const noexcept { + return impl_->model->runtime_statistics(); +} + Scene_Base& Point_Scene::render_scene() noexcept { return impl_->model->scene(); } diff --git a/render_3D/render_3D/Point_Scene.h b/render_3D/render_3D/Point_Scene.h index efe4b3d..70ce3a9 100644 --- a/render_3D/render_3D/Point_Scene.h +++ b/render_3D/render_3D/Point_Scene.h @@ -68,6 +68,21 @@ struct Pixel_Frame { std::vector rgba8; }; +struct Runtime_Admission_Statistics { + std::size_t capacity{}; + std::size_t active{}; + std::size_t peak_active{}; + std::size_t queued{}; + std::size_t peak_queued{}; + std::uint64_t backpressure_count{}; + std::uint64_t backpressure_wait_ns{}; +}; + +struct Runtime_Statistics { + Runtime_Admission_Statistics render_domain; + Runtime_Admission_Statistics gpu_completion; +}; + struct Frame_Status { Frame_Mode mode{Frame_Mode::Low_Latency}; double frequency_hz{}; @@ -123,6 +138,7 @@ public: [[nodiscard]] std::shared_ptr latest_frame() const; [[nodiscard]] Frame_Status frame_status() const; + [[nodiscard]] Runtime_Statistics runtime_statistics() const noexcept; [[nodiscard]] Scene_Base& render_scene() noexcept; [[nodiscard]] const Scene_Base& render_scene() const noexcept; diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp index 8a1bbd1..54fa137 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 #include #include #include @@ -28,6 +27,7 @@ void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence) if (!pending_ || device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE) std::terminate(); auto pending = std::exchange(pending_, {}); + auto* const service = pending->service; { std::lock_guard lock(pending->mutex); if (pending->status != Pending_Fence::Status::reserved) @@ -36,9 +36,11 @@ void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence) pending->fence = fence; if (pending->observe) pending->watched_at = std::chrono::steady_clock::now(); + const std::size_t watched = service->watched_.fetch_add(1, std::memory_order_relaxed) + 1; + update_peak(service->peak_watched_, watched); pending->status = Pending_Fence::Status::watched; } - pending->service->wake(); + service->wake(); } void Gpu_Completion_Service::Reservation::cancel() noexcept { if (!pending_) @@ -47,6 +49,10 @@ void Gpu_Completion_Service::Reservation::cancel() noexcept { cancel_reserved(pending); pending->service->wake(); } +void Gpu_Completion_Service::update_peak(std::atomic_size_t& peak, std::size_t value) noexcept { + std::size_t current = peak.load(std::memory_order_relaxed); + while (current < value && !peak.compare_exchange_weak(current, value, std::memory_order_relaxed)) {} +} void Gpu_Completion_Service::cancel_reserved(const std::shared_ptr& pending) noexcept { if (!pending) return; @@ -54,6 +60,23 @@ void Gpu_Completion_Service::cancel_reserved(const std::shared_ptrstatus == Pending_Fence::Status::reserved) pending->status = Pending_Fence::Status::canceled; } +void Gpu_Completion_Service::acquire_slot() { + const auto started = std::chrono::steady_clock::now(); + if (!slots_.try_acquire()) { + backpressure_count_.fetch_add(1, std::memory_order_relaxed); + slots_.acquire(); + const auto waited = std::chrono::duration_cast( + std::chrono::steady_clock::now() - started).count(); + if (waited > 0) + backpressure_wait_ns_.fetch_add(static_cast(waited), std::memory_order_relaxed); + } + const std::size_t in_flight = in_flight_.fetch_add(1, std::memory_order_relaxed) + 1; + update_peak(peak_in_flight_, in_flight); +} +void Gpu_Completion_Service::release_slot() noexcept { + in_flight_.fetch_sub(1, std::memory_order_relaxed); + slots_.release(); +} Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(Completion completion, bool observe) { if (!completion) throw std::invalid_argument("GPU completion callback is empty"); @@ -63,27 +86,39 @@ Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(Completion c pending->completion = std::move(completion); pending->observe = observe; pending->service = this; - slots_.acquire(); - try { - pending_.push(pending); - } catch (...) { - slots_.release(); - throw; + acquire_slot(); + if (!pending_.try_push(pending)) { + release_slot(); + throw std::logic_error("GPU completion admission invariant violated"); } wake(); return Reservation(std::move(pending)); } +Gpu_Completion_Service::Statistics Gpu_Completion_Service::statistics() const noexcept { + return { + static_cast(default_capacity), + in_flight_.load(std::memory_order_relaxed), + peak_in_flight_.load(std::memory_order_relaxed), + watched_.load(std::memory_order_relaxed), + peak_watched_.load(std::memory_order_relaxed), + backpressure_count_.load(std::memory_order_relaxed), + backpressure_wait_ns_.load(std::memory_order_relaxed) + }; +} void Gpu_Completion_Service::wake() noexcept { - wake_.notify_one(); + wake_generation_.fetch_add(1, std::memory_order_release); + wake_condition_.notify_one(); } void Gpu_Completion_Service::run() noexcept { std::vector> active; active.reserve(static_cast(default_capacity)); for (;;) { + const std::uint64_t wake_generation = wake_generation_.load(std::memory_order_acquire); std::shared_ptr incoming; while (pending_.try_pop(incoming)) active.push_back(std::move(incoming)); bool progressed{}; + bool has_watched{}; for (auto iterator = active.begin(); iterator != active.end();) { auto& pending = *iterator; VkDevice device{VK_NULL_HANDLE}; @@ -104,7 +139,7 @@ void Gpu_Completion_Service::run() noexcept { } if (status == Pending_Fence::Status::canceled) { iterator = active.erase(iterator); - slots_.release(); + release_slot(); progressed = true; continue; } @@ -112,13 +147,14 @@ void Gpu_Completion_Service::run() noexcept { if (stopping_.load(std::memory_order_acquire)) { cancel_reserved(pending); iterator = active.erase(iterator); - slots_.release(); + release_slot(); progressed = true; continue; } ++iterator; continue; } + has_watched = true; const VkResult result = vkGetFenceStatus(device, fence); if (result == VK_NOT_READY) { ++iterator; @@ -129,9 +165,11 @@ void Gpu_Completion_Service::run() noexcept { completion = std::move(pending->completion); pending->status = Pending_Fence::Status::canceled; } + watched_.fetch_sub(1, std::memory_order_relaxed); Result completion_result; if (observe) { - const auto duration = std::chrono::duration_cast(std::chrono::steady_clock::now() - watched_at).count(); + 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) { @@ -146,19 +184,24 @@ void Gpu_Completion_Service::run() noexcept { } catch (...) { } iterator = active.erase(iterator); - slots_.release(); + release_slot(); progressed = true; } 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); + if (progressed) + continue; + std::unique_lock lock(wait_mutex_); + if (wake_generation_.load(std::memory_order_acquire) != wake_generation) + continue; + if (has_watched) { + wake_condition_.wait_for(lock, poll_interval, [this, wake_generation] { + return wake_generation_.load(std::memory_order_acquire) != wake_generation; + }); + } else { + wake_condition_.wait(lock, [this, wake_generation] { + return wake_generation_.load(std::memory_order_acquire) != wake_generation; + }); } } } diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.h b/render_3D/render_3D/detail/Gpu_Completion_Service.h index ff8f4b5..f4dd684 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.h +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.h @@ -20,6 +20,15 @@ public: std::exception_ptr error; std::uint64_t wait_duration_ns{}; }; + struct Statistics { + std::size_t capacity{}; + std::size_t in_flight{}; + std::size_t peak_in_flight{}; + std::size_t watched{}; + std::size_t peak_watched{}; + std::uint64_t backpressure_count{}; + std::uint64_t backpressure_wait_ns{}; + }; using Completion = std::function; class Reservation final { public: @@ -40,6 +49,7 @@ public: Gpu_Completion_Service(const Gpu_Completion_Service&) = delete; Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete; [[nodiscard]] Reservation prepare(Completion completion, bool observe); + [[nodiscard]] Statistics statistics() const noexcept; private: struct Pending_Fence { enum class Status { @@ -58,7 +68,10 @@ private: }; Gpu_Completion_Service(); ~Gpu_Completion_Service(); + static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept; static void cancel_reserved(const std::shared_ptr& pending) noexcept; + void acquire_slot(); + void release_slot() noexcept; void wake() noexcept; void run() noexcept; static constexpr std::ptrdiff_t default_capacity = 1024; @@ -66,7 +79,14 @@ private: std::counting_semaphore slots_{default_capacity}; oneapi::tbb::concurrent_bounded_queue> pending_; std::mutex wait_mutex_; - std::condition_variable wake_; + std::condition_variable wake_condition_; + std::atomic_uint64_t wake_generation_{}; + std::atomic_size_t in_flight_{}; + std::atomic_size_t peak_in_flight_{}; + std::atomic_size_t watched_{}; + std::atomic_size_t peak_watched_{}; + std::atomic_uint64_t backpressure_count_{}; + std::atomic_uint64_t backpressure_wait_ns_{}; std::atomic_bool stopping_{}; std::thread thread_; }; diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp index 1531d18..cca51be 100644 --- a/render_3D/render_3D/detail/Render_Domain.cpp +++ b/render_3D/render_3D/detail/Render_Domain.cpp @@ -1,10 +1,11 @@ #include "Render_Domain.h" +#include #include namespace renderive::render_3d::detail { namespace { struct Render_Domain_Registry { std::mutex mutex; - std::unordered_map> domains; + std::unordered_map> domains; }; Render_Domain_Registry& registry() { static Render_Domain_Registry value; @@ -15,12 +16,12 @@ 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_)) {} + : domain_(std::move(other.domain_)), 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); + domain_ = std::move(other.domain_); task_ = std::move(other.task_); return *this; } @@ -28,16 +29,17 @@ void Render_Domain::Prepared_Task::release() noexcept { if (!domain_) return; task_.reset(); - domain_->slots_.release(); - domain_ = nullptr; + domain_->release_admission(); + domain_.reset(); } 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& entry = storage.domains[gpu_index]; + if (auto domain = entry.lock()) + return domain; auto domain = std::shared_ptr(new Render_Domain()); - storage.domains.emplace(gpu_index, domain); + entry = domain; return domain; } Render_Domain::Render_Domain() { @@ -45,36 +47,82 @@ Render_Domain::Render_Domain() { thread_ = std::thread([this] { run(); }); } Render_Domain::~Render_Domain() { - { - std::lock_guard lock(mutex_); - stopping_ = true; - } + stopping_.store(true, std::memory_order_release); slots_.acquire(); if (!tasks_.try_push(std::unique_ptr{})) std::terminate(); if (thread_.joinable()) thread_.join(); } +void Render_Domain::update_peak(std::atomic_size_t& peak, std::size_t value) noexcept { + std::size_t current = peak.load(std::memory_order_relaxed); + while (current < value && !peak.compare_exchange_weak(current, value, std::memory_order_relaxed)) {} +} +void Render_Domain::acquire_admission() { + const auto started = std::chrono::steady_clock::now(); + if (!slots_.try_acquire()) { + backpressure_count_.fetch_add(1, std::memory_order_relaxed); + slots_.acquire(); + const auto waited = std::chrono::duration_cast( + std::chrono::steady_clock::now() - started).count(); + if (waited > 0) + backpressure_wait_ns_.fetch_add(static_cast(waited), std::memory_order_relaxed); + } + const std::size_t admitted = admitted_.fetch_add(1, std::memory_order_relaxed) + 1; + update_peak(peak_admitted_, admitted); +} +void Render_Domain::release_admission() noexcept { + admitted_.fetch_sub(1, std::memory_order_relaxed); + slots_.release(); +} +Render_Domain::Prepared_Task Render_Domain::prepare(std::function function) { + if (!function) + throw std::invalid_argument("render domain task is empty"); + if (stopping_.load(std::memory_order_acquire)) + throw std::runtime_error("render domain is stopping"); + acquire_admission(); + try { + return Prepared_Task(shared_from_this(), std::make_unique(std::move(function))); + } catch (...) { + release_admission(); + throw; + } +} 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_) + if (!task.task_ || task.domain_.get() != this || stopping_.load(std::memory_order_acquire)) std::terminate(); if (!tasks_.try_push(std::move(task.task_))) std::terminate(); - task.domain_ = nullptr; + const std::size_t queued = queued_.fetch_add(1, std::memory_order_relaxed) + 1; + update_peak(peak_queued_, queued); + task.domain_.reset(); +} +Render_Domain::Statistics Render_Domain::statistics() const noexcept { + return { + static_cast(default_capacity), + admitted_.load(std::memory_order_relaxed), + peak_admitted_.load(std::memory_order_relaxed), + 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) + }; } void Render_Domain::run() { + current_domain_ = this; for (;;) { std::unique_ptr task; tasks_.pop(task); - slots_.release(); - if (!task) + if (!task) { + slots_.release(); + current_domain_ = nullptr; return; + } + queued_.fetch_sub(1, std::memory_order_relaxed); + release_admission(); task->function(); } } diff --git a/render_3D/render_3D/detail/Render_Domain.h b/render_3D/render_3D/detail/Render_Domain.h index ea702f8..0ea15cb 100644 --- a/render_3D/render_3D/detail/Render_Domain.h +++ b/render_3D/render_3D/detail/Render_Domain.h @@ -1,9 +1,11 @@ #pragma once +#include +#include +#include #include #include #include #include -#include #include #include #include @@ -11,12 +13,21 @@ #include #include namespace renderive::render_3d::detail { -class Render_Domain final { +class Render_Domain final : public std::enable_shared_from_this { struct Task { explicit Task(std::function value) : function(std::move(value)) {} std::function function; }; public: + struct Statistics { + std::size_t capacity{}; + std::size_t admitted{}; + std::size_t peak_admitted{}; + std::size_t queued{}; + std::size_t peak_queued{}; + std::uint64_t backpressure_count{}; + std::uint64_t backpressure_wait_ns{}; + }; class Prepared_Task final { public: Prepared_Task() = default; @@ -26,10 +37,10 @@ public: Prepared_Task(Prepared_Task&& other) noexcept; Prepared_Task& operator=(Prepared_Task&& other) noexcept; private: - Prepared_Task(Render_Domain* domain, std::unique_ptr task) noexcept - : domain_(domain), task_(std::move(task)) {} + Prepared_Task(std::shared_ptr domain, std::unique_ptr task) noexcept + : domain_(std::move(domain)), task_(std::move(task)) {} void release() noexcept; - Render_Domain* domain_{}; + std::shared_ptr domain_; std::unique_ptr task_; friend class Render_Domain; }; @@ -37,22 +48,20 @@ public: ~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"); - slots_.acquire(); - try { - return Prepared_Task(this, std::make_unique(std::move(function))); - } catch (...) { - slots_.release(); - throw; - } - } + [[nodiscard]] Prepared_Task prepare(std::function function); 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; + if (current_domain_ == this) { + if constexpr (std::is_void_v) { + std::invoke(std::forward(function)); + return; + } else { + return std::invoke(std::forward(function)); + } + } auto task = std::make_shared>(std::forward(function)); auto result = task->get_future(); post([task] { (*task)(); }); @@ -61,14 +70,24 @@ public: else return result.get(); } + [[nodiscard]] Statistics statistics() const noexcept; private: Render_Domain(); + static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept; + void acquire_admission(); + void release_admission() noexcept; void run(); static constexpr std::ptrdiff_t default_capacity = 64; + inline static thread_local Render_Domain* current_domain_{}; std::counting_semaphore slots_{default_capacity}; oneapi::tbb::concurrent_bounded_queue> tasks_; - std::mutex mutex_; - bool stopping_{}; + std::atomic_size_t admitted_{}; + std::atomic_size_t peak_admitted_{}; + std::atomic_size_t queued_{}; + std::atomic_size_t peak_queued_{}; + std::atomic_uint64_t backpressure_count_{}; + std::atomic_uint64_t backpressure_wait_ns_{}; + std::atomic_bool stopping_{}; std::thread thread_; }; } diff --git a/render_3D/tests/Gpu_Completion_Service_Tests.cpp b/render_3D/tests/Gpu_Completion_Service_Tests.cpp index 8bcdad3..beb0fc9 100644 --- a/render_3D/tests/Gpu_Completion_Service_Tests.cpp +++ b/render_3D/tests/Gpu_Completion_Service_Tests.cpp @@ -1,12 +1,12 @@ #include "render_3D/detail/Gpu_Completion_Service.h" -#include "render_3D/detail/Render_Domain.h" #include #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, UsesOneProcessWideService) { EXPECT_EQ(&Gpu_Completion_Service::instance(), &Gpu_Completion_Service::instance()); } @@ -20,29 +20,18 @@ TEST(GpuCompletionService, AbandonedReservationDoesNotComplete) { } EXPECT_EQ(completion_count.load(std::memory_order_relaxed), 0); } -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(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) { - auto domain = Render_Domain::acquire(0); - std::atomic executed{}; - auto task = domain->prepare([&] { - executed.store(true, std::memory_order_release); - }); - domain->post(std::move(task)); - domain->invoke([] {}); - EXPECT_TRUE(executed.load(std::memory_order_acquire)); -} -TEST(RenderDomain, AbandonedPreparedTasksReleaseReservedCapacity) { - auto domain = Render_Domain::acquire(2); - for (std::size_t index = 0; index < 128; ++index) { - auto task = domain->prepare([] {}); +TEST(GpuCompletionService, CanceledReservationWakesIdleService) { + auto& service = Gpu_Completion_Service::instance(); + const auto baseline = service.statistics().in_flight; + { + auto reservation = service.prepare([](Gpu_Completion_Service::Result) {}, false); + EXPECT_EQ(service.statistics().in_flight, baseline + 1); } - domain->invoke([] {}); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); + while (service.statistics().in_flight != baseline && + std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + EXPECT_EQ(service.statistics().in_flight, baseline); } } } diff --git a/render_3D/tests/Render_Domain_Tests.cpp b/render_3D/tests/Render_Domain_Tests.cpp new file mode 100644 index 0000000..cdd53d7 --- /dev/null +++ b/render_3D/tests/Render_Domain_Tests.cpp @@ -0,0 +1,47 @@ +#include "render_3D/detail/Render_Domain.h" +#include +#include +#include +namespace renderive::render_3d::detail { +namespace { +static_assert(noexcept(std::declval().post(std::declval()))); +TEST(RenderDomain, SharesDomainPerGpuIndex) { + auto first = Render_Domain::acquire(0x7ffffffcU); + auto second = Render_Domain::acquire(0x7ffffffcU); + auto other = Render_Domain::acquire(0x7ffffffbU); + EXPECT_EQ(first, second); + EXPECT_NE(first, other); +} +TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) { + auto domain = Render_Domain::acquire(0x7ffffffaU); + std::atomic executed{}; + auto task = domain->prepare([&] { + executed.store(true, std::memory_order_release); + }); + domain->post(std::move(task)); + domain->invoke([] {}); + EXPECT_TRUE(executed.load(std::memory_order_acquire)); +} +TEST(RenderDomain, AbandonedPreparedTasksReleaseReservedCapacity) { + auto domain = Render_Domain::acquire(0x7ffffff9U); + for (std::size_t index = 0; index < 128; ++index) { + auto task = domain->prepare([] {}); + } + domain->invoke([] {}); +} +TEST(RenderDomain, NestedInvokeExecutesInlineOnTheAffinityThread) { + auto domain = Render_Domain::acquire(0x7ffffff8U); + const int value = domain->invoke([domain] { + return domain->invoke([] { return 42; }); + }); + EXPECT_EQ(value, 42); +} +TEST(RenderDomain, ReportsBoundedAdmissionStatistics) { + auto domain = Render_Domain::acquire(0x7ffffff7U); + const auto statistics = domain->statistics(); + EXPECT_EQ(statistics.capacity, 64U); + EXPECT_TRUE(statistics.admitted <= statistics.capacity); + EXPECT_TRUE(statistics.queued <= statistics.capacity); +} +} +} diff --git a/web_server/app/Gallery_Capture_Json.h b/web_server/app/Gallery_Capture_Json.h index c284e58..ef26726 100644 --- a/web_server/app/Gallery_Capture_Json.h +++ b/web_server/app/Gallery_Capture_Json.h @@ -348,7 +348,7 @@ inline nlohmann::json gallery_performance_capture_json(const Scene_Base& scene) using namespace gallery_capture_detail; const auto controller = scene.capture_state(); Json sessions = Json::array(); - std::set referenced_plans; + std::map> referenced_plans; for (Capture_Session_Id id : scene.capture_sessions()) { const auto session = scene.capture_session(id); if (!session) @@ -356,7 +356,7 @@ inline nlohmann::json gallery_performance_capture_json(const Scene_Base& scene) Json frames = Json::array(); for (const auto& frame : session->frames) { frames.push_back(frame_json(frame)); - referenced_plans.insert(frame.snapshot->render_plan_version); + referenced_plans[frame.snapshot->render_plan_version] = frame.render_plan; } Json node_statistics = Json::array(); for (const auto& node : scene.node_statistics(id)) @@ -376,11 +376,11 @@ inline nlohmann::json gallery_performance_capture_json(const Scene_Base& scene) }); } if (const auto current = scene.render_plan_snapshot()) - referenced_plans.insert(current->version); + referenced_plans[current->version] = current; Json plans = Json::array(); std::shared_ptr previous; - for (Render_Plan_Version version : referenced_plans) { - const auto plan = scene.find_render_plan(version); + for (const auto& [version, captured_plan] : referenced_plans) { + auto plan = captured_plan ? captured_plan : scene.find_render_plan(version); if (!plan) continue; Json value = plan_json(scene, *plan); diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index b0933d2..6d5bb2f 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -3,6 +3,7 @@ #include "common/Gallery_Scene_Interface.h" #include "render_2D/Gallery_Scene2D.h" #include "render_3D/Gallery_Scene3D.h" +#include #include #include #include @@ -10,12 +11,13 @@ #include #include #include -#include +#include #include #include #include #include #include +#include #include #include namespace renderive::web { @@ -123,26 +125,84 @@ std::unique_ptr make_gallery_scene(std::uint64_t sessio return make_gallery_scene_2d(session_id, std::move(case_id), mode, automatic_low_latency); } } -struct Gallery_Plot_Session::Impl { +struct Gallery_Plot_Session::Impl : std::enable_shared_from_this { + using Clock = std::chrono::steady_clock; + class Automatic_Render_Scheduler final { + public: + static Automatic_Render_Scheduler& instance() { + static Automatic_Render_Scheduler scheduler; + return scheduler; + } + void arm(std::uint64_t session_id, std::weak_ptr session, Clock::time_point deadline) { + std::lock_guard lock(mutex_); + if (const auto current = entries_.find(session_id); current != entries_.end()) { + schedule_.erase(current->second); + entries_.erase(current); + } + const auto iterator = schedule_.emplace(deadline, Entry{session_id, std::move(session)}); + entries_.emplace(session_id, iterator); + condition_.notify_one(); + } + void disarm(std::uint64_t session_id) noexcept { + std::lock_guard lock(mutex_); + const auto current = entries_.find(session_id); + if (current == entries_.end()) + return; + schedule_.erase(current->second); + entries_.erase(current); + condition_.notify_one(); + } + private: + struct Entry { + std::uint64_t session_id{}; + std::weak_ptr session; + }; + using Schedule = std::multimap; + Automatic_Render_Scheduler() : thread_([this](std::stop_token stop) { run(stop); }) {} + ~Automatic_Render_Scheduler() { + thread_.request_stop(); + condition_.notify_all(); + } + void run(std::stop_token stop) noexcept { + std::unique_lock lock(mutex_); + while (!stop.stop_requested()) { + if (schedule_.empty()) { + condition_.wait(lock, [this, &stop] { + return stop.stop_requested() || !schedule_.empty(); + }); + continue; + } + const auto deadline = schedule_.begin()->first; + if (condition_.wait_until(lock, deadline) != std::cv_status::timeout) + continue; + if (schedule_.empty() || schedule_.begin()->first > Clock::now()) + continue; + auto iterator = schedule_.begin(); + Entry entry = std::move(iterator->second); + entries_.erase(entry.session_id); + schedule_.erase(iterator); + lock.unlock(); + if (auto session = entry.session.lock()) + session->automatic_render_due(); + lock.lock(); + } + } + std::mutex mutex_; + std::condition_variable condition_; + Schedule schedule_; + std::unordered_map entries_; + std::jthread thread_; + }; explicit Impl(bool enable_automatic_low_latency) - : automatic_low_latency(enable_automatic_low_latency), - session_id(next_session_id()) {} + : automatic_low_latency(enable_automatic_low_latency), session_id(next_session_id()) {} ~Impl() { - render_worker.request_stop(); - scheduler_condition.notify_all(); + if (automatic_low_latency) + Automatic_Render_Scheduler::instance().disarm(session_id); } static std::uint64_t next_session_id() noexcept { static std::atomic next{1}; return next.fetch_add(1, std::memory_order_relaxed); } - void ensure_render_worker() { - if (!automatic_low_latency || render_worker.joinable()) - return; - render_worker = std::jthread( - [this](std::stop_token stop) { - run_automatic_renderer(stop); - }); - } bool update_client_metrics(std::string_view message) { if (!scene) return false; @@ -204,57 +264,59 @@ struct Gallery_Plot_Session::Impl { return true; } [[nodiscard]] std::unique_lock acquire_foreground_lock() { - foreground_waiters.fetch_add(1, std::memory_order_release); - scheduler_condition.notify_all(); - std::unique_lock lock(mutex); - foreground_waiters.fetch_sub(1, std::memory_order_release); - scheduler_condition.notify_all(); - return lock; + return std::unique_lock(mutex); } - void run_automatic_renderer(std::stop_token stop) { - using Clock = std::chrono::steady_clock; - std::unique_lock lock(mutex); - std::optional last_render_started; - std::uint64_t active_generation = std::numeric_limits::max(); - while (!stop.stop_requested()) { - scheduler_condition.wait(lock, [this, &stop] { - return stop.stop_requested() || (scene && scene->can_render_automatically()); + void arm_automatic_render_locked() noexcept { + if (!automatic_low_latency) + return; + auto& scheduler = Automatic_Render_Scheduler::instance(); + if (!scene || !scene->can_render_automatically() || render_task_pending) { + scheduler.disarm(session_id); + 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(); + scheduler.arm(session_id, weak_from_this(), deadline); + } catch (...) { + scheduler.disarm(session_id); + } + } + void automatic_render_due() { + std::uint64_t generation{}; + { + std::lock_guard lock(mutex); + if (!automatic_low_latency || !scene || !scene->can_render_automatically() || render_task_pending) + return; + render_task_pending = true; + generation = scene_generation; + } + auto self = shared_from_this(); + try { + renderive::scheduling::enqueue_task([self = std::move(self), generation] { + self->run_automatic_render(generation); }); - if (stop.stop_requested()) - break; - if (active_generation != scene_generation) { - active_generation = scene_generation; - last_render_started.reset(); + } catch (...) { + std::lock_guard lock(mutex); + render_task_pending = false; + arm_automatic_render_locked(); + } + } + void run_automatic_render(std::uint64_t generation) noexcept { + const auto started = Clock::now(); + try { + std::lock_guard lock(mutex); + if (generation == scene_generation && scene && scene->can_render_automatically()) { + (void)scene->render_latest_frame(); + last_render_started = started; } - if (foreground_waiters.load(std::memory_order_acquire) != 0) { - scheduler_condition.wait(lock, [this, &stop] { - return stop.stop_requested() || foreground_waiters.load(std::memory_order_acquire) == 0; - }); - continue; - } - const auto revision = scheduler_revision; - const auto interval = std::chrono::nanoseconds(std::max(1, scene->kernel_refresh_interval_ns())); - const auto now = Clock::now(); - const auto deadline = last_render_started ? *last_render_started + interval : now; - if (now < deadline) { - const auto spin_window = std::min(interval / 4, std::chrono::duration_cast(std::chrono::microseconds(250))); - const auto coarse_deadline = deadline > now + spin_window ? deadline - spin_window : now; - if (now < coarse_deadline) { - scheduler_condition.wait_until(lock, coarse_deadline, [this, &stop, revision, active_generation] { - return stop.stop_requested() || scheduler_revision != revision || scene_generation != active_generation || - foreground_waiters.load(std::memory_order_acquire) != 0 || !scene || !scene->can_render_automatically(); - }); - continue; - } - lock.unlock(); - while (!stop.stop_requested() && Clock::now() < deadline) - std::this_thread::yield(); - lock.lock(); - continue; - } - const auto started = Clock::now(); - (void)scene->render_latest_frame(); - 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(); } } static bool affects_render_schedule(const Web_Event& event) { @@ -278,13 +340,11 @@ struct Gallery_Plot_Session::Impl { } std::unique_ptr scene; std::mutex mutex; - std::condition_variable scheduler_condition; - std::uint64_t scheduler_revision{}; + std::optional last_render_started; std::uint64_t scene_generation{}; bool automatic_low_latency{}; + bool render_task_pending{}; std::uint64_t session_id{}; - std::atomic foreground_waiters{}; - std::jthread render_worker; std::optional handle_gallery(const Gallery_Request& request) { if (request.kind == Gallery_Request_Kind::Catalog) return Web_Response{Web_Response_Type::Json, Gallery_Protocol::catalog_json()}; @@ -297,8 +357,8 @@ struct Gallery_Plot_Session::Impl { }; scene = make_gallery_scene(session_id, open->case_id, open->frame_mode, automatic_low_latency); ++scene_generation; - if (scene->frame_mode() == Gallery_Frame_Mode::Low_Latency) - ensure_render_worker(); + last_render_started.reset(); + arm_automatic_render_locked(); return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::case_json_from_controls(scene->case_id(), scene->controls().dump(), @@ -313,10 +373,8 @@ struct Gallery_Plot_Session::Impl { Gallery_Protocol::error_json("请先发送 gallery_open") }; if (request.kind == Gallery_Request_Kind::Observe) { - if (update_client_metrics(request.message)) { - ++scheduler_revision; - scheduler_condition.notify_all(); - } + if (update_client_metrics(request.message)) + arm_automatic_render_locked(); return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::observer_json( @@ -324,10 +382,8 @@ struct Gallery_Plot_Session::Impl { }; } if (request.kind == Gallery_Request_Kind::Refresh) { - if (update_client_metrics(request.message)) { - ++scheduler_revision; - scheduler_condition.notify_all(); - } + if (update_client_metrics(request.message)) + arm_automatic_render_locked(); return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::case_json_from_controls( @@ -382,6 +438,8 @@ struct Gallery_Plot_Session::Impl { const auto mode = scene->frame_mode(); scene = make_gallery_scene(session_id, id, mode, automatic_low_latency); ++scene_generation; + last_render_started.reset(); + arm_automatic_render_locked(); return Web_Response{ Web_Response_Type::Json, Gallery_Protocol::case_json_from_controls(id, scene->controls().dump(), @@ -455,16 +513,14 @@ struct Gallery_Plot_Session::Impl { }, event); if (reschedule) - ++scheduler_revision; + arm_automatic_render_locked(); } - if (reschedule) - scheduler_condition.notify_all(); return response; } }; Gallery_Plot_Session::Gallery_Plot_Session() : Gallery_Plot_Session(false) {} Gallery_Plot_Session::Gallery_Plot_Session(bool automatic_low_latency) - : impl_(std::make_unique(automatic_low_latency)) {} + : impl_(std::make_shared(automatic_low_latency)) {} Gallery_Plot_Session::~Gallery_Plot_Session() = default; std::optional Gallery_Plot_Session::handle(const Web_Event& event) { return impl_->handle(event); diff --git a/web_server/app/Gallery_Plot_Session.h b/web_server/app/Gallery_Plot_Session.h index 6bf2678..8fc58c6 100644 --- a/web_server/app/Gallery_Plot_Session.h +++ b/web_server/app/Gallery_Plot_Session.h @@ -19,7 +19,7 @@ public: private: struct Impl; - std::unique_ptr impl_; + std::shared_ptr impl_; }; } // namespace renderive::web diff --git a/web_server/app/render_2D/Gallery_Scene2D.cpp b/web_server/app/render_2D/Gallery_Scene2D.cpp index fc2099d..bce71fc 100644 --- a/web_server/app/render_2D/Gallery_Scene2D.cpp +++ b/web_server/app/render_2D/Gallery_Scene2D.cpp @@ -630,6 +630,7 @@ public: render_performance.automatic_low_latency_scheduler = automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency; const Gallery_Client_Performance client_performance = client_performance_; + const auto scene_execution = plot_.render_scene().execution_statistics(); nlohmann::json observer_data = adminive::to_frontend_json(observer); nlohmann::json consumer_feedback_data = @@ -656,6 +657,13 @@ public: {"client_performance", std::move(client_performance_data)}, {"scheduler", adminive::to_frontend_json( renderive::scheduling::scheduler_statistics())}, + {"queue_pressure", + {{"scene_execution", + {{"capacity", scene_execution.capacity}, + {"queued", scene_execution.queued}, + {"peak_queued", scene_execution.peak_queued}, + {"backpressure_count", scene_execution.backpressure_count}, + {"backpressure_wait_ns", scene_execution.backpressure_wait_ns}}}}}, {"renderable_observers", controls_.observers(plot_.render_scene())}, {"performance_capture", gallery_performance_capture_json(plot_.render_scene())}, {"last_action_result", last_action_result_} diff --git a/web_server/app/render_3D/Gallery_Scene3D.cpp b/web_server/app/render_3D/Gallery_Scene3D.cpp index b65cd07..4f94097 100644 --- a/web_server/app/render_3D/Gallery_Scene3D.cpp +++ b/web_server/app/render_3D/Gallery_Scene3D.cpp @@ -440,6 +440,8 @@ public: [[nodiscard]] std::string telemetry_json() const override { const auto status = scene_.frame_status(); const auto now = std::chrono::steady_clock::now(); + const auto scene_execution = scene_.render_scene().execution_statistics(); + const auto runtime = scene_.runtime_statistics(); return nlohmann::json{ {"kernel_observer", {{"mode", mode_id(frame_mode_)}, @@ -464,6 +466,29 @@ public: client_performance_.websocket_buffered_bytes}}}, {"scheduler", adminive::to_frontend_json( renderive::scheduling::scheduler_statistics())}, + {"queue_pressure", + {{"scene_execution", + {{"capacity", scene_execution.capacity}, + {"queued", scene_execution.queued}, + {"peak_queued", scene_execution.peak_queued}, + {"backpressure_count", scene_execution.backpressure_count}, + {"backpressure_wait_ns", scene_execution.backpressure_wait_ns}}}, + {"render_domain", + {{"capacity", runtime.render_domain.capacity}, + {"active", runtime.render_domain.active}, + {"peak_active", runtime.render_domain.peak_active}, + {"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}}}, + {"gpu_completion", + {{"capacity", runtime.gpu_completion.capacity}, + {"active", runtime.gpu_completion.active}, + {"peak_active", runtime.gpu_completion.peak_active}, + {"queued", runtime.gpu_completion.queued}, + {"peak_queued", runtime.gpu_completion.peak_queued}, + {"backpressure_count", runtime.gpu_completion.backpressure_count}, + {"backpressure_wait_ns", runtime.gpu_completion.backpressure_wait_ns}}}}}, {"session_id", session_id_}, {"performance_capture", gallery_performance_capture_json(scene_.render_scene())}, diff --git a/webapp_gallery/src/inspector/performance_panel.tsx b/webapp_gallery/src/inspector/performance_panel.tsx index 1ed8d65..c87b15b 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/protocol/gallery_types.ts b/webapp_gallery/src/protocol/gallery_types.ts index 01f8db1..d38f12d 100644 --- a/webapp_gallery/src/protocol/gallery_types.ts +++ b/webapp_gallery/src/protocol/gallery_types.ts @@ -26,7 +26,7 @@ export interface Gallery_Performance extends Record {} export interface Gallery_Kernel_Observer extends Record {} export interface Gallery_Scheduler_Statistics {concurrency: number; active_workers: number; peak_workers: number; active_external_threads: number; peak_external_threads: number; worker_entry_count: number; worker_exit_count: number; external_entry_count: number; external_exit_count: number;} export interface Gallery_Renderable_Observer extends Gallery_Resource {} -export interface Gallery_Telemetry {[key: string]: unknown; scheduler?: Gallery_Scheduler_Statistics; renderable_observers?: Gallery_Renderable_Observer[]; performance_capture?: Gallery_Performance_Capture;} +export interface Gallery_Telemetry {[key: string]: unknown; scheduler?: Gallery_Scheduler_Statistics; queue_pressure?: Record; renderable_observers?: Gallery_Renderable_Observer[]; performance_capture?: Gallery_Performance_Capture;} export interface Gallery_Render_Node {node_id: number; id?: string | number; name: string; label?: string; owner: string; kind: string;} export interface Gallery_Render_Edge {from: number | string; to: number | string;}