diff --git a/Kernel/CMakeLists.txt b/Kernel/CMakeLists.txt index de22386..11c1a23 100644 --- a/Kernel/CMakeLists.txt +++ b/Kernel/CMakeLists.txt @@ -49,7 +49,7 @@ install(TARGETS Renderive_Kernel RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") install(DIRECTORY "${Renderive_Kernel_source_dir}/renderive" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" - FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp") + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.inl") if (RENDERIVE_BUILD_TESTS) set(Renderive_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") append_glob_source(Renderive_Kernel_test_sources "${Renderive_Kernel_test_dir}") diff --git a/Kernel/src/renderive/render_graph/Frame_Analysis.cpp b/Kernel/src/renderive/render_graph/Frame_Analysis.cpp index 4e1cab0..c2b1b59 100644 --- a/Kernel/src/renderive/render_graph/Frame_Analysis.cpp +++ b/Kernel/src/renderive/render_graph/Frame_Analysis.cpp @@ -224,7 +224,10 @@ Frame_Analysis analyze_frame(const Render_Plan& plan, const Frame_Snapshot& fram } auto& analysis = result.nodes[index]; const auto& execution = frame.node_executions[index]; - analysis.dependency_ready_time_ns = execution.ready_time_ns; + analysis.dependency_ready_offset_ns = + execution.ready_time_ns >= frame.render_start_ns + ? execution.ready_time_ns - frame.render_start_ns + : 0; analysis.scheduler_wait_ns = execution.start_time_ns >= execution.ready_time_ns ? execution.start_time_ns - execution.ready_time_ns : 0; diff --git a/Kernel/src/renderive/render_graph/Frame_Analysis.hpp b/Kernel/src/renderive/render_graph/Frame_Analysis.hpp index 26e4d7e..1f3219e 100644 --- a/Kernel/src/renderive/render_graph/Frame_Analysis.hpp +++ b/Kernel/src/renderive/render_graph/Frame_Analysis.hpp @@ -23,7 +23,7 @@ struct Node_Frame_Analysis { std::uint64_t gpu_execution_duration_ns{}; std::uint64_t start_offset_ns{}; std::uint64_t end_offset_ns{}; - std::uint64_t dependency_ready_time_ns{}; + std::uint64_t dependency_ready_offset_ns{}; std::uint64_t scheduler_wait_ns{}; double work_contribution{}; double critical_path_contribution{}; diff --git a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp index dbae8d2..9fa1827 100644 --- a/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp +++ b/Kernel/src/renderive/render_graph/detail/Render_Graph_Runtime.cpp @@ -64,8 +64,17 @@ struct Render_Graph_Runtime::State } void execute(std::span execution_slots, Execute_Node execute) { - if (running.exchange(true, std::memory_order_acq_rel)) - throw std::logic_error("render graph runtime is already executing"); + { + std::lock_guard lock(lifecycle_mutex); + if (running.load(std::memory_order_acquire)) + throw std::logic_error("render graph runtime is already executing"); + cancellation_requested.store(false, std::memory_order_release); + { + std::lock_guard cancellation_lock(cancellation_mutex); + cancellation_exception = nullptr; + } + running.store(true, std::memory_order_release); + } std::exception_ptr execution_error; try { @@ -119,23 +128,31 @@ struct Render_Graph_Runtime::State std::fill(external_operations.begin(), external_operations.end(), External_Operation{}); } - cancellation_requested.store(false, std::memory_order_release); { - std::lock_guard lock(cancellation_mutex); - cancellation_exception = nullptr; + std::lock_guard lock(lifecycle_mutex); + cancellation_requested.store(false, std::memory_order_release); + { + std::lock_guard cancellation_lock(cancellation_mutex); + cancellation_exception = nullptr; + } + running.store(false, std::memory_order_release); } - running.store(false, std::memory_order_release); } void cancel_pending(std::exception_ptr reason) noexcept { auto error = make_cancellation_error(std::move(reason)); { - std::lock_guard lock(cancellation_mutex); - if (!cancellation_exception) - cancellation_exception = error; - else - error = cancellation_exception; + std::lock_guard lifecycle_lock(lifecycle_mutex); + if (!running.load(std::memory_order_acquire)) + return; + { + std::lock_guard cancellation_lock(cancellation_mutex); + if (!cancellation_exception) + cancellation_exception = error; + else + error = cancellation_exception; + } + cancellation_requested.store(true, std::memory_order_release); } - cancellation_requested.store(true, std::memory_order_release); cancel_external_operations(error); } std::exception_ptr cancellation_error() noexcept { @@ -291,6 +308,7 @@ struct Render_Graph_Runtime::State std::vector external_operations; std::mutex cancellation_mutex; std::exception_ptr cancellation_exception; + std::mutex lifecycle_mutex; std::atomic_bool running{}; std::atomic_bool failed{}; std::atomic_bool cancellation_requested{}; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index bff40a6..3bf38dd 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -196,9 +196,11 @@ void Scene_Base::Renderable_Editor::attach(Renderable renderable) { } if (!scene_.dependency_resolver_.contains(id)) scene_.dependency_resolver_.attach(id); - if (auto* composition = scene_.composition_relationships()) - composition->attach(id); - auto cache = scene_.make_renderable_color_cache(); + if (scene_.raster_capabilities_) + scene_.raster_capabilities_->attach(id); + auto cache = scene_.raster_capabilities_ + ? scene_.raster_capabilities_->make_renderable_color_cache() + : std::shared_ptr{}; if (!scene_.renderables_.try_emplace(id, renderable).second) throw std::logic_error("renderable id is already attached"); try { @@ -275,7 +277,9 @@ Scene_2D_Base::Scene_2D_Base(std::pmr::memory_resource& memory_resource) Scene_2D_Base::Scene_2D_Base(std::pmr::memory_resource& memory_resource, std::unique_ptr impl) : Scene_Base(memory_resource, std::move(impl)), - display_resolver_(this->memory_resource()) {} + display_resolver_(this->memory_resource()) { + bind_raster_capabilities(*this); +} Scene_2D_Base::Attach_Builder Scene_2D_Base::attach_builder() { return Attach_Builder(*this); @@ -512,8 +516,8 @@ void Scene_Base::cleanup_detached_topology_locked() { std::to_string(id)); dependency_resolver_.erase(id); } - if (auto* composition = composition_relationships()) - composition->cleanup(); + if (raster_capabilities_) + raster_capabilities_->cleanup(); } void Scene_Base::validate_structure_locked() { for (const auto& [id, renderable] : renderables_) { @@ -536,8 +540,8 @@ void Scene_Base::validate_structure_locked() { "dependency graph references detached renderable " + std::to_string(id)); } - if (const auto* composition = composition_relationships()) - composition->validate(); + if (raster_capabilities_) + raster_capabilities_->validate(); } [[noreturn]] void Scene_Base::structure_fail_fast(const char* stage, std::exception_ptr exception) noexcept { std::fprintf(stderr, "Renderive scene structure failure during %s", stage ? stage : "unknown stage"); @@ -641,9 +645,8 @@ Scene_Base::Topology_Snapshot Scene_Base::topology_snapshot() const { std::lock_guard lock(model_mutex_); Topology_Snapshot snapshot; const auto dependency = dependency_resolver_.resolve(); - const auto* composition = composition_relationships(); - const auto display = composition - ? composition->resolve() + const auto display = raster_capabilities_ + ? raster_capabilities_->resolve() : renderive::scene::dependency::Resolution< Renderable_Id>{}; const auto& ids = dependency.order; @@ -675,9 +678,8 @@ Scene_Base::Topology_Snapshot Scene_Base::topology_snapshot() const { std::vector Scene_Base::paint_order_snapshot() const { std::lock_guard lock(model_mutex_); std::vector result; - const auto* composition = composition_relationships(); - auto display = composition - ? composition->resolve() + auto display = raster_capabilities_ + ? raster_capabilities_->resolve() : renderive::scene::dependency::Resolution< Renderable_Id>{}; if (display.order.empty()) @@ -691,9 +693,8 @@ std::shared_ptr Scene_Base::snapshot_live_model() { auto snapshot = std::make_shared(); std::lock_guard lock(model_mutex_); const auto dependency = dependency_resolver_.resolve(); - const auto* composition = composition_relationships(); - const auto display = composition - ? composition->resolve() + const auto display = raster_capabilities_ + ? raster_capabilities_->resolve() : renderive::scene::dependency::Resolution< Renderable_Id>{}; const auto& dependency_order = dependency.order; @@ -774,7 +775,8 @@ std::shared_ptr Scene_Base::capture_live_frame() { auto snapshot = snapshot_live_model(); snapshot->scene_state_revision_ = acquire_scene_state(); capture_scene_state(*snapshot); - snapshot->viewport = frame_viewport(); + if (raster_capabilities_) + snapshot->viewport = raster_capabilities_->frame_viewport(); return snapshot; } @@ -855,44 +857,11 @@ const Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy() const { return frame_control_strategy_impl(); } -Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy_impl() { - throw std::logic_error("frame control strategy is not available"); -} - -const Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy_impl() const { - throw std::logic_error("frame control strategy is not available"); -} - -std::shared_ptr Scene_Base::make_renderable_color_cache() { - return {}; -} - -Frame_Viewport Scene_Base::frame_viewport() const { - return {}; -} - -std::uint64_t Scene_Base::acquire_scene_state() { - return 0; -} - -void Scene_Base::capture_scene_state(Frame_Render_Snapshot&) const {} - -renderive::scene::detail::Composition_Relationships* -Scene_Base::composition_relationships() noexcept { - auto* scene_2d = dynamic_cast(this); - return scene_2d - ? static_cast< - renderive::scene::detail::Composition_Relationships*>( - scene_2d) - : nullptr; -} -const renderive::scene::detail::Composition_Relationships* -Scene_Base::composition_relationships() const noexcept { - const auto* scene_2d = dynamic_cast(this); - return scene_2d - ? static_cast(scene_2d) - : nullptr; +void Scene_Base::bind_raster_capabilities( + renderive::scene::detail::Raster_Capabilities& capabilities) noexcept { + if (raster_capabilities_ != nullptr) + std::terminate(); + raster_capabilities_ = &capabilities; } void Scene_2D_Base::attach(Renderable_Id id) { diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index 6c5a22d..2dda39b 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -44,6 +44,11 @@ struct Composition_Relationships { virtual dependency::Resolution resolve() const = 0; }; +struct Raster_Capabilities : Composition_Relationships { + virtual std::shared_ptr make_renderable_color_cache() = 0; + virtual Frame_Viewport frame_viewport() const = 0; +}; + } // namespace renderive::scene::detail class Scene_Compositor { @@ -204,12 +209,10 @@ protected: Impl& d_func() noexcept { return *impl_; } const Impl& d_func() const noexcept { return *impl_; } - virtual Frame_Control_Strategy_Base& frame_control_strategy_impl(); - virtual const Frame_Control_Strategy_Base& frame_control_strategy_impl() const; - virtual std::shared_ptr make_renderable_color_cache(); - virtual Frame_Viewport frame_viewport() const; - virtual std::uint64_t acquire_scene_state(); - virtual void capture_scene_state(Frame_Render_Snapshot& snapshot) const; + virtual Frame_Control_Strategy_Base& frame_control_strategy_impl() = 0; + virtual const Frame_Control_Strategy_Base& frame_control_strategy_impl() const = 0; + virtual std::uint64_t acquire_scene_state() = 0; + virtual void capture_scene_state(Frame_Render_Snapshot& snapshot) const = 0; std::unique_lock lock_render_idle(); [[nodiscard]] bool is_render_execution_context() const noexcept; bool consume_model_dirty() noexcept; @@ -271,10 +274,8 @@ private: const Renderable& renderable) const; void validate_structure_locked(); void cleanup_detached_topology_locked(); - renderive::scene::detail::Composition_Relationships* - composition_relationships() noexcept; - const renderive::scene::detail::Composition_Relationships* - composition_relationships() const noexcept; + void bind_raster_capabilities( + renderive::scene::detail::Raster_Capabilities& capabilities) noexcept; [[noreturn]] static void structure_fail_fast(const char* stage, std::exception_ptr exception = {}) noexcept; inline static thread_local Scene_Base* active_execution_scene_{}; @@ -289,6 +290,7 @@ private: std::pmr::unordered_map renderables_; renderive::scene::dependency::Dependency_Resolver dependency_resolver_; + renderive::scene::detail::Raster_Capabilities* raster_capabilities_{}; std::pmr::unordered_map> color_caches_; mutable std::mutex model_mutex_; @@ -313,7 +315,7 @@ private: class Scene_2D_Base : public Scene_Base, - private renderive::scene::detail::Composition_Relationships { + private renderive::scene::detail::Raster_Capabilities { public: class Renderable_Editor : public renderive::scene_inheritance::Editor_Node< @@ -366,6 +368,8 @@ protected: }; Scene_2D_Base(std::pmr::memory_resource& memory_resource, std::unique_ptr impl); + virtual std::shared_ptr make_renderable_color_cache() override = 0; + virtual Frame_Viewport frame_viewport() const override = 0; private: friend class Scene_Base; diff --git a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp index 85775ec..e9a91fb 100644 --- a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp @@ -89,7 +89,7 @@ TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_f const auto analysis = analyze_frame(*plan, snapshot); ASSERT_EQ(analysis.nodes.size(), 3u); - EXPECT_EQ(analysis.nodes[2].dependency_ready_time_ns, 150u); + EXPECT_EQ(analysis.nodes[2].dependency_ready_offset_ns, 50u); EXPECT_EQ(analysis.nodes[2].scheduler_wait_ns, 10u); EXPECT_EQ(analysis.total_render_duration_ns, 100u); EXPECT_EQ(analysis.total_work_duration_ns, 100u); diff --git a/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp b/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp index 7b10902..cc25168 100644 --- a/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp @@ -280,6 +280,25 @@ TEST(render_graph_runtime_test, EXPECT_EQ(source.operation().status(), External_Operation_Status::cancelled); } +TEST(render_graph_runtime_test, idle_cancellation_does_not_arm_next_execution) { + const auto plan = two_node_plan(); + std::vector execution_storage; + auto slots = execution_slots(*plan, execution_storage); + renderive::render_graph::detail::Render_Graph_Runtime runtime(*plan); + + runtime.cancel_pending(); + std::atomic executed{}; + EXPECT_NO_THROW(runtime.execute( + slots, [&](std::size_t, Node_Execution_Metrics*) { + executed.fetch_add(1, std::memory_order_relaxed); + return Node_Execution_Result::completed(); + })); + + EXPECT_EQ(executed.load(std::memory_order_relaxed), 2); + EXPECT_EQ(execution_storage[0].status, Node_Execution_Status::complete); + EXPECT_EQ(execution_storage[1].status, Node_Execution_Status::complete); +} + TEST(external_operation_test, completion_commit_is_exclusive_with_cancellation) { External_Operation_Source source; const auto operation = source.operation(); diff --git a/cmake/RenderiveConfig.cmake.in b/cmake/RenderiveConfig.cmake.in index f3dfc17..d233052 100644 --- a/cmake/RenderiveConfig.cmake.in +++ b/cmake/RenderiveConfig.cmake.in @@ -26,7 +26,11 @@ if (@RENDERIVE_INSTALL_3D@) find_dependency(tinyobjloader CONFIG) find_dependency(ZLIB) if (WIN32) + set(_Renderive_saved_module_path "${CMAKE_MODULE_PATH}") + list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules") find_dependency(PThreads4W) + set(CMAKE_MODULE_PATH "${_Renderive_saved_module_path}") + unset(_Renderive_saved_module_path) endif () endif () diff --git a/cmake/RenderiveInstall.cmake b/cmake/RenderiveInstall.cmake index c5704b6..eb41ea9 100644 --- a/cmake/RenderiveInstall.cmake +++ b/cmake/RenderiveInstall.cmake @@ -34,6 +34,12 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/RenderiveConfigVersion.cmake" DESTINATION "${Renderive_install_cmake_dir}") +if (RENDERIVE_INSTALL_3D AND WIN32) + install(FILES + "${PROJECT_SOURCE_DIR}/render_3D/cmake/module/FindPThreads4W.cmake" + DESTINATION "${Renderive_install_cmake_dir}/modules") +endif () + if (RENDERIVE_INSTALL_QT) install(FILES "${PROJECT_SOURCE_DIR}/export.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") diff --git a/export.h b/export.h index b4e7601..f745fdc 100644 --- a/export.h +++ b/export.h @@ -1,9 +1,5 @@ #pragma once -#include -#include -#include #include -#include #include #include #include @@ -147,37 +143,4 @@ inline Qt::Orientation to_qt_orientation(Orientation orientation) { inline Time_Of_Day qt_to_time_of_day(const QTime& time) { return {time.isValid() ? time.msecsSinceStartOfDay() : -1}; } -namespace detail { -inline std::default_random_engine& mock_data_random_engine() { - static thread_local std::default_random_engine engine(std::random_device{}()); - return engine; -} -} // namespace detail -inline void fill_data(Range value_range, std::span output) { - if (value_range.origin > value_range.target) - std::swap(value_range.origin, value_range.target); - std::uniform_real_distribution dist(value_range.origin, value_range.target); - for (double& value : output) - value = dist(detail::mock_data_random_engine()); -} -inline std::vector get_data(Range value_range, int size) { - std::vector ret(static_cast(std::max(0, size))); - fill_data(value_range, ret); - return ret; -} -inline double get_data(Range value_range) { - if (value_range.origin > value_range.target) - std::swap(value_range.origin, value_range.target); - std::uniform_real_distribution dist(value_range.origin, value_range.target); - return dist(detail::mock_data_random_engine()); -} -inline std::vector get_data(int value, int size) { - return std::vector(size, value); -} -inline std::vector get_colored_data(int size) { - std::vector ret(size); - for (int i = 0; i < size; ++i) - ret[i] = i; - return ret; -} } // namespace renderive diff --git a/render_3D/cmake/module/FindPThreads4W.cmake b/render_3D/cmake/module/FindPThreads4W.cmake index 2c8d8af..8554e8a 100644 --- a/render_3D/cmake/module/FindPThreads4W.cmake +++ b/render_3D/cmake/module/FindPThreads4W.cmake @@ -1,5 +1,16 @@ include_guard(GLOBAL) -rcl_get_effective_install_dir(render_3D::PThreads4W PTHREADS4W_ROOT) + +# Self-contained PThreads4W discovery used both by the source tree and by the +# installed Renderive package. The build infrastructure may define +# PTHREADS4W_ROOT, but consumers only need to provide PThreads4W_ROOT, +# PTHREADS4W_ROOT, the corresponding environment variable, or CMAKE_PREFIX_PATH. +set(_PThreads4W_roots) +foreach(_root IN ITEMS "${PThreads4W_ROOT}" "${PTHREADS4W_ROOT}" "$ENV{PThreads4W_ROOT}" "$ENV{PTHREADS4W_ROOT}") + if (_root) + list(APPEND _PThreads4W_roots "${_root}") + endif () +endforeach() + if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64)$") set(PThreads4W_ARCH "x86_64") elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") @@ -9,36 +20,77 @@ elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86|i[3-6]86)$") else () set(PThreads4W_ARCH "${CMAKE_SYSTEM_PROCESSOR}") endif () -set(PThreads4W_INSTALL_DIR "${PTHREADS4W_ROOT}/${PThreads4W_ARCH}/${CMAKE_BUILD_TYPE}") + find_path(PThreads4W_INCLUDE_DIR NAMES pthread.h - PATHS "${PThreads4W_INSTALL_DIR}/include" - NO_DEFAULT_PATH -) -if (CMAKE_BUILD_TYPE STREQUAL "Debug") - set(PThreads4W_LIBRARY_NAME pthreadVC3d) + HINTS ${_PThreads4W_roots} + PATH_SUFFIXES + "${PThreads4W_ARCH}/Debug/include" + "${PThreads4W_ARCH}/Release/include" + "${PThreads4W_ARCH}/RelWithDebInfo/include" + "${PThreads4W_ARCH}/MinSizeRel/include" + include) + +find_library(PThreads4W_LIBRARY_DEBUG + NAMES pthreadVC3d + HINTS ${_PThreads4W_roots} + PATH_SUFFIXES "${PThreads4W_ARCH}/Debug/lib" Debug/lib lib) +find_library(PThreads4W_LIBRARY_RELEASE + NAMES pthreadVC3 + HINTS ${_PThreads4W_roots} + PATH_SUFFIXES + "${PThreads4W_ARCH}/Release/lib" + "${PThreads4W_ARCH}/RelWithDebInfo/lib" + "${PThreads4W_ARCH}/MinSizeRel/lib" + Release/lib RelWithDebInfo/lib MinSizeRel/lib lib) + +# Keep the conventional singular variable available for callers that inspect it. +if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND PThreads4W_LIBRARY_DEBUG) + set(PThreads4W_LIBRARY "${PThreads4W_LIBRARY_DEBUG}") +elseif (PThreads4W_LIBRARY_RELEASE) + set(PThreads4W_LIBRARY "${PThreads4W_LIBRARY_RELEASE}") else () - set(PThreads4W_LIBRARY_NAME pthreadVC3) + set(PThreads4W_LIBRARY "${PThreads4W_LIBRARY_DEBUG}") endif () -find_library(PThreads4W_LIBRARY - NAMES "${PThreads4W_LIBRARY_NAME}" - PATHS "${PThreads4W_INSTALL_DIR}/lib" - NO_DEFAULT_PATH -) + include(FindPackageHandleStandardArgs) find_package_handle_standard_args(PThreads4W - REQUIRED_VARS - PThreads4W_INCLUDE_DIR - PThreads4W_LIBRARY -) + REQUIRED_VARS PThreads4W_INCLUDE_DIR PThreads4W_LIBRARY) + if (PThreads4W_FOUND AND NOT TARGET PThreads4W::PThreads4W) add_library(PThreads4W::PThreads4W UNKNOWN IMPORTED) set_target_properties(PThreads4W::PThreads4W PROPERTIES - IMPORTED_LOCATION "${PThreads4W_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${PThreads4W_INCLUDE_DIR}" - ) + INTERFACE_INCLUDE_DIRECTORIES "${PThreads4W_INCLUDE_DIR}") + if (PThreads4W_LIBRARY_DEBUG) + set_property(TARGET PThreads4W::PThreads4W APPEND PROPERTY + IMPORTED_CONFIGURATIONS DEBUG) + set_target_properties(PThreads4W::PThreads4W PROPERTIES + IMPORTED_LOCATION_DEBUG "${PThreads4W_LIBRARY_DEBUG}") + endif () + if (PThreads4W_LIBRARY_RELEASE) + set_property(TARGET PThreads4W::PThreads4W APPEND PROPERTY + IMPORTED_CONFIGURATIONS RELEASE RELWITHDEBINFO MINSIZEREL) + set_target_properties(PThreads4W::PThreads4W PROPERTIES + IMPORTED_LOCATION_RELEASE "${PThreads4W_LIBRARY_RELEASE}" + IMPORTED_LOCATION_RELWITHDEBINFO "${PThreads4W_LIBRARY_RELEASE}" + IMPORTED_LOCATION_MINSIZEREL "${PThreads4W_LIBRARY_RELEASE}") + endif () + # A single available configuration remains usable for the other build types. + if (NOT PThreads4W_LIBRARY_RELEASE AND PThreads4W_LIBRARY_DEBUG) + set_target_properties(PThreads4W::PThreads4W PROPERTIES + MAP_IMPORTED_CONFIG_RELEASE DEBUG + MAP_IMPORTED_CONFIG_RELWITHDEBINFO DEBUG + MAP_IMPORTED_CONFIG_MINSIZEREL DEBUG) + elseif (NOT PThreads4W_LIBRARY_DEBUG AND PThreads4W_LIBRARY_RELEASE) + set_target_properties(PThreads4W::PThreads4W PROPERTIES + MAP_IMPORTED_CONFIG_DEBUG RELEASE) + endif () endif () + mark_as_advanced( PThreads4W_INCLUDE_DIR PThreads4W_LIBRARY -) + PThreads4W_LIBRARY_DEBUG + PThreads4W_LIBRARY_RELEASE) +unset(_PThreads4W_roots) +unset(_root) diff --git a/render_3D/render_3D/Point_Scene.cpp b/render_3D/render_3D/Point_Scene.cpp index 3afe336..af8c5f9 100644 --- a/render_3D/render_3D/Point_Scene.cpp +++ b/render_3D/render_3D/Point_Scene.cpp @@ -124,8 +124,36 @@ struct Point_Backend_State final { render_domain->invoke([this] { backend.reset(); }); } + detail::Datoviz_Visual_Backend& require_backend() { + if (backend) + return *backend; + if (backend_failure) + std::rethrow_exception(backend_failure); + throw std::runtime_error("Datoviz backend is unavailable"); + } + + void quarantine_backend(std::exception_ptr failure) noexcept { + if (!failure) { + try { + throw std::runtime_error("Datoviz backend was quarantined"); + } catch (...) { + failure = std::current_exception(); + } + } + backend_failure = std::move(failure); + // An abandoned fence means the driver never proved that the submitted + // work stopped touching the backend resources. Destroying or reusing + // them would be unsafe. Deliberately relinquish ownership of the whole + // backend and let the OS/driver reclaim it at process teardown. This is + // a fault-containment path only; all subsequent scene operations fail. + static_cast(backend.release()); + std::lock_guard lock(frame_mutex); + latest.reset(); + } + std::shared_ptr render_domain; std::unique_ptr backend; + std::exception_ptr backend_failure; mutable std::mutex frame_mutex; std::shared_ptr latest; }; @@ -205,7 +233,7 @@ struct Basic_Point_Scene final &detail::Scene_State::viewport>(); const auto state = backend_state; state->render_domain->invoke([state, type, x, y, button, modifiers, viewport] { - state->backend->dispatch_pointer(type, x, y, button, modifiers, viewport); + state->require_backend().dispatch_pointer(type, x, y, button, modifiers, viewport); }); this->notify_model_dirty(); } @@ -218,7 +246,7 @@ struct Basic_Point_Scene final const auto state = backend_state; state->render_domain->invoke( [state, x, y, delta_x, delta_y, modifiers, viewport] { - state->backend->dispatch_wheel(x, y, delta_x, delta_y, modifiers, + state->require_backend().dispatch_wheel(x, y, delta_x, delta_y, modifiers, viewport); }); this->notify_model_dirty(); @@ -227,7 +255,7 @@ struct Basic_Point_Scene final void dispatch_key(const ::renderive::Key_Event& event) override { const auto state = backend_state; state->render_domain->invoke([state, event] { - state->backend->dispatch_key(event); + state->require_backend().dispatch_key(event); }); this->notify_model_dirty(); } @@ -328,7 +356,9 @@ struct Basic_Point_Scene final {completion.capacity, completion.in_flight, completion.peak_in_flight, completion.watched, completion.peak_watched, completion.backpressure_count, completion.backpressure_wait_ns}, - render.unhandled_exception_count + render.unhandled_exception_count, + completion.fault_count, + completion.abandoned_count }; } @@ -377,9 +407,15 @@ struct Basic_Point_Scene final async_frame->pending.reset(); pending.trace.gpu_fence_wait_ns = async_frame->completion.wait_duration_ns; + if (async_frame->completion.abandoned) { + auto failure = std::move(async_frame->completion.error); + state->quarantine_backend(failure); + static_cast(source->fail(std::move(failure))); + return; + } if (async_frame->completion.error) { try { - state->backend->discard(std::move(pending)); + state->require_backend().discard(std::move(pending)); static_cast(source->fail( std::move(async_frame->completion.error))); } catch (...) { @@ -389,7 +425,7 @@ struct Basic_Point_Scene final return; } try { - auto completed = state->backend->collect( + auto completed = state->require_backend().collect( std::move(pending)); auto frame = std::move(completed.frame); auto trace = std::make_shared( @@ -447,7 +483,7 @@ struct Basic_Point_Scene final ? static_cast(queue_wait) : 0; } - auto pending = state->backend->submit( + auto pending = state->require_backend().submit( scene_state, scene_revision, *prepared, frame_sequence, observe); if (!pending) { diff --git a/render_3D/render_3D/Point_Scene.h b/render_3D/render_3D/Point_Scene.h index af0e1ef..b365fbf 100644 --- a/render_3D/render_3D/Point_Scene.h +++ b/render_3D/render_3D/Point_Scene.h @@ -82,6 +82,8 @@ struct Runtime_Statistics { Runtime_Admission_Statistics render_domain; Runtime_Admission_Statistics gpu_completion; std::uint64_t render_domain_unhandled_exception_count{}; + std::uint64_t gpu_completion_fault_count{}; + std::uint64_t gpu_completion_abandoned_count{}; }; struct Frame_Status { diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp index 2481d00..6b9d7f7 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp @@ -19,12 +19,14 @@ Gpu_Completion_Service::~Gpu_Completion_Service() { if (thread_.joinable()) thread_.join(); } -Gpu_Completion_Service::Reservation::Reservation(std::shared_ptr pending) noexcept : pending_(std::move(pending)) {} -Gpu_Completion_Service::Reservation::~Reservation() { - cancel(); -} -Gpu_Completion_Service::Reservation::Reservation(Reservation&& other) noexcept : pending_(std::exchange(other.pending_, {})) {} -void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence) noexcept { +Gpu_Completion_Service::Reservation::Reservation( + std::shared_ptr pending) noexcept + : pending_(std::move(pending)) {} +Gpu_Completion_Service::Reservation::~Reservation() { cancel(); } +Gpu_Completion_Service::Reservation::Reservation(Reservation&& other) noexcept + : pending_(std::exchange(other.pending_, {})) {} +void Gpu_Completion_Service::Reservation::watch(VkDevice device, + VkFence fence) noexcept { if (!pending_ || device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE) std::terminate(); auto pending = std::exchange(pending_, {}); @@ -35,9 +37,11 @@ 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(); - const std::size_t watched = service->watched_.fetch_add(1, std::memory_order_relaxed) + 1; + // This timestamp is part of correctness, not only observability: it + // bounds the lifetime of a submission whose fence never signals. + 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; } @@ -50,11 +54,15 @@ 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 { +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)) {} + 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 { +void Gpu_Completion_Service::cancel_reserved( + const std::shared_ptr& pending) noexcept { if (!pending) return; std::lock_guard lock(pending->mutex); @@ -67,18 +75,23 @@ void Gpu_Completion_Service::acquire_slot() { 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(); + std::chrono::steady_clock::now() - started) + .count(); if (waited > 0) - backpressure_wait_ns_.fetch_add(static_cast(waited), std::memory_order_relaxed); + 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; + 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) { +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)) @@ -95,7 +108,8 @@ Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(Completion c wake(); return Reservation(std::move(pending)); } -Gpu_Completion_Service::Statistics Gpu_Completion_Service::statistics() const noexcept { +Gpu_Completion_Service::Statistics +Gpu_Completion_Service::statistics() const noexcept { return { static_cast(default_capacity), in_flight_.load(std::memory_order_relaxed), @@ -103,8 +117,9 @@ Gpu_Completion_Service::Statistics Gpu_Completion_Service::statistics() const no 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) - }; + backpressure_wait_ns_.load(std::memory_order_relaxed), + fault_count_.load(std::memory_order_relaxed), + abandoned_count_.load(std::memory_order_relaxed)}; } void Gpu_Completion_Service::wake() noexcept { wake_generation_.fetch_add(1, std::memory_order_release); @@ -118,7 +133,17 @@ void Gpu_Completion_Service::run() noexcept { std::vector> active; active.reserve(static_cast(default_capacity)); std::size_t wait_group_index{}; - const auto finish = [this](const std::shared_ptr& pending, VkResult result) { + + const auto wait_age_ns = [](std::chrono::steady_clock::time_point started) { + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - started) + .count(); + return elapsed > 0 ? static_cast(elapsed) : 0ULL; + }; + const auto finish = [this, &wait_age_ns]( + const std::shared_ptr& pending, + VkResult result, bool abandoned, + const char* abandonment_reason = nullptr) { Completion completion; std::chrono::steady_clock::time_point watched_at{}; bool observe{}; @@ -131,14 +156,25 @@ void Gpu_Completion_Service::run() noexcept { } 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(); - completion_result.wait_duration_ns = duration > 0 ? static_cast(duration) : 0; - } - if (result != VK_SUCCESS) { + const std::uint64_t wait_ns = wait_age_ns(watched_at); + if (observe) + completion_result.wait_duration_ns = wait_ns; + completion_result.abandoned = abandoned; + if (abandoned || result != VK_SUCCESS) { + fault_count_.fetch_add(1, std::memory_order_relaxed); + if (abandoned) + abandoned_count_.fetch_add(1, std::memory_order_relaxed); try { - throw std::runtime_error("GPU fence wait failed with Vulkan result " + std::to_string(static_cast(result))); + if (abandoned) { + std::string message = abandonment_reason != nullptr + ? abandonment_reason + : "GPU fence was abandoned"; + message += " after " + std::to_string(wait_ns) + " ns"; + throw std::runtime_error(std::move(message)); + } + throw std::runtime_error( + "GPU fence wait failed with Vulkan result " + + std::to_string(static_cast(result))); } catch (...) { completion_result.error = std::current_exception(); } @@ -149,11 +185,14 @@ void Gpu_Completion_Service::run() noexcept { } release_slot(); }; + for (;;) { - const std::uint64_t wake_generation = wake_generation_.load(std::memory_order_acquire); + 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)); + std::vector groups; for (auto iterator = active.begin(); iterator != active.end();) { Pending_Fence::Status status; @@ -166,16 +205,19 @@ void Gpu_Completion_Service::run() noexcept { fence = (*iterator)->fence; } if (status == Pending_Fence::Status::canceled || - (status == Pending_Fence::Status::reserved && stopping_.load(std::memory_order_acquire))) { + (status == Pending_Fence::Status::reserved && + stopping_.load(std::memory_order_acquire))) { cancel_reserved(*iterator); iterator = active.erase(iterator); release_slot(); continue; } if (status == Pending_Fence::Status::watched) { - auto group = std::find_if(groups.begin(), groups.end(), [device](const Device_Fences& item) { - return item.device == device; - }); + auto group = std::find_if( + groups.begin(), groups.end(), + [device](const Device_Fences& item) { + return item.device == device; + }); if (group == groups.end()) { groups.push_back(Device_Fences{device, {}}); group = groups.end() - 1; @@ -184,27 +226,47 @@ void Gpu_Completion_Service::run() noexcept { } ++iterator; } - if (stopping_.load(std::memory_order_acquire) && active.empty() && pending_.empty()) + if (stopping_.load(std::memory_order_acquire) && active.empty() && + pending_.empty()) return; - // Always probe every watched fence before entering a timed wait. The - // timed wait still prevents busy-spinning, while this full probe keeps - // completion latency bounded by one wait quantum instead of one quantum - // per active device group. + + // Probe every watched fence first. A permanently unsignaled fence is + // converted into a logical failure after a bounded interval. The + // submission is explicitly marked abandoned so its owner can + // quarantine, rather than recycle, the referenced GPU resources. bool completed_any = false; for (auto iterator = active.begin(); iterator != active.end();) { VkDevice device{VK_NULL_HANDLE}; VkFence fence{VK_NULL_HANDLE}; Pending_Fence::Status status; + std::chrono::steady_clock::time_point watched_at{}; { std::lock_guard lock((*iterator)->mutex); status = (*iterator)->status; device = (*iterator)->device; fence = (*iterator)->fence; + watched_at = (*iterator)->watched_at; } if (status != Pending_Fence::Status::watched) { ++iterator; continue; } + if (stopping_.load(std::memory_order_acquire)) { + auto pending = *iterator; + iterator = active.erase(iterator); + finish(pending, VK_TIMEOUT, true, + "GPU completion service stopped with an in-flight fence; submission quarantined"); + completed_any = true; + continue; + } + if (wait_age_ns(watched_at) >= maximum_fence_age_ns) { + auto pending = *iterator; + iterator = active.erase(iterator); + finish(pending, VK_TIMEOUT, true, + "GPU fence exceeded the maximum completion age; submission quarantined"); + completed_any = true; + continue; + } const VkResult result = vkGetFenceStatus(device, fence); if (result == VK_NOT_READY) { ++iterator; @@ -212,25 +274,31 @@ void Gpu_Completion_Service::run() noexcept { } auto pending = *iterator; iterator = active.erase(iterator); - finish(pending, result); + finish(pending, result, result != VK_SUCCESS, + result == VK_SUCCESS + ? nullptr + : "GPU fence query returned an error; submission quarantined"); completed_any = true; } if (completed_any) continue; if (groups.empty()) { std::unique_lock lock(wait_mutex_); - if (wake_generation_.load(std::memory_order_acquire) == wake_generation) { + if (wake_generation_.load(std::memory_order_acquire) == + wake_generation) { wake_condition_.wait(lock, [this, wake_generation] { - return wake_generation_.load(std::memory_order_acquire) != wake_generation; + return wake_generation_.load(std::memory_order_acquire) != + wake_generation; }); } continue; } + wait_group_index %= groups.size(); const Device_Fences& group = groups[wait_group_index++]; const VkResult wait_result = vkWaitForFences( - group.device, static_cast(group.fences.size()), group.fences.data(), VK_FALSE, - fence_wait_timeout_ns); + group.device, static_cast(group.fences.size()), + group.fences.data(), VK_FALSE, fence_wait_timeout_ns); for (auto iterator = active.begin(); iterator != active.end();) { VkDevice device{VK_NULL_HANDLE}; VkFence fence{VK_NULL_HANDLE}; @@ -241,7 +309,8 @@ void Gpu_Completion_Service::run() noexcept { device = (*iterator)->device; fence = (*iterator)->fence; } - if (status != Pending_Fence::Status::watched || device != group.device) { + if (status != Pending_Fence::Status::watched || + device != group.device) { ++iterator; continue; } @@ -254,7 +323,10 @@ void Gpu_Completion_Service::run() noexcept { } auto pending = *iterator; iterator = active.erase(iterator); - finish(pending, result); + finish(pending, result, result != VK_SUCCESS, + result == VK_SUCCESS + ? nullptr + : "GPU fence wait returned an error; submission quarantined"); } } } diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.h b/render_3D/render_3D/detail/Gpu_Completion_Service.h index b625bfb..1f08ed0 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.h +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.h @@ -19,6 +19,11 @@ public: struct Result { std::exception_ptr error; std::uint64_t wait_duration_ns{}; + // True when the service cannot prove that the submission completed: + // a fence exceeded the bounded wait policy, Vulkan returned an error, + // or the service is shutting down. Callers must not recycle or destroy + // resources referenced by that submission. + bool abandoned{}; }; struct Statistics { std::size_t capacity{}; @@ -28,6 +33,8 @@ public: std::size_t peak_watched{}; std::uint64_t backpressure_count{}; std::uint64_t backpressure_wait_ns{}; + std::uint64_t fault_count{}; + std::uint64_t abandoned_count{}; }; using Completion = std::function; class Reservation final { @@ -76,6 +83,7 @@ private: void run() noexcept; static constexpr std::ptrdiff_t default_capacity = 1024; static constexpr std::uint64_t fence_wait_timeout_ns = 1'000'000; + static constexpr std::uint64_t maximum_fence_age_ns = 30'000'000'000ULL; std::counting_semaphore slots_{default_capacity}; oneapi::tbb::concurrent_bounded_queue> pending_; std::mutex wait_mutex_; @@ -87,6 +95,8 @@ private: std::atomic_size_t peak_watched_{}; std::atomic_uint64_t backpressure_count_{}; std::atomic_uint64_t backpressure_wait_ns_{}; + std::atomic_uint64_t fault_count_{}; + std::atomic_uint64_t abandoned_count_{}; std::atomic_bool stopping_{}; std::thread thread_; }; diff --git a/web_server/app/Gallery_Capture_Json.h b/web_server/app/Gallery_Capture_Json.h index c098c89..49a0f80 100644 --- a/web_server/app/Gallery_Capture_Json.h +++ b/web_server/app/Gallery_Capture_Json.h @@ -157,12 +157,6 @@ inline Json execution_json(const Node_Execution& execution, } return { {"node_id", execution.node_id}, - {"ready_time_ns", execution.ready_time_ns}, - {"start_time_ns", execution.start_time_ns}, - {"cpu_end_time_ns", execution.cpu_end_time_ns}, - {"external_start_time_ns", execution.external_start_time_ns}, - {"external_end_time_ns", execution.external_end_time_ns}, - {"end_time_ns", execution.end_time_ns}, {"start_offset_ns", execution.start_time_ns >= frame.render_start_ns ? execution.start_time_ns - frame.render_start_ns : 0}, {"cpu_end_offset_ns", execution.cpu_end_time_ns >= frame.render_start_ns @@ -198,7 +192,7 @@ inline Json node_analysis_json(const Node_Frame_Analysis& node) { {"gpu_execution_duration_ns", node.gpu_execution_duration_ns}, {"start_offset_ns", node.start_offset_ns}, {"end_offset_ns", node.end_offset_ns}, - {"dependency_ready_time_ns", node.dependency_ready_time_ns}, + {"dependency_ready_offset_ns", node.dependency_ready_offset_ns}, {"scheduler_wait_ns", node.scheduler_wait_ns}, {"work_contribution", node.work_contribution}, {"critical_path_contribution", node.critical_path_contribution}, @@ -222,8 +216,6 @@ inline Json frame_json(const Captured_Frame& frame) { return { {"frame_id", frame.snapshot->frame_id}, {"render_plan_version", frame.snapshot->render_plan_version}, - {"render_start_ns", frame.snapshot->render_start_ns}, - {"render_end_ns", frame.snapshot->render_end_ns}, {"render_duration_ns", frame.snapshot->render_duration_ns()}, {"node_executions", std::move(executions)}, {"analysis", { diff --git a/web_server/app/render_3D/Gallery_Scene3D.cpp b/web_server/app/render_3D/Gallery_Scene3D.cpp index 60bbbcb..b0e01a2 100644 --- a/web_server/app/render_3D/Gallery_Scene3D.cpp +++ b/web_server/app/render_3D/Gallery_Scene3D.cpp @@ -490,7 +490,9 @@ public: {"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}}}}}, + {"backpressure_wait_ns", runtime.gpu_completion.backpressure_wait_ns}, + {"fault_count", runtime.gpu_completion_fault_count}, + {"abandoned_count", runtime.gpu_completion_abandoned_count}}}}}, {"session_id", session_id_}, {"performance_capture", gallery_performance_capture_json(scene_.render_scene())}, diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 54d84e4..388e5d3 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -544,12 +544,14 @@ TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistic for (const auto& frame : captured.at("frames")) { EXPECT_TRUE(frame.contains("frame_id")); EXPECT_TRUE(frame.contains("render_plan_version")); - EXPECT_TRUE(frame.contains("render_start_ns")); - EXPECT_TRUE(frame.contains("render_end_ns")); + EXPECT_FALSE(frame.contains("render_start_ns")); + EXPECT_FALSE(frame.contains("render_end_ns")); ASSERT_FALSE(frame.at("node_executions").empty()); const auto& execution = frame.at("node_executions").at(0); - EXPECT_TRUE(execution.contains("start_time_ns")); - EXPECT_TRUE(execution.contains("end_time_ns")); + EXPECT_FALSE(execution.contains("start_time_ns")); + EXPECT_FALSE(execution.contains("end_time_ns")); + EXPECT_TRUE(execution.contains("start_offset_ns")); + EXPECT_TRUE(execution.contains("end_offset_ns")); EXPECT_TRUE(execution.contains("worker_id")); EXPECT_TRUE(execution.contains("status")); EXPECT_TRUE(execution.contains("attachments")); diff --git a/webapp_gallery/src/protocol/gallery_types.ts b/webapp_gallery/src/protocol/gallery_types.ts index 3f4c308..ca2861e 100644 --- a/webapp_gallery/src/protocol/gallery_types.ts +++ b/webapp_gallery/src/protocol/gallery_types.ts @@ -36,10 +36,10 @@ export interface Gallery_Renderable_Cache {owner_id: number; name: string; prepa export interface Gallery_Render_Plan {version: number; nodes: Gallery_Render_Node[]; edges: Gallery_Render_Edge[]; renderables: Gallery_Renderable_Cache[];} export interface Gallery_Node_Diagnostic_Attachment {type: string; content: string;} export interface Gallery_Node_Execution {node_id: number; worker_id: number; start_offset_ns: number; cpu_end_offset_ns: number; external_start_offset_ns: number; external_end_offset_ns: number; end_offset_ns: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; status: "pending"|"ready"|"running"|"waiting_external"|"complete"|"failed"|"cancelled"; metrics?: Record; attachments: Gallery_Node_Diagnostic_Attachment[];} -export interface Gallery_Node_Analysis {node_id: number; duration_ns: number; cpu_duration_ns: number; render_domain_cpu_duration_ns: number; total_cpu_work_duration_ns: number; external_duration_ns: number; render_domain_queue_wait_ns: number; gpu_completion_wait_ns: number; gpu_execution_duration_ns: number; start_offset_ns: number; end_offset_ns: number; dependency_ready_time_ns: number; scheduler_wait_ns: number; work_contribution: number; critical_path_contribution: number; on_critical_path: boolean;} +export interface Gallery_Node_Analysis {node_id: number; duration_ns: number; cpu_duration_ns: number; render_domain_cpu_duration_ns: number; total_cpu_work_duration_ns: number; external_duration_ns: number; render_domain_queue_wait_ns: number; gpu_completion_wait_ns: number; gpu_execution_duration_ns: number; start_offset_ns: number; end_offset_ns: number; dependency_ready_offset_ns: number; scheduler_wait_ns: number; work_contribution: number; critical_path_contribution: number; on_critical_path: boolean;} export interface Gallery_Worker_Analysis {worker_id: number; work_duration_ns: number; utilization: number;} export interface Gallery_Frame_Analysis {total_render_duration_ns: number; critical_path_duration_ns: number; total_work_duration_ns: number; scheduler_work_duration_ns: number; render_domain_work_duration_ns: number; total_external_duration_ns: number; total_render_domain_queue_wait_ns: number; total_gpu_completion_wait_ns: number; total_gpu_execution_duration_ns: number; scheduler_parallel_overlap_ns: number; scheduler_peak_parallelism: number; average_cpu_concurrency: number; scheduler_average_parallelism: number; render_domain_utilization: number; critical_path: number[]; bottleneck_nodes: number[]; nodes: Gallery_Node_Analysis[]; workers: Gallery_Worker_Analysis[];} -export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_start_ns: number; render_end_ns: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: Gallery_Frame_Analysis;} +export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: Gallery_Frame_Analysis;} export interface Gallery_Node_Statistics {node_id: number; execution_count: number; average_ns: number; moving_average_ns: number; p50_ns: number; p95_ns: number; p99_ns: number; minimum_ns: number; maximum_ns: number; average_cpu_ns: number; average_render_domain_cpu_ns: number; average_total_cpu_work_ns: number; average_external_ns: number; average_render_domain_queue_wait_ns: number; average_gpu_completion_wait_ns: number; average_gpu_execution_ns: number; p95_cpu_ns: number; p95_render_domain_cpu_ns: number; p95_external_ns: number; p95_gpu_execution_ns: number; critical_path_frequency: number; average_scheduler_wait_ns: number;} export interface Gallery_Plan_Statistics {render_plan_version: number; frame_count: number; render_average_ns: number; render_p50_ns: number; render_p95_ns: number; render_maximum_ns: number; average_cpu_concurrency: number; scheduler_average_parallelism: number; render_domain_utilization: number; scheduler_peak_parallelism: number; scheduler_wait_average_ns: number; scheduler_wait_p95_ns: number; nodes: Gallery_Node_Statistics[];} export interface Gallery_Capture_Session {session_id: number; active: boolean; requested_count: number; captured_count: number; frames: Gallery_Captured_Frame[]; node_statistics: Gallery_Node_Statistics[]; plan_statistics: Gallery_Plan_Statistics[]; summary?: Record;} diff --git a/webapp_gallery/tests/dag/dag_model.test.ts b/webapp_gallery/tests/dag/dag_model.test.ts index f635b9f..1f84966 100644 --- a/webapp_gallery/tests/dag/dag_model.test.ts +++ b/webapp_gallery/tests/dag/dag_model.test.ts @@ -14,8 +14,6 @@ describe("DAG view model", () => { const frame: Gallery_Captured_Frame = { frame_id: 1, render_plan_version: 2, - render_start_ns: 100, - render_end_ns: 110, render_duration_ns: 10, node_executions: [{ node_id: 7, @@ -60,7 +58,7 @@ describe("DAG view model", () => { gpu_execution_duration_ns: 0, start_offset_ns: 0, end_offset_ns: 10, - dependency_ready_time_ns: 0, + dependency_ready_offset_ns: 0, scheduler_wait_ns: 0, work_contribution: 1, critical_path_contribution: 1,