From 38a752cb625bca52dd035e4ceeefd32def9c08c8 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 3 Sep 2026 13:43:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=8C3D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/kernel/include/global.hpp | 1 + kernel/kernel/include/model/Model.hpp | 4 +- .../kernel/module/task_flow/export/export.h | 8 +- .../module/task_flow/src/Task_Graph_Model.hpp | 1 + .../module/task_flow/src/Task_Runtime.cpp | 89 +-- .../module/task_flow/src/Task_Runtime.hpp | 16 +- .../module/task_flow/src/task_flow_facade.cpp | 3 + render_3D/render_3D/Render_3D.hpp | 5 + render_3D/render_3D/axis/Axes_3D.hpp | 27 + render_3D/render_3D/axis/Axis.cpp | 167 ++++++ render_3D/render_3D/axis/Axis.hpp | 46 ++ .../base/Datoviz_Frame_Observation.hpp | 100 ++++ render_3D/render_3D/base/Frame_3D.cpp | 136 +++++ render_3D/render_3D/base/Frame_3D.hpp | 158 ++++++ render_3D/render_3D/base/Types.hpp | 82 +++ render_3D/render_3D/base/Types.ipp | 36 ++ render_3D/render_3D/camera/Camera.hpp | 90 +++ render_3D/render_3D/camera/Camera_3D.hpp | 33 ++ render_3D/render_3D/detail/Backend_Types.cpp | 11 + render_3D/render_3D/detail/Backend_Types.hpp | 35 ++ render_3D/render_3D/detail/Exception.cpp | 24 + render_3D/render_3D/detail/Exception.hpp | 8 + .../detail/Gpu_Completion_Service.cpp | 154 +++++ .../detail/Gpu_Completion_Service.hpp | 65 +++ .../detail/Gpu_Completion_Service.ipp | 41 ++ render_3D/render_3D/main.cpp | 1 + render_3D/render_3D/scene/Render_Scene_3D.cpp | 14 + render_3D/render_3D/scene/Render_Scene_3D.hpp | 47 ++ render_3D/render_3D/scene/Render_Scene_3D.ipp | 137 +++++ .../scene/Render_Scene_3D_Datoviz.cpp | 1 + .../render_3D/scene/Render_Scene_3D_Model.ipp | 529 ++++++++++++++++++ .../scene/detail/Datoviz_Frame_Pipeline.ipp | 494 ++++++++++++++++ .../scene/detail/Datoviz_Frame_Target.ipp | 424 ++++++++++++++ .../scene/detail/Datoviz_Interaction.ipp | 59 ++ .../scene/detail/Datoviz_Scene_Common.ipp | 390 +++++++++++++ .../scene/detail/Datoviz_Scene_Resources.ipp | 319 +++++++++++ render_3D/render_3D/scene/rely_facade.h | 33 ++ render_3D/render_3D/visual/Basic_Visual.cpp | 5 + render_3D/render_3D/visual/Basic_Visual.hpp | 25 + render_3D/render_3D/visual/Basic_Visual.ipp | 94 ++++ render_3D/render_3D/visual/Glyph_Visual.cpp | 6 + render_3D/render_3D/visual/Glyph_Visual.hpp | 48 ++ render_3D/render_3D/visual/Glyph_Visual.ipp | 53 ++ render_3D/render_3D/visual/Image_Visual.cpp | 6 + render_3D/render_3D/visual/Image_Visual.hpp | 49 ++ render_3D/render_3D/visual/Image_Visual.ipp | 50 ++ render_3D/render_3D/visual/Labels_Visual.cpp | 6 + render_3D/render_3D/visual/Labels_Visual.hpp | 49 ++ render_3D/render_3D/visual/Labels_Visual.ipp | 50 ++ render_3D/render_3D/visual/Marker_Visual.cpp | 6 + render_3D/render_3D/visual/Marker_Visual.hpp | 57 ++ render_3D/render_3D/visual/Marker_Visual.ipp | 56 ++ render_3D/render_3D/visual/Mesh_Visual.cpp | 6 + render_3D/render_3D/visual/Mesh_Visual.hpp | 47 ++ render_3D/render_3D/visual/Mesh_Visual.ipp | 50 ++ render_3D/render_3D/visual/Path_Visual.cpp | 6 + render_3D/render_3D/visual/Path_Visual.hpp | 46 ++ render_3D/render_3D/visual/Path_Visual.ipp | 47 ++ render_3D/render_3D/visual/Pixel_Visual.cpp | 6 + render_3D/render_3D/visual/Pixel_Visual.hpp | 46 ++ render_3D/render_3D/visual/Pixel_Visual.ipp | 47 ++ render_3D/render_3D/visual/Point_Visual.cpp | 6 + render_3D/render_3D/visual/Point_Visual.hpp | 60 ++ render_3D/render_3D/visual/Point_Visual.ipp | 48 ++ .../render_3D/visual/Prepared_Visual.hpp | 28 + .../render_3D/visual/Primitive_Visual.cpp | 6 + .../render_3D/visual/Primitive_Visual.hpp | 46 ++ .../render_3D/visual/Primitive_Visual.ipp | 47 ++ render_3D/render_3D/visual/Segment_Visual.cpp | 6 + render_3D/render_3D/visual/Segment_Visual.hpp | 47 ++ render_3D/render_3D/visual/Segment_Visual.ipp | 51 ++ render_3D/render_3D/visual/Sphere_Visual.cpp | 6 + render_3D/render_3D/visual/Sphere_Visual.hpp | 46 ++ render_3D/render_3D/visual/Sphere_Visual.ipp | 48 ++ render_3D/render_3D/visual/Splat_Visual.cpp | 6 + render_3D/render_3D/visual/Splat_Visual.hpp | 47 ++ render_3D/render_3D/visual/Splat_Visual.ipp | 50 ++ render_3D/render_3D/visual/Text_Visual.cpp | 6 + render_3D/render_3D/visual/Text_Visual.hpp | 48 ++ render_3D/render_3D/visual/Text_Visual.ipp | 50 ++ render_3D/render_3D/visual/Vector_Visual.cpp | 6 + render_3D/render_3D/visual/Vector_Visual.hpp | 47 ++ render_3D/render_3D/visual/Vector_Visual.ipp | 51 ++ render_3D/render_3D/visual/Visuals.hpp | 16 + render_3D/render_3D/visual/Volume_Visual.cpp | 6 + render_3D/render_3D/visual/Volume_Visual.hpp | 47 ++ render_3D/render_3D/visual/Volume_Visual.ipp | 44 ++ .../visual/detail/Datoviz_Visual_Common.ipp | 152 +++++ .../detail/Datoviz_Visual_Factories.ipp | 172 ++++++ .../detail/Datoviz_Visual_Operations.hpp | 46 ++ .../detail/Datoviz_Visual_Operations.ipp | 339 +++++++++++ .../visual/detail/Datoviz_Visual_Set.ipp | 205 +++++++ .../visual/detail/Datoviz_Visual_State.hpp | 37 ++ .../visual/detail/Visual_Prepare.hpp | 8 + .../visual/detail/Visual_Prepare.ipp | 12 + .../tests/Async_Visual_Contract_Test.cpp | 46 ++ render_3D/tests/Visual_Tests.cpp | 96 ++++ 97 files changed, 6561 insertions(+), 70 deletions(-) create mode 100644 render_3D/render_3D/Render_3D.hpp create mode 100644 render_3D/render_3D/axis/Axes_3D.hpp create mode 100644 render_3D/render_3D/axis/Axis.cpp create mode 100644 render_3D/render_3D/axis/Axis.hpp create mode 100644 render_3D/render_3D/base/Datoviz_Frame_Observation.hpp create mode 100644 render_3D/render_3D/base/Frame_3D.cpp create mode 100644 render_3D/render_3D/base/Frame_3D.hpp create mode 100644 render_3D/render_3D/base/Types.hpp create mode 100644 render_3D/render_3D/base/Types.ipp create mode 100644 render_3D/render_3D/camera/Camera.hpp create mode 100644 render_3D/render_3D/camera/Camera_3D.hpp create mode 100644 render_3D/render_3D/detail/Backend_Types.cpp create mode 100644 render_3D/render_3D/detail/Backend_Types.hpp create mode 100644 render_3D/render_3D/detail/Exception.cpp create mode 100644 render_3D/render_3D/detail/Exception.hpp create mode 100644 render_3D/render_3D/detail/Gpu_Completion_Service.cpp create mode 100644 render_3D/render_3D/detail/Gpu_Completion_Service.hpp create mode 100644 render_3D/render_3D/detail/Gpu_Completion_Service.ipp create mode 100644 render_3D/render_3D/main.cpp create mode 100644 render_3D/render_3D/scene/Render_Scene_3D.cpp create mode 100644 render_3D/render_3D/scene/Render_Scene_3D.hpp create mode 100644 render_3D/render_3D/scene/Render_Scene_3D.ipp create mode 100644 render_3D/render_3D/scene/Render_Scene_3D_Datoviz.cpp create mode 100644 render_3D/render_3D/scene/Render_Scene_3D_Model.ipp create mode 100644 render_3D/render_3D/scene/detail/Datoviz_Frame_Pipeline.ipp create mode 100644 render_3D/render_3D/scene/detail/Datoviz_Frame_Target.ipp create mode 100644 render_3D/render_3D/scene/detail/Datoviz_Interaction.ipp create mode 100644 render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp create mode 100644 render_3D/render_3D/scene/detail/Datoviz_Scene_Resources.ipp create mode 100644 render_3D/render_3D/scene/rely_facade.h create mode 100644 render_3D/render_3D/visual/Basic_Visual.cpp create mode 100644 render_3D/render_3D/visual/Basic_Visual.hpp create mode 100644 render_3D/render_3D/visual/Basic_Visual.ipp create mode 100644 render_3D/render_3D/visual/Glyph_Visual.cpp create mode 100644 render_3D/render_3D/visual/Glyph_Visual.hpp create mode 100644 render_3D/render_3D/visual/Glyph_Visual.ipp create mode 100644 render_3D/render_3D/visual/Image_Visual.cpp create mode 100644 render_3D/render_3D/visual/Image_Visual.hpp create mode 100644 render_3D/render_3D/visual/Image_Visual.ipp create mode 100644 render_3D/render_3D/visual/Labels_Visual.cpp create mode 100644 render_3D/render_3D/visual/Labels_Visual.hpp create mode 100644 render_3D/render_3D/visual/Labels_Visual.ipp create mode 100644 render_3D/render_3D/visual/Marker_Visual.cpp create mode 100644 render_3D/render_3D/visual/Marker_Visual.hpp create mode 100644 render_3D/render_3D/visual/Marker_Visual.ipp create mode 100644 render_3D/render_3D/visual/Mesh_Visual.cpp create mode 100644 render_3D/render_3D/visual/Mesh_Visual.hpp create mode 100644 render_3D/render_3D/visual/Mesh_Visual.ipp create mode 100644 render_3D/render_3D/visual/Path_Visual.cpp create mode 100644 render_3D/render_3D/visual/Path_Visual.hpp create mode 100644 render_3D/render_3D/visual/Path_Visual.ipp create mode 100644 render_3D/render_3D/visual/Pixel_Visual.cpp create mode 100644 render_3D/render_3D/visual/Pixel_Visual.hpp create mode 100644 render_3D/render_3D/visual/Pixel_Visual.ipp create mode 100644 render_3D/render_3D/visual/Point_Visual.cpp create mode 100644 render_3D/render_3D/visual/Point_Visual.hpp create mode 100644 render_3D/render_3D/visual/Point_Visual.ipp create mode 100644 render_3D/render_3D/visual/Prepared_Visual.hpp create mode 100644 render_3D/render_3D/visual/Primitive_Visual.cpp create mode 100644 render_3D/render_3D/visual/Primitive_Visual.hpp create mode 100644 render_3D/render_3D/visual/Primitive_Visual.ipp create mode 100644 render_3D/render_3D/visual/Segment_Visual.cpp create mode 100644 render_3D/render_3D/visual/Segment_Visual.hpp create mode 100644 render_3D/render_3D/visual/Segment_Visual.ipp create mode 100644 render_3D/render_3D/visual/Sphere_Visual.cpp create mode 100644 render_3D/render_3D/visual/Sphere_Visual.hpp create mode 100644 render_3D/render_3D/visual/Sphere_Visual.ipp create mode 100644 render_3D/render_3D/visual/Splat_Visual.cpp create mode 100644 render_3D/render_3D/visual/Splat_Visual.hpp create mode 100644 render_3D/render_3D/visual/Splat_Visual.ipp create mode 100644 render_3D/render_3D/visual/Text_Visual.cpp create mode 100644 render_3D/render_3D/visual/Text_Visual.hpp create mode 100644 render_3D/render_3D/visual/Text_Visual.ipp create mode 100644 render_3D/render_3D/visual/Vector_Visual.cpp create mode 100644 render_3D/render_3D/visual/Vector_Visual.hpp create mode 100644 render_3D/render_3D/visual/Vector_Visual.ipp create mode 100644 render_3D/render_3D/visual/Visuals.hpp create mode 100644 render_3D/render_3D/visual/Volume_Visual.cpp create mode 100644 render_3D/render_3D/visual/Volume_Visual.hpp create mode 100644 render_3D/render_3D/visual/Volume_Visual.ipp create mode 100644 render_3D/render_3D/visual/detail/Datoviz_Visual_Common.ipp create mode 100644 render_3D/render_3D/visual/detail/Datoviz_Visual_Factories.ipp create mode 100644 render_3D/render_3D/visual/detail/Datoviz_Visual_Operations.hpp create mode 100644 render_3D/render_3D/visual/detail/Datoviz_Visual_Operations.ipp create mode 100644 render_3D/render_3D/visual/detail/Datoviz_Visual_Set.ipp create mode 100644 render_3D/render_3D/visual/detail/Datoviz_Visual_State.hpp create mode 100644 render_3D/render_3D/visual/detail/Visual_Prepare.hpp create mode 100644 render_3D/render_3D/visual/detail/Visual_Prepare.ipp create mode 100644 render_3D/tests/Async_Visual_Contract_Test.cpp create mode 100644 render_3D/tests/Visual_Tests.cpp diff --git a/kernel/kernel/include/global.hpp b/kernel/kernel/include/global.hpp index 552a23a..7b15c8a 100644 --- a/kernel/kernel/include/global.hpp +++ b/kernel/kernel/include/global.hpp @@ -26,6 +26,7 @@ PRO_MEM_DISPATCH(clear); PRO_MEM_DISPATCH(empty); PRO_MEM_DISPATCH(run); PRO_MEM_DISPATCH(run_observed); +PRO_MEM_DISPATCH(corun_until); PRO_MEM_DISPATCH(use); PRO_MEM_DISPATCH(now); PRO_MEM_DISPATCH(render); diff --git a/kernel/kernel/include/model/Model.hpp b/kernel/kernel/include/model/Model.hpp index 64624d2..f562e6b 100644 --- a/kernel/kernel/include/model/Model.hpp +++ b/kernel/kernel/include/model/Model.hpp @@ -41,7 +41,9 @@ struct Select_Model_Private_API; template struct Compose_Model_APIs; -struct Empty_Model_Value {}; /* 某 Tag 在首个定义层没有前驱值时使用的空根。 */ +struct Empty_Model_Value { + bool operator==(const Empty_Model_Value&) const = default; +}; /* 某 Tag 在首个定义层没有前驱值时使用的空根。 */ template struct Endpoint_Attachments; template diff --git a/kernel/kernel/module/task_flow/export/export.h b/kernel/kernel/module/task_flow/export/export.h index c236761..fa2432c 100644 --- a/kernel/kernel/module/task_flow/export/export.h +++ b/kernel/kernel/module/task_flow/export/export.h @@ -67,7 +67,13 @@ struct Task_Graph_Common_Facade : facade_builder struct Task_Graph_Build_Facade : facade_builder::add_facade_with_substitution ::add_convention<_add, std::expected, Add_Task_Graph_Result>(std::string, std::function < void() >)> ::add_convention<_clear, void()>::build {}; -struct Task_Graph_Compose_Facade : facade_builder::add_facade_with_substitution::add_convention<_compose, std::expected, Compose_Task_Graph_Result>(std::string, proxy &)>::add_convention<_precede, Precede_Task_Graph_Result(proxy &, proxy &)>::add_convention<_run, Run_Taskflow_Result(Taskflow_Completion)>::build {}; +struct Task_Graph_Compose_Facade : facade_builder +::add_facade_with_substitution +::add_convention<_compose, std::expected, Compose_Task_Graph_Result>(std::string, proxy &)> +::add_convention<_precede, Precede_Task_Graph_Result(proxy &, proxy &)> +::add_convention<_run, Run_Taskflow_Result(Taskflow_Completion)> +::add_convention<_corun_until, void(std::function)> +::build {}; struct Task_Graph_Facade : facade_builder::add_facade_with_substitution::add_facade_with_substitution::build {}; proxy make_task_graph(std::string name); } // namespace task_flow diff --git a/kernel/kernel/module/task_flow/src/Task_Graph_Model.hpp b/kernel/kernel/module/task_flow/src/Task_Graph_Model.hpp index a4a3589..415d43b 100644 --- a/kernel/kernel/module/task_flow/src/Task_Graph_Model.hpp +++ b/kernel/kernel/module/task_flow/src/Task_Graph_Model.hpp @@ -19,6 +19,7 @@ struct Task_Graph_Model::Private : Prev_Private { void clear(); bool empty() const noexcept; Run_Taskflow_Result run(Taskflow_Completion completion); + void corun_until(std::function predicate); private: std::shared_ptr graph; /* 所有消费 facade 共享的唯一权威任务图。 */ }; diff --git a/kernel/kernel/module/task_flow/src/Task_Runtime.cpp b/kernel/kernel/module/task_flow/src/Task_Runtime.cpp index e0c4b2e..dc66b97 100644 --- a/kernel/kernel/module/task_flow/src/Task_Runtime.cpp +++ b/kernel/kernel/module/task_flow/src/Task_Runtime.cpp @@ -50,8 +50,7 @@ void* task_observation(const tf::TaskView& task) noexcept { static_assert(alignof(tf::TaskView) == alignof(tf::Node*)); static_assert(sizeof(tf::Task) == sizeof(tf::Node*)); static_assert(alignof(tf::Task) == alignof(tf::Node*)); - auto* node = - *reinterpret_cast(std::addressof(task)); + auto* node = *reinterpret_cast(std::addressof(task)); tf::Task handle{}; *reinterpret_cast(std::addressof(handle)) = node; return handle.data(); @@ -59,9 +58,7 @@ void* task_observation(const tf::TaskView& task) noexcept { const tf::Worker& task_worker(const tf::WorkerView& worker) noexcept { static_assert(sizeof(tf::WorkerView) == sizeof(tf::Worker*)); static_assert(alignof(tf::WorkerView) == alignof(tf::Worker*)); - const auto* native_worker = - *reinterpret_cast( - std::addressof(worker)); + const auto* native_worker = *reinterpret_cast(std::addressof(worker)); return *native_worker; } struct Task_Observer final : tf::ObserverInterface { @@ -71,30 +68,22 @@ struct Task_Observer final : tf::ObserverInterface { if (observation == nullptr) { return; } - auto& execution = *static_cast< - detail::Taskflow_Observation_Execution*>(observation); - execution.observe_entry( - task_worker(worker), - static_cast(task.hash_value()), - Clock::now()); + auto& execution = *static_cast(observation); + execution.observe_entry(task_worker(worker), static_cast(task.hash_value()), Clock::now()); } void on_exit(tf::WorkerView worker, tf::TaskView task) override { auto* observation = task_observation(task); if (observation == nullptr) { return; } - auto& execution = *static_cast< - detail::Taskflow_Observation_Execution*>(observation); - execution.observe_exit( - task_worker(worker), - static_cast(task.hash_value()), - Clock::now()); + auto& execution = *static_cast(observation); + execution.observe_exit(task_worker(worker), static_cast(task.hash_value()), Clock::now()); } }; enum struct Runtime_Phase : std::uint8_t { - uninitialized, - initializing, - ready + uninitialized, /* No Executor or completion boundary has been published. */ + initializing, /* One caller is constructing the process Runtime. */ + ready /* The Executor accepts asynchronous graph submissions. */ }; struct Taskflow_Execution { Taskflow_Execution(std::shared_ptr graph_execution, detail::Taskflow_Observation_Execution observation_execution, Taskflow_Completion completion_function, const detail::Taskflow_Completion_Boundary& completion_boundary) : observation(std::move(observation_execution)), graph(std::move(graph_execution)), completion(std::move(completion_function)), boundary(completion_boundary) {} @@ -118,10 +107,12 @@ private: std::unique_ptr executor; /* 进程级 Taskflow Executor 的唯一所有权。 */ std::atomic phase{Runtime_Phase::uninitialized}; /* 一次性初始化状态。 */ public: - Run_Taskflow_Result run( - Task_Graph& graph, - Taskflow_Completion completion, - std::optional observation) { + void corun_until(std::function predicate) { + if (!ready()) throw std::logic_error("task runtime is not initialized"); + if (!predicate) throw std::invalid_argument("task graph cooperative predicate is empty"); + executor->corun_until(std::move(predicate)); + } + Run_Taskflow_Result run(Task_Graph& graph, Taskflow_Completion completion, std::optional observation) { if (!ready()) { return Run_Taskflow_Result::runtime_not_initialized; } @@ -143,23 +134,16 @@ public: if (!observation) { return detail::Taskflow_Observation_Execution{}; } - return detail::Taskflow_Observation_Execution::try_start( - std::move(*observation), - executor->num_workers(), - std::move(taskflow_name), - std::move(nodes)); + return detail::Taskflow_Observation_Execution::try_start(std::move(*observation), executor->num_workers(), std::move(taskflow_name), std::move(nodes)); }(); if (!observation_execution) { return Run_Taskflow_Result::observation_unavailable; } auto execution = std::make_shared(std::move(graph_execution), std::move(*observation_execution), std::move(completion), *completion_boundary); - execution->graph->bind_observation( - execution->observation.binding()); - (void)executor->run( - execution->graph->native_taskflow(), - [execution] { - execution->complete(Clock::now()); - }); + execution->graph->bind_observation(execution->observation.binding()); + (void)executor->run(execution->graph->native_taskflow(), [execution] { + execution->complete(Clock::now()); + }); return Run_Taskflow_Result::submitted; } static Task_Resource& instance() { @@ -174,11 +158,7 @@ public: if (failure_policy == Unknown_Failure_Policy::exception && !exception_boundary) return Initialize_Task_Runtime_Result::exception_boundary_missing; if (failure_policy == Unknown_Failure_Policy::fast_fail && exception_boundary) return Initialize_Task_Runtime_Result::exception_boundary_unexpected; auto expected = Runtime_Phase::uninitialized; - if (!phase.compare_exchange_strong( - expected, - Runtime_Phase::initializing, - std::memory_order_acq_rel, - std::memory_order_acquire)) { + if (!phase.compare_exchange_strong(expected, Runtime_Phase::initializing, std::memory_order_acq_rel, std::memory_order_acquire)) { return expected == Runtime_Phase::ready ? Initialize_Task_Runtime_Result::already_initialized : Initialize_Task_Runtime_Result::initialization_in_progress; @@ -193,9 +173,7 @@ public: phase.store(Runtime_Phase::ready, std::memory_order_release); } catch (...) { - phase.store( - Runtime_Phase::uninitialized, - std::memory_order_release); + phase.store(Runtime_Phase::uninitialized, std::memory_order_release); Failure_Policy::handle_unknown_failure(std::current_exception()); } return Initialize_Task_Runtime_Result::initialized; @@ -205,21 +183,14 @@ public: Initialize_Task_Runtime_Result initialize_task_runtime(std::size_t workers, Unknown_Failure_Policy failure_policy, Taskflow_Exception_Boundary exception_boundary) { return Task_Resource::instance().initialize(workers, failure_policy, std::move(exception_boundary)); } -Run_Taskflow_Result run_taskflow( - Task_Graph& graph, - Taskflow_Completion completion) { - return Task_Resource::instance().run( - graph, - std::move(completion), - std::nullopt); +Run_Taskflow_Result run_taskflow(Task_Graph& graph, Taskflow_Completion completion) { + return Task_Resource::instance().run(graph, std::move(completion), std::nullopt); } -Run_Taskflow_Result run_taskflow( - Task_Graph& graph, - Taskflow_Completion completion, - Taskflow_Observation observation) { - return Task_Resource::instance().run( - graph, - std::move(completion), - std::move(observation)); +Run_Taskflow_Result run_taskflow(Task_Graph& graph, Taskflow_Completion completion, Taskflow_Observation observation) { + return Task_Resource::instance().run(graph, std::move(completion), std::move(observation)); +} + +void detail::corun_taskflow_until(std::function predicate) { + Task_Resource::instance().corun_until(std::move(predicate)); } } diff --git a/kernel/kernel/module/task_flow/src/Task_Runtime.hpp b/kernel/kernel/module/task_flow/src/Task_Runtime.hpp index 254acf2..7cb76d8 100644 --- a/kernel/kernel/module/task_flow/src/Task_Runtime.hpp +++ b/kernel/kernel/module/task_flow/src/Task_Runtime.hpp @@ -9,13 +9,11 @@ #include namespace aethera { Initialize_Task_Runtime_Result initialize_task_runtime(std::size_t workers = std::thread::hardware_concurrency(), Unknown_Failure_Policy failure_policy = Unknown_Failure_Policy::fast_fail, Taskflow_Exception_Boundary exception_boundary = {}); -/* 完成回调接收业务任务传播出的 Unknown Failure;本次执行不记录观察数据。 */ -Run_Taskflow_Result run_taskflow( - Task_Graph& graph, - Taskflow_Completion completion); -/* 完成回调接收业务任务传播出的 Unknown Failure;observation 记录本次执行。 */ -Run_Taskflow_Result run_taskflow( - Task_Graph& graph, - Taskflow_Completion completion, - Taskflow_Observation observation); +namespace detail { +void corun_taskflow_until(std::function predicate); +} +/* 完成回调接收业务任务传播出的 Unknown Failure;本次执行不记录观察数据。 */ +Run_Taskflow_Result run_taskflow(Task_Graph& graph, Taskflow_Completion completion); +/* 完成回调接收业务任务传播出的 Unknown Failure;observation 记录本次执行。 */ +Run_Taskflow_Result run_taskflow(Task_Graph& graph, Taskflow_Completion completion, Taskflow_Observation observation); } diff --git a/kernel/kernel/module/task_flow/src/task_flow_facade.cpp b/kernel/kernel/module/task_flow/src/task_flow_facade.cpp index befe9cb..59adb90 100644 --- a/kernel/kernel/module/task_flow/src/task_flow_facade.cpp +++ b/kernel/kernel/module/task_flow/src/task_flow_facade.cpp @@ -42,6 +42,9 @@ bool Task_Graph_Model::Private::empty() const noexcept { Run_Taskflow_Result Task_Graph_Model::Private::run(Taskflow_Completion completion) { return run_taskflow(*graph, std::move(completion)); } +void Task_Graph_Model::Private::corun_until(std::function predicate) { + aethera::detail::corun_taskflow_until(std::move(predicate)); +} proxy make_task_graph(std::string name) { return make_model_proxy(std::make_shared(std::move(name))); } diff --git a/render_3D/render_3D/Render_3D.hpp b/render_3D/render_3D/Render_3D.hpp new file mode 100644 index 0000000..ca8e74d --- /dev/null +++ b/render_3D/render_3D/Render_3D.hpp @@ -0,0 +1,5 @@ +#pragma once +#include "axis/Axes_3D.hpp" +#include "camera/Camera_3D.hpp" +#include "scene/Render_Scene_3D.hpp" +#include "visual/Visuals.hpp" diff --git a/render_3D/render_3D/axis/Axes_3D.hpp b/render_3D/render_3D/axis/Axes_3D.hpp new file mode 100644 index 0000000..ec444d9 --- /dev/null +++ b/render_3D/render_3D/axis/Axes_3D.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "Axis.hpp" +#include "model/Model.hpp" +#include "model/attachment/structure/Struct_Concurrent.hpp" +#include +namespace aethera::render_3d { +struct Axes_3D : Def> { + struct Prop : Prev { + plot::Axis_Descriptor x_axis{{0.0, 4.0}, plot::Axis_Scale::time, "Time", "s", 6, 1, true, true, true}; + plot::Axis_Descriptor y_axis{{10.0, 20'000.0}, plot::Axis_Scale::logarithmic, "Frequency", "Hz", 6, 1, true, true, true}; + plot::Axis_Descriptor z_axis{{18.0, 78.0}, plot::Axis_Scale::linear, "SPL", "dB", 7, 0, true, true, true}; + bool operator==(const Prop&) const = default; + }; + + struct Private; +}; + +struct Axes_3D::Private : Prev_Private { + void advance() { + concurrent().internal.advance(); + } + [[nodiscard]] std::array use() const { + const auto& prop = static_cast(*concurrent().internal.use()); + return {prop.x_axis, prop.y_axis, prop.z_axis}; + } +}; +} // namespace aethera::render_3d diff --git a/render_3D/render_3D/axis/Axis.cpp b/render_3D/render_3D/axis/Axis.cpp new file mode 100644 index 0000000..0545235 --- /dev/null +++ b/render_3D/render_3D/axis/Axis.cpp @@ -0,0 +1,167 @@ +#include "Axis.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +namespace aethera::render_3d::plot { +namespace { +std::string trim_number(std::string value) { + const auto exponent = value.find_first_of("eE"); + const auto fraction_end = exponent == std::string::npos ? value.size() : exponent; + const auto decimal = value.find('.'); + if (decimal == std::string::npos || decimal >= fraction_end) return value; + auto last = fraction_end; + while (last > decimal + 1 && value[last - 1] == '0') --last; + if (last == decimal + 1) --last; + value.erase(last, fraction_end - last); + return value; +} + +std::string numeric_label(double value, int precision) { + std::ostringstream stream; + const auto magnitude = std::abs(value); + std::string suffix; + if (magnitude >= 1.0e3 && magnitude < 1.0e6) { + value /= 1.0e3; + suffix = "k"; + stream << std::fixed; + } + else if (magnitude >= 1.0e6 || (magnitude > 0.0 && magnitude < 1.0e-4)) stream << std::scientific; + else stream << std::fixed; + stream << std::setprecision(std::clamp(precision, 0, 12)) << value; + return trim_number(stream.str()) + suffix; +} + +bool ascii_iequals(std::string_view left, std::string_view right) { + return left.size() == right.size() && std::ranges::equal(left, right, [](char first, char second) { return std::tolower(static_cast(first)) == std::tolower(static_cast(second)); }); +} + +std::string time_label(double seconds, int precision, std::string_view unit) { + if (!unit.empty() && !ascii_iequals(unit, "s") && !ascii_iequals(unit, "sec") && !ascii_iequals(unit, "second") && !ascii_iequals(unit, "seconds")) return numeric_label(seconds, precision) + " " + std::string(unit); + const auto sign = seconds < 0.0 ? "-" : ""; + const auto absolute = std::abs(seconds); + if (absolute == 0.0) return "0 s"; + if (absolute < 1.0) return std::string(sign) + numeric_label(absolute * 1'000.0, precision) + " ms"; + if (absolute < 60.0) return std::string(sign) + numeric_label(absolute, precision) + " s"; + const auto hours = static_cast(absolute / 3'600.0); + const auto minutes = static_cast(absolute / 60.0) % 60U; + const auto remaining = absolute - static_cast(hours * 3'600U + minutes * 60U); + std::ostringstream stream; + stream << sign; + if (hours != 0U) stream << hours << ':' << std::setfill('0') << std::setw(2); + stream << minutes << ':' << std::setfill('0') << std::setw(precision > 0 ? precision + 3 : 2) << std::fixed << std::setprecision(std::clamp(precision, 0, 6)) << remaining; + return stream.str(); +} + +std::string frequency_label(double frequency, int precision) { + const auto magnitude = std::abs(frequency); + double divisor{1.0}; + std::string_view unit{"Hz"}; + if (magnitude >= 1.0e9) { + divisor = 1.0e9; + unit = "GHz"; + } + else if (magnitude >= 1.0e6) { + divisor = 1.0e6; + unit = "MHz"; + } + else if (magnitude >= 1.0e3) { + divisor = 1.0e3; + unit = "kHz"; + } + return numeric_label(frequency / divisor, precision) + " " + std::string(unit); +} + +std::string axis_tick_label(const Axis_Descriptor& axis, double coordinate) { + if (axis.scale == Axis_Scale::time) return time_label(coordinate, axis.precision, axis.unit); + if (ascii_iequals(axis.unit, "hz")) return frequency_label(coordinate, axis.precision); + return numeric_label(coordinate, axis.precision); +} + +std::vector linear_ticks(const Axis_Descriptor& axis) { + const auto step = nice_tick_step(axis.range, axis.target_tick_count); + if (!(step > 0.0) || !std::isfinite(step)) return {}; + const auto [minimum, maximum] = std::minmax(axis.range.origin, axis.range.target); + const auto first = std::ceil(minimum / step) * step; + std::vector ticks; + const auto maximum_count = std::max(2, axis.target_tick_count * 4 + 4); + for (std::size_t index = 0; index < maximum_count; ++index) { + const auto coordinate = first + static_cast(index) * step; + if (coordinate > maximum + step * 1.0e-9) break; + ticks.push_back({coordinate, axis_tick_label(axis, coordinate)}); + } + if (axis.range.length() < 0.0) std::ranges::reverse(ticks); + return ticks; +} + +std::vector logarithmic_ticks(const Axis_Descriptor& axis) { + const auto [minimum, maximum] = std::minmax(axis.range.origin, axis.range.target); + if (!(minimum > 0.0) || !std::isfinite(maximum)) return {}; + const auto first_power = static_cast(std::floor(std::log10(minimum))); + const auto last_power = static_cast(std::ceil(std::log10(maximum))); + const auto decade_count = static_cast(last_power - first_power + 1); + const auto multiples = axis.target_tick_count <= decade_count ? std::vector{1.0} : axis.target_tick_count <= decade_count * 2 ? std::vector{1.0, 5.0} : std::vector{1.0, 2.0, 5.0}; + std::vector ticks; + for (int power = first_power; power <= last_power; ++power) { + const auto decade = std::pow(10.0, static_cast(power)); + for (const double multiple : multiples) { + const auto coordinate = multiple * decade; + if (coordinate >= minimum * (1.0 - 1.0e-12) && coordinate <= maximum * (1.0 + 1.0e-12)) ticks.push_back({coordinate, axis_tick_label(axis, coordinate)}); + } + } + const auto add_boundary = [&](double coordinate) { + const auto tolerance = coordinate * 1.0e-9; + if (std::ranges::none_of(ticks, [&](const Axis_Tick& tick) { return std::abs(tick.coordinate - coordinate) <= tolerance; })) { + const auto logarithmic_span = std::log(maximum / minimum); + const auto minimum_spacing = 0.5 / static_cast(std::max(2, axis.target_tick_count) - 1); + if (logarithmic_span > 0.0) { + const auto nearest = std::ranges::min_element(ticks, {}, [&](const Axis_Tick& tick) { return std::abs(std::log(tick.coordinate / coordinate)) / logarithmic_span; }); + if (nearest != ticks.end() && std::abs(std::log(nearest->coordinate / coordinate)) / logarithmic_span < minimum_spacing) ticks.erase(nearest); + } + ticks.push_back({coordinate, axis_tick_label(axis, coordinate)}); + } + }; + add_boundary(minimum); + add_boundary(maximum); + std::ranges::sort(ticks, {}, &Axis_Tick::coordinate); + if (axis.range.length() < 0.0) std::ranges::reverse(ticks); + return ticks; +} +} // namespace + +Axis_Coordinate Axis_Range::size() const noexcept { + return std::abs(length()); +} + +Axis_Coordinate Axis_Range::length() const noexcept { + return target - origin; +} + +Axis_Coordinate Axis_Range::center() const noexcept { + return origin + length() * 0.5; +} + +bool Axis_Range::contains(Axis_Coordinate coordinate) const noexcept { + const auto [minimum, maximum] = std::minmax(origin, target); + return coordinate >= minimum - 1.0e-9 && coordinate <= maximum + 1.0e-9; +} + +Axis_Coordinate nice_tick_step(Axis_Range range, std::size_t target_tick_count) { + if (!std::isfinite(range.origin) || !std::isfinite(range.target) || range.size() <= 0.0) throw std::invalid_argument("axis range must be finite and non-empty"); + const auto count = std::max(2, target_tick_count); + const auto raw = range.size() / static_cast(count - 1); + const auto magnitude = std::pow(10.0, std::floor(std::log10(raw))); + const auto normalized = raw / magnitude; + const auto nice = normalized <= 1.0 ? 1.0 : normalized <= 2.0 ? 2.0 : normalized <= 5.0 ? 5.0 : 10.0; + return nice * magnitude; +} + +std::vector axis_ticks(const Axis_Descriptor& axis) { + if (!axis.visible) return {}; + return axis.scale == Axis_Scale::logarithmic ? logarithmic_ticks(axis) : linear_ticks(axis); +} +} // namespace aethera::render_3d::plot diff --git a/render_3D/render_3D/axis/Axis.hpp b/render_3D/render_3D/axis/Axis.hpp new file mode 100644 index 0000000..2b0ca2a --- /dev/null +++ b/render_3D/render_3D/axis/Axis.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include +#include +#include +namespace aethera::render_3d::plot { +using Axis_Coordinate = double; + +enum struct Axis_Scale : std::uint8_t { + linear, /* Uniform numeric spacing. */ + logarithmic, /* Base-ten logarithmic spacing. */ + time /* Time-oriented labels on a linear coordinate. */ +}; + +struct Axis_Range { + Axis_Coordinate origin{}; + Axis_Coordinate target{}; + [[nodiscard]] Axis_Coordinate size() const noexcept; + [[nodiscard]] Axis_Coordinate length() const noexcept; + [[nodiscard]] Axis_Coordinate center() const noexcept; + [[nodiscard]] bool contains(Axis_Coordinate coordinate) const noexcept; + bool operator==(const Axis_Range&) const = default; +}; + +struct Axis_Descriptor { + Axis_Range range{}; + Axis_Scale scale{Axis_Scale::linear}; + std::string label{}; + std::string unit{}; + std::size_t target_tick_count{6}; + int precision{2}; + bool visible{true}; + bool grid_visible{true}; + bool labels_visible{true}; + bool operator==(const Axis_Descriptor&) const = default; +}; + +struct Axis_Tick { + Axis_Coordinate coordinate{}; + std::string label{}; + bool operator==(const Axis_Tick&) const = default; +}; + +[[nodiscard]] Axis_Coordinate nice_tick_step(Axis_Range range, std::size_t target_tick_count = 6); +[[nodiscard]] std::vector axis_ticks(const Axis_Descriptor& axis); +} // namespace aethera::render_3d::plot diff --git a/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp b/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp new file mode 100644 index 0000000..260c270 --- /dev/null +++ b/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp @@ -0,0 +1,100 @@ +#pragma once +#include +#include +#include +namespace aethera::render_3d { +enum struct Datoviz_Frame_Path : std::uint8_t { + recorded, /* Recorded a new command buffer for this frame. */ + updated, /* Reused commands after updating dynamic bindings. */ + reused /* Reused the previously recorded commands unchanged. */ +}; + +struct Datoviz_Gpu_Timing { + std::uint64_t render_ns{}; /* GPU 绘制阶段耗时,单位为纳秒。 */ + std::uint64_t transition_ns{}; /* GPU 布局转换耗时,单位为纳秒。 */ + std::uint64_t copy_ns{}; /* GPU 读回复制耗时,单位为纳秒。 */ + std::uint64_t total_ns{}; /* GPU 整帧总耗时,单位为纳秒。 */ +}; + +/* + * Datoviz 的逐帧诊断直接属于产生它的 Frame_3D。它随物理帧槽 begin() 清空, + * 后端完成后一次写入,调用方只在完成边界读取;这里没有旁路历史或状态快照。 + */ +struct Datoviz_Frame_Observation { + std::uint64_t render_sequence{}; /* Scene 分配的帧序号。 */ + Datoviz_Frame_Path path{Datoviz_Frame_Path::recorded}; /* 本帧录制、更新动态绑定或直接复用命令。 */ + bool gpu_timing_requested{}; /* 本帧是否读取已录制的 GPU 时间戳。 */ + bool readback_requested{}; /* 本帧是否请求最终 RGBA 读回。 */ + bool controller_input_applied{}; /* 本帧是否把已提交输入应用到 Datoviz 控制器。 */ + bool prepare_released_after_submission{}; /* 本帧实际提交后是否立即允许下一帧 Prepare。 */ + std::uint64_t queue_submit_wait_ns{}; /* 获取共享 VkQueue 提交权的墙钟耗时。 */ + std::uint64_t target_acquire_ns{}; /* 查找或创建本帧三槽目标的墙钟耗时。 */ + std::uint64_t structure_check_ns{}; /* 判断命令结构是否变化的墙钟耗时。 */ + std::uint64_t apply_ns{}; /* 应用 Camera、Axes 和 Visual 数据的墙钟耗时。 */ + std::uint64_t query_ns{}; /* 处理 Datoviz 查询与悬浮读数的墙钟耗时。 */ + std::uint64_t runtime_plan_ns{}; /* 生成增量 Runtime 资源命令流的墙钟耗时。 */ + std::uint64_t runtime_plan_cpu_ns{}; /* 生成增量 Runtime 资源命令流的当前线程 CPU 耗时。 */ + std::uint64_t runtime_execute_ns{}; /* 执行增量 Runtime 资源命令流的墙钟耗时。 */ + std::uint64_t runtime_execute_cpu_ns{}; /* 执行增量 Runtime 资源命令流的当前线程 CPU 耗时。 */ + std::uint64_t mvp_update_ns{}; /* 更新可复用命令所引用 MVP 缓冲的墙钟耗时。 */ + std::uint64_t frame_begin_ns{}; /* 重置并开始目标命令缓冲录制的墙钟耗时。 */ + std::uint64_t frame_plan_ns{}; /* 生成完整 Datoviz 帧计划的墙钟耗时。 */ + std::uint64_t frame_plan_cpu_ns{}; /* 生成完整 Datoviz 帧计划的当前线程 CPU 耗时。 */ + std::uint64_t emit_replay_dirty_ns{}; /* 恢复本 Runtime 需要重放的 payload dirty 状态。 */ + std::uint64_t emit_layout_ns{}; /* 布局与屏幕缩放状态求解墙钟。 */ + std::uint64_t emit_prepare_ns{}; /* Guide、bar、band 等 Scene 装饰准备墙钟。 */ + std::uint64_t emit_plan_build_ns{}; /* Upload、compute 与 panel render 节点建图墙钟。 */ + std::uint64_t emit_contract_ns{}; /* 透明模式与 FramePlan contract 验证墙钟。 */ + std::uint64_t emit_stream_ns{}; /* FramePlan 转换为增量 DRP2 stream 的墙钟。 */ + std::uint64_t emit_stream_freeze_ns{}; /* DRP2 stream 借用 payload 固化墙钟。 */ + std::uint64_t emit_commit_ns{}; /* 成功发布后提交 Scene dirty 状态的墙钟。 */ + std::uint64_t emit_plan_reset_ns{}; /* FramePlan 语义清理且保留 arena 容量的墙钟。 */ + std::uint64_t emit_artifact_create_ns{}; /* 帧 artifact 本体创建墙钟。 */ + std::uint64_t emit_artifact_freeze_ns{}; /* artifact 边界的 payload 固化复查墙钟。 */ + std::uint64_t emit_packet_encode_ns{}; /* setup/update/frame 三段 packet 编码墙钟。 */ + std::uint64_t external_register_ns{}; /* 注册外部属性缓冲的墙钟耗时。 */ + std::uint64_t frame_attach_ns{}; /* 把借用目标附加到 DRP2 Runtime 的墙钟耗时。 */ + std::uint64_t frame_execute_ns{}; /* 执行完整帧命令流的墙钟耗时。 */ + std::uint64_t frame_execute_cpu_ns{}; /* 执行完整帧命令流的当前线程 CPU 耗时。 */ + std::uint64_t frame_finish_ns{}; /* 完成命令录制并建立提交描述的墙钟耗时。 */ + std::uint64_t drp_validation_ns{}; /* DRP2 语义验证与状态复制耗时。 */ + std::uint64_t drp_state_ns{}; /* DRP2 状态建立与提交耗时。 */ + std::uint64_t drp_buffer_create_ns{}; /* Vulkan Buffer 创建命令耗时。 */ + std::uint64_t drp_texture_create_ns{}; /* Vulkan Texture 创建命令耗时。 */ + std::uint64_t drp_shader_create_ns{}; /* Vulkan Shader Module 创建命令耗时。 */ + std::uint64_t drp_shader_compile_ns{}; /* Shader Module 命令中 GLSL 到 SPIR-V 的编译耗时。 */ + std::uint64_t drp_shader_module_create_ns{}; /* Shader Module 命令中 vkCreateShaderModule 的耗时。 */ + std::uint64_t drp_pipeline_create_ns{}; /* Vulkan Pipeline 创建命令耗时。 */ + std::uint64_t drp_binding_create_ns{}; /* Sampler、Layout 与 Bind Group 创建命令耗时。 */ + std::uint64_t drp_upload_ns{}; /* Buffer 与 Texture 写入命令耗时。 */ + std::uint64_t drp_upload_decode_ns{}; /* Base64 解码。 */ + std::uint64_t drp_upload_vulkan_allocate_ns{}; /* Staging Vulkan allocation。 */ + std::uint64_t drp_upload_host_copy_ns{}; /* Host 到 mapped buffer 的 memcpy。 */ + std::uint64_t drp_upload_command_allocate_ns{}; /* 临时 command pool/buffer allocation。 */ + std::uint64_t drp_upload_command_record_ns{}; /* Upload copy 命令录制。 */ + std::uint64_t drp_upload_fence_create_ns{}; /* Upload fence 创建。 */ + std::uint64_t drp_upload_submit_enqueue_ns{}; /* 进入唯一 VkQueue 提交通道。 */ + std::uint64_t drp_upload_submit_queue_wait_ns{}; /* 在提交通道中的排队墙钟。 */ + std::uint64_t drp_upload_queue_submit_ns{}; /* vkQueueSubmit2 调用墙钟。 */ + std::uint64_t drp_upload_fence_wait_ns{}; /* 协作让出期间的 fence 完成墙钟。 */ + std::uint64_t drp_upload_retire_ns{}; /* 已完成临时资源退役。 */ + std::uint64_t drp_transfer_ns{}; /* 显式 Copy 命令耗时。 */ + std::uint64_t drp_record_ns{}; /* Render/Compute Pass 录制命令耗时。 */ + std::uint32_t drp_pipeline_create_count{}; /* 本帧实际创建的图形/计算管线数。 */ + std::uint64_t submit_ns{}; /* Vulkan 队列提交耗时。 */ + std::uint64_t gpu_completion_observation_ns{}; /* 从 watch 到探测到 fence 完成的墙钟耗时。 */ + std::uint64_t completion_task_queue_ns{}; /* 探测到 fence 完成后,Scene completion task 的排队耗时。 */ + std::uint64_t readback_ns{}; /* 已完成 GPU 目标的 CPU 读回耗时。 */ + std::uint64_t uploaded_bytes{}; /* 本帧写入映射属性存储的有效字节数。 */ + std::uint64_t readback_bytes{}; /* 本帧产出的连续像素字节数。 */ + std::optional gpu{}; /* 支持时间戳时的 GPU 分段数据。 */ + std::uint64_t artifact_resource_version{}; /* Datoviz 帧计划资源版本。 */ + std::uint64_t artifact_frame_index{}; /* Datoviz 帧计划内部帧下标。 */ + std::uint32_t artifact_status{}; /* Datoviz 帧计划状态码。 */ + std::uint32_t validation_code{}; /* 帧计划验证结果码。 */ + std::uint64_t validation_command_index{}; /* 首个无效命令下标。 */ + bool validation_performed{}; /* 本帧是否执行了帧计划验证。 */ + bool validation_ok{}; /* 已执行验证时,帧计划是否有效。 */ + std::string artifact_json{}; /* 验证失败时的可选 Datoviz 帧计划 JSON。 */ +}; +} // namespace aethera::render_3d diff --git a/render_3D/render_3D/base/Frame_3D.cpp b/render_3D/render_3D/base/Frame_3D.cpp new file mode 100644 index 0000000..4b3b608 --- /dev/null +++ b/render_3D/render_3D/base/Frame_3D.cpp @@ -0,0 +1,136 @@ +#include "Frame_3D.hpp" +#include +#include +#include +#include +#include +#include +namespace aethera::render_3d { +namespace { +constexpr std::size_t marker_count = static_cast(Frame_Trace_Marker::count); +constexpr std::size_t measurement_count = static_cast(Frame_Trace_Measurement::count); +std::uint64_t encode_present_value(std::uint64_t value) noexcept { + return value == std::numeric_limits::max() ? value : value + 1; +} +} // namespace + +struct Frame_3D::Private { + Pixel_Format output_format{Frame_3D::native_pixel_format}; + Extent extent{}; + std::shared_ptr> pixels{}; + Frame_3D_Output output{Frame_3D_Output::pixels}; + Frame_Identity identity{}; + Frame_Identity rendered_identity{}; + Frame_Request_Source source{Frame_Request_Source::unspecified}; + std::chrono::steady_clock::time_point created_at{}; + std::array markers{}; + std::array measurements{}; + std::atomic_bool taskflow_trace_requested{}; + std::optional datoviz_observation{}; +}; + +Frame_3D::Frame_3D(Frame_Identity identity, Frame_3D_Output output, Pixel_Format output_format) : d(std::make_unique()) { + begin(identity, output, output_format); +} + +Frame_3D::~Frame_3D() = default; + +void Frame_3D::begin(Frame_Identity identity, Frame_3D_Output output, Pixel_Format output_format, Frame_Request_Source source) { + if (output_format != native_pixel_format) throw std::invalid_argument("Frame_3D output pixel format is unsupported"); + d->identity = identity; + d->source = source; + d->output = output; + d->output_format = output_format; + d->extent = {}; + d->pixels.reset(); + d->rendered_identity = {}; + d->datoviz_observation.reset(); + d->created_at = std::chrono::steady_clock::now(); + for (auto& marker : d->markers) marker.store(0, std::memory_order_relaxed); + for (auto& measurement : d->measurements) measurement.store(0, std::memory_order_relaxed); + d->taskflow_trace_requested.store(false, std::memory_order_relaxed); + d->markers[static_cast(Frame_Trace_Marker::created)].store(encode_present_value(0), std::memory_order_relaxed); +} + +Frame_Identity Frame_3D::identity() const noexcept { + return d->identity; +} + +Frame_Request_Source Frame_3D::source() const noexcept { + return d->source; +} + +Extent Frame_3D::extent() const noexcept { + return d->extent; +} + +std::span Frame_3D::pixels() const noexcept { + return d->pixels ? std::span{*d->pixels} : std::span{}; +} + +std::shared_ptr> Frame_3D::share_pixels() const noexcept { + return d->pixels; +} + +Frame_Identity Frame_3D::rendered_identity() const noexcept { + return d->rendered_identity; +} + +Frame_3D_Output Frame_3D::output() const noexcept { + return d->output; +} + +Pixel_Format Frame_3D::output_format() const noexcept { + return d->output_format; +} + +void Frame_3D::mark(Frame_Trace_Marker marker) noexcept { + const auto index = static_cast(marker); + if (index >= marker_count) return; + const auto elapsed = std::chrono::steady_clock::now() - d->created_at; + const auto elapsed_ns = static_cast(std::max(0, std::chrono::duration_cast(elapsed).count())); + std::uint64_t expected{}; + d->markers[index].compare_exchange_strong(expected, encode_present_value(elapsed_ns), std::memory_order_release, std::memory_order_relaxed); +} + +void Frame_3D::record(Frame_Trace_Measurement measurement, std::uint64_t value_ns) noexcept { + const auto index = static_cast(measurement); + if (index >= measurement_count) return; + std::uint64_t expected{}; + d->measurements[index].compare_exchange_strong(expected, encode_present_value(value_ns), std::memory_order_release, std::memory_order_relaxed); +} + +void Frame_3D::request_taskflow_trace() noexcept { + d->taskflow_trace_requested.store(true, std::memory_order_release); +} + +bool Frame_3D::taskflow_trace_requested() const noexcept { + return d->taskflow_trace_requested.load(std::memory_order_acquire); +} + +std::optional Frame_3D::take_datoviz_observation() { + return std::exchange(d->datoviz_observation, {}); +} + +void detail::Frame_3D_Access::assign_pixels(std::span frames, Extent extent, std::vector pixels, Frame_Identity rendered_identity) { + auto shared_pixels = std::make_shared>(std::move(pixels)); + for (Frame_3D* frame : frames) { + if (!frame) throw std::invalid_argument("Frame_3D output target is null"); + frame->d->extent = extent; + frame->d->rendered_identity = rendered_identity; + frame->d->pixels = frame->d->output == Frame_3D_Output::pixels ? shared_pixels : nullptr; + } +} + +void detail::Frame_3D_Access::share_pixels(Frame_3D* source, Frame_3D* target) { + if (!source || !target) throw std::invalid_argument("Frame_3D shared output endpoint is null"); + target->d->extent = source->d->extent; + target->d->rendered_identity = source->d->rendered_identity; + target->d->pixels = target->d->output == Frame_3D_Output::pixels ? source->d->pixels : nullptr; +} + +void detail::Frame_3D_Access::assign_datoviz_observation(Frame_3D* frame, Datoviz_Frame_Observation observation) { + if (!frame) throw std::invalid_argument("Frame_3D diagnostic target is null"); + frame->d->datoviz_observation.emplace(std::move(observation)); +} +} // namespace aethera::render_3d diff --git a/render_3D/render_3D/base/Frame_3D.hpp b/render_3D/render_3D/base/Frame_3D.hpp new file mode 100644 index 0000000..51b13fd --- /dev/null +++ b/render_3D/render_3D/base/Frame_3D.hpp @@ -0,0 +1,158 @@ +#pragma once +#include "Datoviz_Frame_Observation.hpp" +#include "Types.hpp" +#include +#include +#include +#include +#include +#include +namespace aethera::render_3d { +struct Frame_Identity { + std::uint64_t sequence{}; /* 调用方分配的单调逻辑帧序号。 */ + std::uint64_t generation{}; /* 同一物理帧槽的复用代数。 */ + bool operator==(const Frame_Identity&) const = default; +}; + +enum struct Frame_Request_Source : std::uint8_t { + unspecified /* 调用方未声明请求来源。 */ +}; + +enum struct Frame_3D_Output : std::uint8_t { + pixels, /* 返回 RGBA8 像素。 */ + diagnostics /* 仅返回本帧后端诊断。 */ +}; + +enum struct Frame_Trace_Marker : std::uint8_t { + created, /* The caller created or reset the frame. */ + scene_render_entered, /* The public Scene render entry was reached. */ + scene_advance_started, /* Scene and Renderable advance began. */ + scene_advance_finished, /* Scene and Renderable advance completed. */ + scene_render_requested, /* The frame request entered Scene admission. */ + scene_render_started, /* The admitted frame graph started. */ + event_dispatch_started, /* Event routing for this frame began. */ + event_dispatch_finished, /* Event routing for this frame completed. */ + prepare_started, /* CPU preparation began. */ + prepare_finished, /* CPU preparation completed. */ + paint_started, /* Renderable paint tasks began. */ + paint_finished, /* Renderable paint tasks completed. */ + backend_queue_entered, /* The frame entered the Datoviz submission path. */ + backend_prepare_started, /* Datoviz frame preparation began. */ + backend_prepare_finished, /* Datoviz frame preparation completed. */ + backend_submit_queued, /* Vulkan submission was queued. */ + backend_queue_left, /* CPU submission work left the backend path. */ + gpu_submitted, /* Vulkan accepted the frame submission. */ + gpu_completed, /* The completion service observed the fence. */ + readback_started, /* Pixel readback began. */ + readback_finished, /* Pixel readback completed. */ + scene_render_finished, /* Scene completion processing finished. */ + frame_ready, /* The caller completion callback may consume the frame. */ + count /* Number of trace markers; not a marker. */ +}; + +enum struct Frame_Trace_Measurement : std::uint8_t { + backend_apply_ns, /* Apply prepared Scene and Visual state. */ + backend_plan_ns, /* Build the backend frame plan. */ + backend_execute_ns, /* Execute the backend frame plan. */ + backend_target_acquire_ns, /* Acquire a reusable frame target. */ + backend_structure_check_ns, /* Check whether recorded structure changed. */ + backend_query_ns, /* Process backend query state. */ + backend_runtime_plan_ns, /* Plan incremental Runtime commands. */ + backend_runtime_plan_cpu_ns, /* CPU time spent planning Runtime commands. */ + backend_runtime_execute_ns, /* Execute incremental Runtime commands. */ + backend_runtime_execute_cpu_ns, /* CPU time spent executing Runtime commands. */ + backend_mvp_update_ns, /* Update reusable MVP data. */ + backend_frame_begin_ns, /* Begin target command recording. */ + backend_frame_plan_ns, /* Build the complete Datoviz frame plan. */ + backend_frame_plan_cpu_ns, /* CPU time spent building the frame plan. */ + backend_emit_replay_dirty_ns, /* Restore dirty payload state for replay. */ + backend_emit_layout_ns, /* Resolve layout and viewport state. */ + backend_emit_prepare_ns, /* Prepare guides, bars, bands, and decoration. */ + backend_emit_plan_build_ns, /* Build upload, compute, and panel nodes. */ + backend_emit_contract_ns, /* Validate transparent frame-plan contracts. */ + backend_emit_stream_ns, /* Emit the incremental DRP2 stream. */ + backend_emit_stream_freeze_ns, /* Freeze borrowed stream payloads. */ + backend_emit_commit_ns, /* Commit successfully published Scene state. */ + backend_emit_plan_reset_ns, /* Reset frame-plan semantics while retaining capacity. */ + backend_emit_artifact_create_ns, /* Create the frame artifact. */ + backend_emit_artifact_freeze_ns, /* Freeze payloads at the artifact boundary. */ + backend_emit_packet_encode_ns, /* Encode setup, update, and frame packets. */ + backend_external_register_ns, /* Register external attribute buffers. */ + backend_frame_attach_ns, /* Attach the borrowed target to the Runtime. */ + backend_frame_execute_ns, /* Execute the complete frame stream. */ + backend_frame_execute_cpu_ns, /* CPU time spent executing the frame stream. */ + backend_frame_finish_ns, /* Finish recording and create submission state. */ + backend_drp_validation_ns, /* Validate DRP2 semantics and copy state. */ + backend_drp_state_ns, /* Build and submit DRP2 state. */ + backend_drp_buffer_create_ns, /* Create Vulkan buffers. */ + backend_drp_texture_create_ns, /* Create Vulkan textures. */ + backend_drp_shader_create_ns, /* Create shader resources. */ + backend_drp_shader_compile_ns, /* Compile GLSL to SPIR-V. */ + backend_drp_shader_module_create_ns, /* Create Vulkan shader modules. */ + backend_drp_pipeline_create_ns, /* Create Vulkan graphics or compute pipelines. */ + backend_drp_binding_create_ns, /* Create samplers, layouts, and bind groups. */ + backend_drp_upload_ns, /* Execute buffer and texture uploads. */ + backend_drp_upload_decode_ns, /* Decode upload payloads. */ + backend_drp_upload_vulkan_allocate_ns, /* Allocate Vulkan staging resources. */ + backend_drp_upload_host_copy_ns, /* Copy host data into mapped staging buffers. */ + backend_drp_upload_command_allocate_ns,/* Allocate temporary upload commands. */ + backend_drp_upload_command_record_ns, /* Record upload copy commands. */ + backend_drp_upload_fence_create_ns, /* Create upload fences. */ + backend_drp_upload_submit_enqueue_ns, /* Enter the shared Vulkan queue channel. */ + backend_drp_upload_submit_queue_wait_ns, /* Wait cooperatively for queue submission ownership. */ + backend_drp_upload_queue_submit_ns, /* Submit the upload to Vulkan. */ + backend_drp_upload_fence_wait_ns, /* Yield cooperatively until the upload fence completes. */ + backend_drp_upload_retire_ns, /* Retire completed temporary upload resources. */ + backend_drp_transfer_ns, /* Execute explicit copy commands. */ + backend_drp_record_ns, /* Record render and compute passes. */ + backend_submit_queue_ns, /* Wait for the shared Vulkan submit channel. */ + backend_submit_ns, /* Submit the frame to Vulkan. */ + gpu_completion_observation_ns, /* Observe fence completion after watch registration. */ + completion_task_queue_ns, /* Queue the Scene completion task. */ + gpu_render_ns, /* GPU render phase duration. */ + gpu_transition_ns, /* GPU layout-transition duration. */ + gpu_copy_ns, /* GPU readback-copy duration. */ + gpu_total_ns, /* Total measured GPU duration. */ + readback_ns, /* Download completed pixels to CPU memory. */ + count /* Number of measurements; not a measurement. */ +}; + +struct Frame_3D; +namespace detail { +struct Frame_3D_Access { + /* All frame pointers are required non-owning borrows; callers keep every frame alive for the complete call. */ + static void assign_pixels(std::span frames, Extent extent, std::vector pixels, Frame_Identity rendered_identity); + static void share_pixels(Frame_3D* source, Frame_3D* target); + static void assign_datoviz_observation(Frame_3D* frame, Datoviz_Frame_Observation observation); +}; +} // namespace detail + +struct Frame_3D final { + static constexpr Pixel_Format native_pixel_format{Pixel_Format::rgba8_unorm}; + static constexpr std::array supported_pixel_formats{native_pixel_format}; + explicit Frame_3D(Frame_Identity identity, Frame_3D_Output output = Frame_3D_Output::pixels, Pixel_Format output_format = native_pixel_format); + ~Frame_3D(); + Frame_3D(const Frame_3D&) = delete; + Frame_3D& operator=(const Frame_3D&) = delete; + Frame_3D(Frame_3D&&) = delete; + Frame_3D& operator=(Frame_3D&&) = delete; + void begin(Frame_Identity identity, Frame_3D_Output output = Frame_3D_Output::pixels, Pixel_Format output_format = native_pixel_format, Frame_Request_Source source = Frame_Request_Source::unspecified); + [[nodiscard]] Frame_Identity identity() const noexcept; + [[nodiscard]] Frame_Request_Source source() const noexcept; + [[nodiscard]] Extent extent() const noexcept; + [[nodiscard]] std::span pixels() const noexcept; + [[nodiscard]] std::shared_ptr> share_pixels() const noexcept; + [[nodiscard]] Frame_Identity rendered_identity() const noexcept; + [[nodiscard]] Frame_3D_Output output() const noexcept; + [[nodiscard]] Pixel_Format output_format() const noexcept; + void mark(Frame_Trace_Marker marker) noexcept; + void record(Frame_Trace_Measurement measurement, std::uint64_t value_ns) noexcept; + void request_taskflow_trace() noexcept; + [[nodiscard]] bool taskflow_trace_requested() const noexcept; + [[nodiscard]] std::optional take_datoviz_observation(); +private: + struct Private; + std::unique_ptr d; /* 本帧身份、像素和诊断数据的唯一所有权。 */ + friend struct detail::Frame_3D_Access; +}; +} // namespace aethera::render_3d diff --git a/render_3D/render_3D/base/Types.hpp b/render_3D/render_3D/base/Types.hpp new file mode 100644 index 0000000..8a48298 --- /dev/null +++ b/render_3D/render_3D/base/Types.hpp @@ -0,0 +1,82 @@ +#pragma once +#include +#include +#include +namespace aethera::render_3d { +using Coordinate_3D = float; +using Pixel_Distance = float; + +/* 三维模块使用的非预乘 RGBA8 颜色。 */ +struct Color { + std::uint8_t red{}; /* 红色通道。 */ + std::uint8_t green{}; /* 绿色通道。 */ + std::uint8_t blue{}; /* 蓝色通道。 */ + std::uint8_t alpha{255}; /* 不透明度通道。 */ + [[nodiscard]] static constexpr Color transparent() noexcept { + return {}; + } + [[nodiscard]] static constexpr Color black() noexcept { + return {0, 0, 0, 255}; + } + [[nodiscard]] static constexpr Color white() noexcept { + return {255, 255, 255, 255}; + } + bool operator==(const Color&) const = default; +}; + +/* Datoviz 离屏目标与读回缓冲之间的实际字节布局。 */ +enum struct Pixel_Format : std::uint8_t { + rgba8_unorm /* Four unsigned normalized channels in RGBA byte order. */ +}; + +struct Vec2 { + Coordinate_3D x{}; /* 水平分量。 */ + Coordinate_3D y{}; /* 垂直分量。 */ + bool operator==(const Vec2&) const = default; +}; + +struct Vec3 { + Coordinate_3D x{}; /* X 轴分量。 */ + Coordinate_3D y{}; /* Y 轴分量。 */ + Coordinate_3D z{}; /* Z 轴分量。 */ + bool operator==(const Vec3&) const = default; +}; + +struct Vec4 { + Coordinate_3D x{}; /* 第一分量。 */ + Coordinate_3D y{}; /* 第二分量。 */ + Coordinate_3D z{}; /* 第三分量。 */ + Coordinate_3D w{}; /* 第四分量。 */ + bool operator==(const Vec4&) const = default; +}; + +struct Matrix4 { + std::array values{1.0F, 0.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 0.0F, 0.0F, 1.0F}; /* 按行存储的四阶变换矩阵。 */ + bool operator==(const Matrix4&) const = default; +}; + +struct Extent { + std::uint32_t width{}; /* 帧宽度,单位为像素。 */ + std::uint32_t height{}; /* 帧高度,单位为像素。 */ + [[nodiscard]] bool empty() const noexcept; + bool operator==(const Extent&) const = default; +}; + +struct Linear_Color { + Coordinate_3D red{0.035F}; /* 线性红色通道,范围为 0 到 1。 */ + Coordinate_3D green{0.045F}; /* 线性绿色通道,范围为 0 到 1。 */ + Coordinate_3D blue{0.07F}; /* 线性蓝色通道,范围为 0 到 1。 */ + Coordinate_3D alpha{1.0F}; /* 线性不透明度通道,范围为 0 到 1。 */ + bool operator==(const Linear_Color&) const = default; +}; + +namespace detail { +[[nodiscard]] bool finite(Coordinate_3D value) noexcept; +[[nodiscard]] bool finite(Vec2 value) noexcept; +[[nodiscard]] bool finite(Vec3 value) noexcept; +[[nodiscard]] bool finite(Vec4 value) noexcept; +[[nodiscard]] bool finite(const Matrix4& value) noexcept; +[[nodiscard]] bool valid(Linear_Color value) noexcept; +} +} +#include "Types.ipp" diff --git a/render_3D/render_3D/base/Types.ipp b/render_3D/render_3D/base/Types.ipp new file mode 100644 index 0000000..2d6ae1b --- /dev/null +++ b/render_3D/render_3D/base/Types.ipp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +namespace aethera::render_3d { +inline bool Extent::empty() const noexcept { + return width == 0 || height == 0; +} + +namespace detail { +inline bool finite(Coordinate_3D value) noexcept { + return std::isfinite(value); +} + +inline bool finite(Vec2 value) noexcept { + return finite(value.x) && finite(value.y); +} + +inline bool finite(Vec3 value) noexcept { + return finite(value.x) && finite(value.y) && finite(value.z); +} + +inline bool finite(Vec4 value) noexcept { + return finite(value.x) && finite(value.y) && finite(value.z) && finite(value.w); +} + +inline bool finite(const Matrix4& value) noexcept { + return std::ranges::all_of(value.values, [](Coordinate_3D component) { + return finite(component); + }); +} + +inline bool valid(Linear_Color value) noexcept { + return finite(value.red) && finite(value.green) && finite(value.blue) && finite(value.alpha) && value.red >= 0.0F && value.red <= 1.0F && value.green >= 0.0F && value.green <= 1.0F && value.blue >= 0.0F && value.blue <= 1.0F && value.alpha >= 0.0F && value.alpha <= 1.0F; +} +} +} diff --git a/render_3D/render_3D/camera/Camera.hpp b/render_3D/render_3D/camera/Camera.hpp new file mode 100644 index 0000000..8d95735 --- /dev/null +++ b/render_3D/render_3D/camera/Camera.hpp @@ -0,0 +1,90 @@ +#pragma once +#include +namespace aethera::render_3d { +struct Spatial_Point { + double x{}; + double y{}; + double z{}; + bool operator==(const Spatial_Point&) const = default; +}; + +enum struct Camera_Projection : std::uint8_t { + perspective, /* Perspective projection with depth foreshortening. */ + orthographic /* Orthographic projection without depth foreshortening. */ +}; + +enum struct Camera_Controller : std::uint8_t { + turntable, /* Orbit around a target using yaw and pitch. */ + arcball, /* Rotate the view using an arcball controller. */ + fly, /* Move the camera freely through the scene. */ + panzoom /* Restrict interaction to panning and zooming. */ +}; + +enum struct Camera_Fly_Mode : std::uint8_t { + free, /* Allow movement in all three dimensions. */ + plane /* Constrain movement to the controller plane. */ +}; + +struct Camera_View { + Spatial_Point eye{2.8, -3.2, 2.4}; + Spatial_Point target{}; + Spatial_Point up{0.0, 0.0, 1.0}; + bool operator==(const Camera_View&) const = default; +}; + +struct Camera_Turntable_Control { + double yaw_speed{0.20}; + double pitch_speed{0.20}; + double zoom_speed{0.10}; + double pan_speed{0.002}; + double minimum_pitch{-1.45}; + double maximum_pitch{1.45}; + double minimum_distance{0.25}; + double maximum_distance{50.0}; + bool rotate_enabled{true}; + bool zoom_enabled{true}; + bool pan_enabled{true}; + bool invert_y{}; + bool operator==(const Camera_Turntable_Control&) const = default; +}; + +struct Camera_Arcball_Control { + Spatial_Point constraint_axis{0.0, 0.0, 1.0}; + bool constrain_rotation{}; + bool operator==(const Camera_Arcball_Control&) const = default; +}; + +struct Camera_Fly_Control { + Camera_Fly_Mode mode{Camera_Fly_Mode::free}; + double movement_speed{1.0}; + double fast_multiplier{4.0}; + double slow_multiplier{0.25}; + double look_speed{0.0025}; + double wheel_speed{0.5}; + bool invert_y{}; + bool fixed_up{true}; + bool disable_roll{true}; + bool operator==(const Camera_Fly_Control&) const = default; +}; + +struct Camera_Panzoom_Control { + bool fixed_x{}; + bool fixed_y{}; + bool keep_aspect{true}; + bool operator==(const Camera_Panzoom_Control&) const = default; +}; + +struct Camera_Descriptor { + Camera_View initial_view{}; + Camera_Projection projection{Camera_Projection::perspective}; + Camera_Controller controller{Camera_Controller::turntable}; + Camera_Turntable_Control turntable_control{}; + Camera_Arcball_Control arcball_control{}; + Camera_Fly_Control fly_control{}; + Camera_Panzoom_Control panzoom_control{}; + double vertical_field_of_view_degrees{45.0}; + double near_plane{0.01}; + double far_plane{100.0}; + bool operator==(const Camera_Descriptor&) const = default; +}; +} diff --git a/render_3D/render_3D/camera/Camera_3D.hpp b/render_3D/render_3D/camera/Camera_3D.hpp new file mode 100644 index 0000000..16070a8 --- /dev/null +++ b/render_3D/render_3D/camera/Camera_3D.hpp @@ -0,0 +1,33 @@ +#pragma once +#include "Camera.hpp" +#include "model/Model.hpp" +#include "model/attachment/structure/Struct_Concurrent.hpp" +namespace aethera::render_3d { +struct Camera_3D : Def> { + struct Prop : Prev { + Camera_View initial_view{}; + Camera_Projection projection{Camera_Projection::perspective}; + Camera_Controller controller{Camera_Controller::turntable}; + Camera_Turntable_Control turntable_control{}; + Camera_Arcball_Control arcball_control{}; + Camera_Fly_Control fly_control{}; + Camera_Panzoom_Control panzoom_control{}; + double vertical_field_of_view_degrees{45.0}; + double near_plane{0.01}; + double far_plane{100.0}; + bool operator==(const Prop&) const = default; + }; + + struct Private; +}; + +struct Camera_3D::Private : Prev_Private { + void advance() { + concurrent().internal.advance(); + } + [[nodiscard]] Camera_Descriptor use() const { + const auto& prop = static_cast(*concurrent().internal.use()); + return {prop.initial_view, prop.projection, prop.controller, prop.turntable_control, prop.arcball_control, prop.fly_control, prop.panzoom_control, prop.vertical_field_of_view_degrees, prop.near_plane, prop.far_plane}; + } +}; +} // namespace aethera::render_3d diff --git a/render_3D/render_3D/detail/Backend_Types.cpp b/render_3D/render_3D/detail/Backend_Types.cpp new file mode 100644 index 0000000..e9a54af --- /dev/null +++ b/render_3D/render_3D/detail/Backend_Types.cpp @@ -0,0 +1,11 @@ +#include "Backend_Types.hpp" +#include +namespace aethera::render_3d::detail { +bool prepared_visual_batch_complete(const Prepared_Visual_Batch& visuals) noexcept { + return !visuals.empty() && std::ranges::all_of(visuals, [](const Prepared_Visual_Instance& value) { + return value.visual.has_payload(); + }); +} + +bool Scene_3D_Parameters::operator==(const Scene_3D_Parameters&) const = default; +} diff --git a/render_3D/render_3D/detail/Backend_Types.hpp b/render_3D/render_3D/detail/Backend_Types.hpp new file mode 100644 index 0000000..0b94c2a --- /dev/null +++ b/render_3D/render_3D/detail/Backend_Types.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "../visual/Visuals.hpp" +#include "../visual/detail/Datoviz_Visual_Operations.hpp" +#include "../camera/Camera.hpp" +#include "../axis/Axis.hpp" +#include +#include +#include +namespace aethera::render_3d::detail { +using Visual_Identity = std::uintptr_t; + +struct Visual_Registration { + Visual_Identity identity{}; /* Scene 内稳定对象身份,仅供后端匹配原生 Visual。 */ + const Datoviz_Visual_Operations* operations{}; /* 必填、非拥有;指向具体 Visual 的静态 Datoviz 操作。 */ +}; + +struct Prepared_Visual_Instance { + Visual_Identity identity{}; /* 与 Builder 注册身份一一对应。 */ + Prepared_Visual visual{}; /* Prepare 交给本帧后端阶段的不可变提交数据。 */ +}; + +using Prepared_Visual_Batch = std::vector; +using Shared_Prepared_Visual_Batch = std::shared_ptr; +[[nodiscard]] bool prepared_visual_batch_complete(const Prepared_Visual_Batch& visuals) noexcept; + +struct Scene_3D_Parameters { + Extent viewport{560, 320}; /* 离屏渲染目标尺寸,单位为像素。 */ + Linear_Color clear_color{}; /* 每帧开始时写入的线性背景色。 */ + Camera_Descriptor camera{}; /* Camera 组件为本帧后端阶段提供的配置。 */ + plot::Axis_Descriptor x_axis{}; /* 三维数据 X 轴的业务范围与标签策略。 */ + plot::Axis_Descriptor y_axis{}; /* 三维数据 Y 轴的业务范围与标签策略。 */ + plot::Axis_Descriptor z_axis{}; /* 三维数据 Z 轴的业务范围与标签策略。 */ + bool operator==(const Scene_3D_Parameters&) const; +}; +} diff --git a/render_3D/render_3D/detail/Exception.cpp b/render_3D/render_3D/detail/Exception.cpp new file mode 100644 index 0000000..a32bf19 --- /dev/null +++ b/render_3D/render_3D/detail/Exception.cpp @@ -0,0 +1,24 @@ +#include "Exception.hpp" +#include +#include +namespace aethera::render_3d::detail { +std::exception_ptr contextual_exception(std::string_view context, std::exception_ptr cause) noexcept { + try { + try { + if (cause) std::rethrow_exception(cause); + } + catch (...) { + std::throw_with_nested(std::runtime_error(std::string(context))); + } + throw std::runtime_error(std::string(context)); + } + catch (...) { + return std::current_exception(); + } +} + +void raise_context(std::string_view context, std::exception_ptr cause) { + std::rethrow_exception(contextual_exception(context, std::move(cause))); +} +} + diff --git a/render_3D/render_3D/detail/Exception.hpp b/render_3D/render_3D/detail/Exception.hpp new file mode 100644 index 0000000..d009bf0 --- /dev/null +++ b/render_3D/render_3D/detail/Exception.hpp @@ -0,0 +1,8 @@ +#pragma once +#include +#include +namespace aethera::render_3d::detail { +[[nodiscard]] std::exception_ptr contextual_exception(std::string_view context, std::exception_ptr cause) noexcept; +[[noreturn]] void raise_context(std::string_view context, std::exception_ptr cause); +} + diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp new file mode 100644 index 0000000..6c76066 --- /dev/null +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp @@ -0,0 +1,154 @@ +#include "Gpu_Completion_Service.ipp" +#include "Exception.hpp" +#include +#include +#include +#include +namespace aethera::render_3d::detail { +namespace { +std::uint64_t elapsed_nanoseconds(std::chrono::steady_clock::time_point started) noexcept { + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count(); + return elapsed > 0 ? static_cast(elapsed) : 0ULL; +} +} // namespace + +Gpu_Completion_Service::Private::Private() { + active.reserve(capacity); +} + +Gpu_Completion_Service::Gpu_Completion_Service() : d(std::make_unique()) {} +Gpu_Completion_Service::~Gpu_Completion_Service() = default; + +Gpu_Completion_Service& Gpu_Completion_Service::instance() { + /* 进程级服务主动保留,避免静态析构期为等待计时回调而引入同步取消。 */ + static Gpu_Completion_Service* service = new Gpu_Completion_Service(); + return *service; +} + +Gpu_Completion_Service::Reservation::Reservation(std::shared_ptr pending) noexcept : pending_(std::move(pending)) {} + +Gpu_Completion_Service::Reservation::~Reservation() noexcept { + cancel(); +} + +Gpu_Completion_Service::Reservation::Reservation(Reservation&& other) noexcept : pending_(std::exchange(other.pending_, {})) {} + +Gpu_Completion_Service::Prepare_Result::operator bool() const noexcept { + return result == Admission_Result::none; +} + +void Gpu_Completion_Service::Reservation::watch(std::uintptr_t device, std::uintptr_t fence) { + if (!pending_ || device == 0 || fence == 0) throw std::logic_error("GPU completion reservation or fence is invalid"); + pending_->service->watch(pending_, device, fence); + pending_.reset(); +} + +void Gpu_Completion_Service::Reservation::cancel() noexcept { + if (!pending_) return; + pending_->service->cancel(pending_); + pending_.reset(); +} + +Gpu_Completion_Service::Prepare_Result Gpu_Completion_Service::prepare(Completion completion, Exception_Handler on_exception, bool observe) { + if (!completion) throw std::invalid_argument("GPU completion callback is empty"); + if (!on_exception) throw std::invalid_argument("GPU completion exception handler is empty"); + auto count = d->in_flight.load(std::memory_order_relaxed); + while (count < Private::capacity && !d->in_flight.compare_exchange_weak(count, count + 1, std::memory_order_acq_rel, std::memory_order_relaxed)) {} + if (count >= Private::capacity) return {{}, Admission_Result::capacity_exhausted}; + auto pending = std::make_shared(); + pending->completion = std::move(completion); + pending->on_exception = std::move(on_exception); + pending->observe = observe; + pending->service = this; + if (!d->incoming.enqueue(pending)) { + d->in_flight.fetch_sub(1, std::memory_order_release); + throw std::bad_alloc{}; + } + d->request_poll(std::chrono::nanoseconds{1}); + return {Reservation(std::move(pending)), Admission_Result::none}; +} + +void Gpu_Completion_Service::watch(const std::shared_ptr& pending, std::uintptr_t device, std::uintptr_t fence) { + if (!pending || pending->service != this) throw std::logic_error("GPU completion reservation does not belong to this service"); + pending->device = device; + pending->fence = fence; + pending->watched_at = std::chrono::steady_clock::now(); + auto expected = Gpu_Completion_Pending_Fence::Status::reserved; + if (!pending->status.compare_exchange_strong(expected, Gpu_Completion_Pending_Fence::Status::watched, std::memory_order_release, std::memory_order_acquire)) throw std::logic_error("GPU completion reservation is no longer reserved"); + d->request_poll(std::chrono::nanoseconds{1}); +} + +void Gpu_Completion_Service::cancel(const std::shared_ptr& pending) noexcept { + if (!pending || pending->service != this) return; + auto expected = Gpu_Completion_Pending_Fence::Status::reserved; + pending->status.compare_exchange_strong(expected, Gpu_Completion_Pending_Fence::Status::canceled, std::memory_order_release, std::memory_order_relaxed); + d->request_poll(std::chrono::nanoseconds{1}); +} + +void Gpu_Completion_Service::Private::request_poll(std::chrono::nanoseconds delay) noexcept { + if (poll_armed.exchange(true, std::memory_order_acq_rel)) return; + try { + timer->schedule_after(delay, [this] { + poll_armed.store(false, std::memory_order_release); + poll(); + }); + } + catch (...) { + poll_armed.store(false, std::memory_order_release); + } +} + +void Gpu_Completion_Service::Private::poll() noexcept { + std::shared_ptr incoming_value; + while (incoming.try_dequeue(incoming_value)) { + active.push_back(std::move(incoming_value)); + incoming_value.reset(); + } + bool needs_more{}; + for (auto iterator = active.begin(); iterator != active.end();) { + auto& pending = *iterator; + const auto status = pending->status.load(std::memory_order_acquire); + if (status == Gpu_Completion_Pending_Fence::Status::reserved) { + needs_more = true; + ++iterator; + continue; + } + if (status == Gpu_Completion_Pending_Fence::Status::canceled) { + iterator = active.erase(iterator); + in_flight.fetch_sub(1, std::memory_order_release); + continue; + } + VkResult vulkan_result = VK_NOT_READY; + Completion_Error error = Completion_Error::none; + if (elapsed_nanoseconds(pending->watched_at) >= maximum_fence_age_ns) { + vulkan_result = VK_TIMEOUT; + error = Completion_Error::fence_abandoned; + } + else { + vulkan_result = vkGetFenceStatus(reinterpret_cast(pending->device), reinterpret_cast(pending->fence)); + if (vulkan_result == VK_NOT_READY) { + needs_more = true; + ++iterator; + continue; + } + if (vulkan_result != VK_SUCCESS) error = Completion_Error::vulkan_failure; + } + pending->status.store(Gpu_Completion_Pending_Fence::Status::canceled, std::memory_order_release); + auto completion = std::move(pending->completion); + auto on_exception = std::move(pending->on_exception); + Result result{error, static_cast(vulkan_result), pending->observe ? elapsed_nanoseconds(pending->watched_at) : 0}; + iterator = active.erase(iterator); + in_flight.fetch_sub(1, std::memory_order_release); + try { + completion(std::move(result)); + } + catch (...) { + try { + on_exception(contextual_exception("delivering GPU completion", std::current_exception())); + } + catch (...) {} + } + } + if (needs_more) request_poll(probe_interval); +} +} // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.hpp b/render_3D/render_3D/detail/Gpu_Completion_Service.hpp new file mode 100644 index 0000000..d5e2cbf --- /dev/null +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.hpp @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include +#include +#include +namespace aethera::render_3d::detail { +struct Gpu_Completion_Pending_Fence; + +/* 进程级 GPU fence 完成域:提交方只入队,由 Timer Service 异步轮询,不在调用线程等待。 */ +struct Gpu_Completion_Service final { + enum struct Admission_Result : std::uint8_t { + none, /* 已为本 fence 保留容量。 */ + capacity_exhausted /* 当前在途 fence 已达上限。 */ + }; + + enum struct Completion_Error : std::uint8_t { + none, /* Vulkan fence 正常完成。 */ + fence_abandoned, /* fence 超过最大在途时间。 */ + vulkan_failure /* vkGetFenceStatus 返回失败。 */ + }; + + struct Result { + Completion_Error error{}; /* 归一化完成结果。 */ + std::int32_t vulkan_result{}; /* Vulkan VkResult 数值。 */ + std::uint64_t wait_duration_ns{}; /* 从 watch 到观察完成的时间。 */ + }; + + using Completion = std::function; + using Exception_Handler = std::function; + + struct Reservation final { + Reservation() = default; + ~Reservation() noexcept; + Reservation(const Reservation&) = delete; + Reservation& operator=(const Reservation&) = delete; + Reservation(Reservation&& other) noexcept; + Reservation& operator=(Reservation&&) = delete; + void watch(std::uintptr_t device, std::uintptr_t fence); + private: + explicit Reservation(std::shared_ptr pending) noexcept; + void cancel() noexcept; + std::shared_ptr pending_{}; /* 尚未 watch 或 cancel 的准入记录唯一所有权。 */ + friend struct Gpu_Completion_Service; + }; + + struct Prepare_Result { + Reservation reservation{}; /* 成功时返回的 fence reservation。 */ + Admission_Result result{}; /* 准入结果。 */ + [[nodiscard]] explicit operator bool() const noexcept; + }; + + [[nodiscard]] static Gpu_Completion_Service& instance(); + [[nodiscard]] Prepare_Result prepare(Completion completion, Exception_Handler on_exception, bool observe); +private: + struct Private; + Gpu_Completion_Service(); + ~Gpu_Completion_Service(); + Gpu_Completion_Service(const Gpu_Completion_Service&) = delete; + Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete; + void watch(const std::shared_ptr& pending, std::uintptr_t device, std::uintptr_t fence); + void cancel(const std::shared_ptr& pending) noexcept; + std::unique_ptr d; /* 进程级无锁命令队列与 Timer Service 代理的唯一所有权。 */ +}; +} // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.ipp b/render_3D/render_3D/detail/Gpu_Completion_Service.ipp new file mode 100644 index 0000000..4e03e23 --- /dev/null +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.ipp @@ -0,0 +1,41 @@ +#pragma once +#include "Gpu_Completion_Service.hpp" +#include +#include +#include +#include +#include +namespace aethera::render_3d::detail { +struct Gpu_Completion_Pending_Fence final { + enum struct Status : std::uint8_t { + reserved, /* 已准入,尚未绑定 Vulkan fence。 */ + watched, /* device/fence 已发布,可由计时线程查询。 */ + canceled /* 未提交或已完成,等待计时线程回收记录。 */ + }; + + std::uintptr_t device{}; /* 必填、非拥有;watched 期间 Datoviz target 保证存活。 */ + std::uintptr_t fence{}; /* 必填、非拥有;watched 期间 Datoviz target 保证存活。 */ + Gpu_Completion_Service::Completion completion{}; /* 完成或隔离后的交付回调。 */ + Gpu_Completion_Service::Exception_Handler on_exception{}; /* 回调异常的跨线程交付边界。 */ + std::chrono::steady_clock::time_point watched_at{}; /* 开始监视的单调时钟时刻。 */ + Gpu_Completion_Service* service{}; /* 必填、非拥有;进程级 Service 永久存活。 */ + std::atomic status{Status::reserved}; + bool observe{}; +}; + +struct Gpu_Completion_Service::Private final { + static constexpr std::size_t capacity = 1024; + static constexpr auto probe_interval = std::chrono::milliseconds(1); + static constexpr std::uint64_t maximum_fence_age_ns = 30'000'000'000ULL; + + Private(); + void request_poll(std::chrono::nanoseconds delay) noexcept; + void poll() noexcept; + + proxy timer{time_thread::make_timer_service()}; /* 独占代理;后台计时状态按 proxy 共享。 */ + moodycamel::ConcurrentQueue> incoming{}; + std::vector> active{}; /* 仅 Timer Service 回调线程访问。 */ + std::atomic_size_t in_flight{}; + std::atomic_bool poll_armed{}; +}; +} // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/main.cpp b/render_3D/render_3D/main.cpp new file mode 100644 index 0000000..5768194 --- /dev/null +++ b/render_3D/render_3D/main.cpp @@ -0,0 +1 @@ +#include "Render_3D.hpp" diff --git a/render_3D/render_3D/scene/Render_Scene_3D.cpp b/render_3D/render_3D/scene/Render_Scene_3D.cpp new file mode 100644 index 0000000..c5d1313 --- /dev/null +++ b/render_3D/render_3D/scene/Render_Scene_3D.cpp @@ -0,0 +1,14 @@ +#include "Render_Scene_3D.hpp" +#include +namespace aethera::render_3d { +proxy scene::make_task_graph(std::string name) { + return task_flow::make_task_graph(std::move(name)); +} + +bool Render_Scene_3D::Prop::operator==(const Prop&) const = default; +Render_Scene_3D::~Render_Scene_3D() noexcept = default; + +Render_Scene_3D::Render_Result Render_Scene_3D::render(Frame_3D* frame, Frame_Callbacks callbacks) { + return aethera::detail::model_private(*this).render(frame, std::move(callbacks)); +} +} // namespace aethera::render_3d diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp new file mode 100644 index 0000000..50266d8 --- /dev/null +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -0,0 +1,47 @@ +#pragma once +#include "../axis/Axes_3D.hpp" +#include "../base/Frame_3D.hpp" +#include "../camera/Camera_3D.hpp" +#include "rely_facade.h" +#include +#include +#include +#include +#include +#include +#include +#include +namespace aethera::render_3d { +/* 按 Renderable DAG 准备一整帧,并把 Datoviz/Vulkan 提交与回读保持为异步三槽流水线。 */ +struct Render_Scene_3D : Def, Owned_Dag_Relation> { + struct Prop : Prev { + Extent viewport{560, 320}; /* 离屏渲染目标尺寸,单位为像素。 */ + Linear_Color clear_color{}; /* 每帧开始时写入的线性背景色。 */ + bool view_active{true}; /* 是否接受 render() 产生新的完成帧。 */ + bool operator==(const Prop&) const; + }; + + struct Private; + + enum struct Render_Result : std::uint8_t { + submitted, /* CPU 帧图已异步提交。 */ + frame_pipeline_busy, /* 三槽已满或当前 CPU Prepare 尚未释放。 */ + frame_missing, /* 调用方未提供帧借用。 */ + completion_missing, /* 调用方未提供帧完成回调。 */ + view_inactive, /* 当前视图不产生新帧。 */ + empty_viewport, /* 离屏目标尺寸为空。 */ + backend_unavailable /* DAG、任务运行时或 Datoviz 后端不可用。 */ + }; + + struct Frame_Callbacks { + std::function submitted{}; /* 非拥有借用;通知提交以及是否可与 GPU 重叠。 */ + std::function completed{}; /* Scene 清除全部借用后交付同一帧与失败。 */ + }; + + Render_Scene_3D() = default; + ~Render_Scene_3D() noexcept; + /* frame 为可空非拥有借用;submitted/completed 返回前调用方必须保持其存活。 */ + [[nodiscard]] Render_Result render(Frame_3D* frame, Frame_Callbacks callbacks); +}; +} // namespace aethera::render_3d +#include "Render_Scene_3D.ipp" diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp new file mode 100644 index 0000000..922b024 --- /dev/null +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -0,0 +1,137 @@ +#pragma once +#include "../base/Datoviz_Frame_Observation.hpp" +#include "../detail/Backend_Types.hpp" +#include "../visual/detail/Datoviz_Visual_State.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +struct DvzController; +struct DvzDrp2CommandStream; +struct DvzDrp2Runtime; +struct DvzFigure; +struct DvzFramePlanEmitter; +struct DvzInputRouter; +struct DvzItemInteraction; +struct DvzPanel; +struct DvzPinnedReadout; +struct DvzPointerGestureHandler; +struct DvzScene; +struct DvzSceneFrameArtifact; +struct DvzSceneFrameEmitTiming; +namespace aethera { +namespace render_3d { +namespace detail { +struct Datoviz_Render_Context; +struct Datoviz_Frame_Target; +struct Datoviz_Frame_Target_Set; + +/* Scene 帧槽从录制到 GPU 完成期间携带的唯一状态。 */ +struct Datoviz_Pending_Frame { + std::uintptr_t device{}; /* Datoviz Device 的非拥有句柄值。 */ + std::uintptr_t fence{}; /* 本帧完成 Fence 的非拥有句柄值。 */ + Extent extent{}; /* 本帧离屏目标尺寸。 */ + std::uint8_t target_index{}; /* Scene 三槽中的目标下标。 */ + std::uint64_t target_generation{}; /* 防止槽重建后的陈旧引用命中。 */ + Datoviz_Frame_Observation observation{}; /* 本帧后端执行事实。 */ +}; + +/* 同一个 Scene 帧完成后的交付状态,不拆成独立模块。 */ +struct Datoviz_Completed_Frame { + Extent extent{}; + std::vector pixels{}; + Datoviz_Frame_Observation observation{}; +}; + +/* + * 3D 后端核心资源所有权: + * 1. render_context_ 独占 GPU 上下文入口,并串行投递 Vulkan 工作。 + * 2. 本对象拥有命令池、描述符池、三个 DRP2 Runtime、Datoviz Scene 和外部属性 Buffer。 + * 3. Datoviz Scene 继续拥有 Figure、Panel、Visual、字体和采样场;对应裸指针只在 Scene 生命周期内借用。 + * 4. targets_ 固定拥有三个离屏目标槽;每槽独占颜色图像、命令缓冲、Fence、Submit 和读回 Buffer。 + * 5. prepare() 只取得空闲槽并录制,submit() 异步提交,collect() 在 GPU 完成后读回并归还该槽。 + */ +struct Scene_Datoviz_State { + Scene_Datoviz_State(); + ~Scene_Datoviz_State(); + Scene_Datoviz_State(const Scene_Datoviz_State&) = delete; + Scene_Datoviz_State& operator=(const Scene_Datoviz_State&) = delete; + /* 一次性建立 GPU、Vulkan 池、三槽 Runtime 和 Datoviz Scene 资源。 */ + void initialize(std::uint32_t gpu_index, bool validation_enabled, const std::vector& visuals, const Scene_3D_Parameters& initial_scene); + void create_scene(const std::vector& visuals, const Scene_3D_Parameters& initial_scene); + void apply_camera(const Camera_Descriptor& camera); + void apply_axes(const Scene_3D_Parameters& scene); + [[nodiscard]] bool matches_command_structure(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals) const; + /* 把本帧 Scene 参数和各 Visual 发布版本应用到 Datoviz 权威对象。 */ + [[nodiscard]] std::uint64_t apply(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint8_t target_index, bool bind_target); + [[nodiscard]] std::uint64_t apply_visual(Datoviz_Visual_Instance& target, const Prepared_Visual& visual, std::uint8_t target_index, bool bind_target); + void ensure_external_attributes(Datoviz_Visual_Instance& target, const Prepared_Visual& visual); + [[nodiscard]] std::uint64_t upload_external_attributes(Datoviz_Visual_Instance& target, const Prepared_Visual& visual, std::uint8_t target_index); + void bind_external_attributes(Datoviz_Visual_Instance& target, std::uint8_t target_index, std::uint32_t item_count); + void register_external_attributes(const DvzDrp2CommandStream* stream, std::uint8_t target_index); + [[nodiscard]] bool target_runtime_resources_current(const Prepared_Visual_Batch& visuals, std::uint8_t target_index) const; + [[nodiscard]] std::vector stale_runtime_visuals(const Prepared_Visual_Batch& visuals, std::uint8_t target_index) const; + void mark_target_runtime_resources_uploaded(std::uint8_t target_index); + /* 帧流水线:尝试复用已录制命令,失败时取得空槽并重新录制。 */ + [[nodiscard]] std::optional reuse_recorded_target(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback); + [[nodiscard]] std::optional prepare(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback); + [[nodiscard]] DvzSceneFrameArtifact* emit(const Scene_3D_Parameters& scene, std::uint8_t target_index, DvzSceneFrameEmitTiming& timing); + [[nodiscard]] std::optional acquire_target(Extent extent); + [[nodiscard]] Datoviz_Frame_Target& target(const Datoviz_Pending_Frame& pending); + /* 提交不等待 GPU;完成通知由 Gpu_Completion_Service 驱动 collect()。 */ + void submit(Datoviz_Pending_Frame& pending, std::function completion); + [[nodiscard]] Datoviz_Completed_Frame collect(Datoviz_Pending_Frame pending); + void discard(Datoviz_Pending_Frame pending); + void quarantine(Datoviz_Pending_Frame pending) noexcept; + /* 输入只修改 Datoviz Controller;revision 使下一帧重新录制必要命令。 */ + void mark_controller_input_applied() noexcept; + void dispatch_pointer(Event_Type type, float x, float y, Mouse_Button button, Keyboard_Modifier modifiers, Extent viewport, std::uint64_t occurred_at_ns); + void dispatch_wheel(float x, float y, float delta_x, float delta_y, Keyboard_Modifier modifiers, Extent viewport, std::uint64_t occurred_at_ns); + void dispatch_key(const Key_Event& event); + /* 正常路径按所有权逆序销毁;故障隔离路径只转移资源,不在线程上等待。 */ + void abandon_resources() noexcept; + void destroy(); + private: + struct Runtime_Slot { + DvzDrp2Runtime* runtime{}; /* 本目标槽独占的 DRP2 Runtime。 */ + DvzFramePlanEmitter* emitter{}; /* 本目标槽独占的帧计划 emitter。 */ + }; + + std::shared_ptr render_context_{}; /* 按 GPU 索引与验证模式共享的进程级上下文。 */ + std::uintptr_t command_pool_{}; /* 本 Scene 独占 Vulkan Command Pool。 */ + std::uintptr_t descriptor_pool_{}; /* 本 Scene 独占 Vulkan Descriptor Pool。 */ + std::array runtime_slots_{}; /* 三个固定目标槽的 Runtime。 */ + DvzScene* scene_{}; /* Datoviz Scene 所有权。 */ + DvzFigure* figure_{}; /* 可空、非拥有;由 scene_ 拥有。 */ + DvzPanel* panel_{}; /* 可空、非拥有;由 scene_ 拥有。 */ + DvzVisual* axes_visual_{}; /* 可空、非拥有;由 scene_ 拥有。 */ + DvzText* axes_text_{}; /* 可空、非拥有;由 scene_ 拥有。 */ + Extent figure_extent_{}; /* 当前原生 Figure 尺寸。 */ + std::uint64_t command_revision_{1}; /* 原生命令结构版本。 */ + std::optional applied_camera_{}; /* 已应用相机,仅用于结构复用判定。 */ + std::optional> applied_axes_{}; /* 已应用坐标轴。 */ + std::atomic_bool quarantined_{}; /* Unknown Failure 后是否禁止销毁在途资源。 */ + std::vector visuals_{}; /* 注册顺序稳定的具体 Visual 集合。 */ + std::vector external_buffers_{}; /* Visual 三槽属性 Buffer 唯一所有权。 */ + DvzItemInteraction* item_interaction_{}; /* item query 能力所有权。 */ + DvzPinnedReadout* hover_readout_{}; /* 当前 hover readout 所有权。 */ + DvzController* camera_controller_{}; /* 相机控制器所有权。 */ + DvzInputRouter* input_router_{}; /* 输入路由所有权。 */ + DvzPointerGestureHandler* gesture_handler_{}; /* 指针手势处理器所有权。 */ + std::uint64_t controller_revision_{1}; /* 已应用控制器输入版本。 */ + bool input_changed_{}; /* 当前录制周期是否消费了输入。 */ + mutable std::mutex target_mutex_{}; /* 仅保护三槽在提交与回读边界的生命周期。 */ + std::unique_ptr targets_{}; /* 三槽唯一所有权。 */ + std::uint64_t target_generation_{}; /* 目标槽重建代数。 */ +}; +} +} +} // namespace aethera::render_3d::detail +#include "Render_Scene_3D_Model.ipp" diff --git a/render_3D/render_3D/scene/Render_Scene_3D_Datoviz.cpp b/render_3D/render_3D/scene/Render_Scene_3D_Datoviz.cpp new file mode 100644 index 0000000..fcbf13a --- /dev/null +++ b/render_3D/render_3D/scene/Render_Scene_3D_Datoviz.cpp @@ -0,0 +1 @@ +#include "detail/Datoviz_Frame_Pipeline.ipp" diff --git a/render_3D/render_3D/scene/Render_Scene_3D_Model.ipp b/render_3D/render_3D/scene/Render_Scene_3D_Model.ipp new file mode 100644 index 0000000..5426961 --- /dev/null +++ b/render_3D/render_3D/scene/Render_Scene_3D_Model.ipp @@ -0,0 +1,529 @@ +#pragma once +#include "../detail/Exception.hpp" +#include "../detail/Gpu_Completion_Service.hpp" +#include "Error_handling_specification/Failure_Policy.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace aethera::render_3d { +namespace detail { +struct Scene_Paint_Context final { + Scene_3D_Parameters parameters{}; /* 当前 CPU Prepare 唯一读取的 Scene 参数。 */ + std::shared_ptr visuals{std::make_shared()}; /* 各 Visual 节点只写自己的预分配槽位。 */ +}; + +inline float wheel_step(double pixel, double angle) { + return angle != 0.0 ? static_cast(angle / 120.0) : static_cast(pixel / 100.0); +} + +inline Mouse_Button held_button(Mouse_Button_Mask buttons) { + if ((buttons & 1U) != 0) return Mouse_Button::left; + if ((buttons & 2U) != 0) return Mouse_Button::right; + if ((buttons & 4U) != 0) return Mouse_Button::middle; + return Mouse_Button::none; +} + +inline void publish_visual(void* raw_context, std::size_t slot, const Prepared_Visual& prepared) { + if (!raw_context) throw std::logic_error("3D Visual lost its Scene paint context"); + auto& context = *static_cast(raw_context); + if (!context.visuals || slot >= context.visuals->size()) throw std::logic_error("3D Visual published outside its Scene slot"); + (*context.visuals)[slot].visual = prepared; +} + +inline void publish_datoviz_observation(Frame_3D* frame, Datoviz_Frame_Observation observation) { + if (!frame) return; + if (observation.apply_ns) frame->record(Frame_Trace_Measurement::backend_apply_ns, observation.apply_ns); + const auto plan_ns = observation.runtime_plan_ns + observation.frame_plan_ns; + const auto execute_ns = observation.runtime_execute_ns + observation.frame_attach_ns + observation.frame_execute_ns; + if (plan_ns) frame->record(Frame_Trace_Measurement::backend_plan_ns, plan_ns); + if (execute_ns) frame->record(Frame_Trace_Measurement::backend_execute_ns, execute_ns); + const std::array detailed_timings{std::pair{Frame_Trace_Measurement::backend_target_acquire_ns, observation.target_acquire_ns}, std::pair{Frame_Trace_Measurement::backend_structure_check_ns, observation.structure_check_ns}, std::pair{Frame_Trace_Measurement::backend_query_ns, observation.query_ns}, std::pair{Frame_Trace_Measurement::backend_runtime_plan_ns, observation.runtime_plan_ns}, std::pair{Frame_Trace_Measurement::backend_runtime_plan_cpu_ns, observation.runtime_plan_cpu_ns}, std::pair{Frame_Trace_Measurement::backend_runtime_execute_ns, observation.runtime_execute_ns}, std::pair{Frame_Trace_Measurement::backend_runtime_execute_cpu_ns, observation.runtime_execute_cpu_ns}, std::pair{Frame_Trace_Measurement::backend_mvp_update_ns, observation.mvp_update_ns}, std::pair{Frame_Trace_Measurement::backend_frame_begin_ns, observation.frame_begin_ns}, std::pair{Frame_Trace_Measurement::backend_frame_plan_ns, observation.frame_plan_ns}, std::pair{Frame_Trace_Measurement::backend_frame_plan_cpu_ns, observation.frame_plan_cpu_ns}, std::pair{Frame_Trace_Measurement::backend_emit_replay_dirty_ns, observation.emit_replay_dirty_ns}, std::pair{Frame_Trace_Measurement::backend_emit_layout_ns, observation.emit_layout_ns}, std::pair{Frame_Trace_Measurement::backend_emit_prepare_ns, observation.emit_prepare_ns}, std::pair{Frame_Trace_Measurement::backend_emit_plan_build_ns, observation.emit_plan_build_ns}, std::pair{Frame_Trace_Measurement::backend_emit_contract_ns, observation.emit_contract_ns}, std::pair{Frame_Trace_Measurement::backend_emit_stream_ns, observation.emit_stream_ns}, std::pair{Frame_Trace_Measurement::backend_emit_stream_freeze_ns, observation.emit_stream_freeze_ns}, std::pair{Frame_Trace_Measurement::backend_emit_commit_ns, observation.emit_commit_ns}, std::pair{Frame_Trace_Measurement::backend_emit_plan_reset_ns, observation.emit_plan_reset_ns}, std::pair{Frame_Trace_Measurement::backend_emit_artifact_create_ns, observation.emit_artifact_create_ns}, std::pair{Frame_Trace_Measurement::backend_emit_artifact_freeze_ns, observation.emit_artifact_freeze_ns}, std::pair{Frame_Trace_Measurement::backend_emit_packet_encode_ns, observation.emit_packet_encode_ns}, std::pair{Frame_Trace_Measurement::backend_external_register_ns, observation.external_register_ns}, std::pair{Frame_Trace_Measurement::backend_frame_attach_ns, observation.frame_attach_ns}, std::pair{Frame_Trace_Measurement::backend_frame_execute_ns, observation.frame_execute_ns}, std::pair{Frame_Trace_Measurement::backend_frame_execute_cpu_ns, observation.frame_execute_cpu_ns}, std::pair{Frame_Trace_Measurement::backend_frame_finish_ns, observation.frame_finish_ns}, std::pair{Frame_Trace_Measurement::backend_drp_validation_ns, observation.drp_validation_ns}, std::pair{Frame_Trace_Measurement::backend_drp_state_ns, observation.drp_state_ns}, std::pair{Frame_Trace_Measurement::backend_drp_buffer_create_ns, observation.drp_buffer_create_ns}, std::pair{Frame_Trace_Measurement::backend_drp_texture_create_ns, observation.drp_texture_create_ns}, std::pair{Frame_Trace_Measurement::backend_drp_shader_create_ns, observation.drp_shader_create_ns}, std::pair{Frame_Trace_Measurement::backend_drp_shader_compile_ns, observation.drp_shader_compile_ns}, std::pair{Frame_Trace_Measurement::backend_drp_shader_module_create_ns, observation.drp_shader_module_create_ns}, std::pair{Frame_Trace_Measurement::backend_drp_pipeline_create_ns, observation.drp_pipeline_create_ns}, std::pair{Frame_Trace_Measurement::backend_drp_binding_create_ns, observation.drp_binding_create_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_ns, observation.drp_upload_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_decode_ns, observation.drp_upload_decode_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_vulkan_allocate_ns, observation.drp_upload_vulkan_allocate_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_host_copy_ns, observation.drp_upload_host_copy_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_command_allocate_ns, observation.drp_upload_command_allocate_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_command_record_ns, observation.drp_upload_command_record_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_fence_create_ns, observation.drp_upload_fence_create_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_submit_enqueue_ns, observation.drp_upload_submit_enqueue_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_submit_queue_wait_ns, observation.drp_upload_submit_queue_wait_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_queue_submit_ns, observation.drp_upload_queue_submit_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_fence_wait_ns, observation.drp_upload_fence_wait_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_retire_ns, observation.drp_upload_retire_ns}, std::pair{Frame_Trace_Measurement::backend_drp_transfer_ns, observation.drp_transfer_ns}, std::pair{Frame_Trace_Measurement::backend_drp_record_ns, observation.drp_record_ns}}; + for (const auto& [measurement, value] : detailed_timings) if (value) frame->record(measurement, value); + if (observation.queue_submit_wait_ns) frame->record(Frame_Trace_Measurement::backend_submit_queue_ns, observation.queue_submit_wait_ns); + if (observation.submit_ns) frame->record(Frame_Trace_Measurement::backend_submit_ns, observation.submit_ns); + if (observation.readback_ns) frame->record(Frame_Trace_Measurement::readback_ns, observation.readback_ns); + if (observation.gpu_completion_observation_ns) frame->record(Frame_Trace_Measurement::gpu_completion_observation_ns, observation.gpu_completion_observation_ns); + if (observation.completion_task_queue_ns) frame->record(Frame_Trace_Measurement::completion_task_queue_ns, observation.completion_task_queue_ns); + if (observation.gpu) { + frame->record(Frame_Trace_Measurement::gpu_render_ns, observation.gpu->render_ns); + frame->record(Frame_Trace_Measurement::gpu_transition_ns, observation.gpu->transition_ns); + frame->record(Frame_Trace_Measurement::gpu_copy_ns, observation.gpu->copy_ns); + frame->record(Frame_Trace_Measurement::gpu_total_ns, observation.gpu->total_ns); + } + Frame_3D_Access::assign_datoviz_observation(frame, std::move(observation)); +} +} // namespace detail + +struct Render_Scene_3D::Private : Prev_Private, private detail::Scene_Datoviz_State { + enum struct Frame_Phase : std::uint8_t { + available, /* Scene 没有借用本槽的 Frame。 */ + preparing, /* CPU DAG 或 Vulkan queue submit 尚未释放 Prepare 准入。 */ + submitted, /* Prepare 已释放,等待 GPU 回读与顺序交付。 */ + completing /* 唯一 completion task 正在交付本槽。 */ + }; + + struct Frame_Context final { + std::atomic phase{Frame_Phase::available}; + Frame_3D* frame{}; /* 可空、非拥有;available 时为空,调用方保持至 completed。 */ + Event_Batch events{}; + bool overlaps_gpu{}; + std::atomic_bool cpu_finished{}; + std::atomic_bool gpu_submitted{}; + std::atomic_bool collection_ready{}; + std::atomic_bool collection_completed{}; + std::chrono::steady_clock::time_point completion_observed_at{}; + std::optional completion_result{}; + std::exception_ptr completion_failure{}; + std::exception_ptr failure{}; + Frame_Callbacks callbacks{}; + std::optional datoviz_frame{}; + std::shared_ptr paint_context{}; + proxy graph{}; /* 拥有本槽正在运行的完整帧 DAG。 */ + }; + + Private(Camera_3D& camera_value, Axes_3D& axes_value, std::uint32_t gpu_index_value = 0, bool validation_enabled_value = false); + ~Private() override = default; + Render_Result render(Frame_3D* frame, Frame_Callbacks callbacks); +private: + static constexpr std::size_t frame_capacity{3}; + std::array frame_contexts{}; + std::atomic_bool prepare_active{}; + std::atomic_bool completion_busy{}; + std::atomic_bool backend_available{true}; + bool backend_initialized{}; + bool overlaps_gpu{}; + std::uint32_t gpu_index{}; + bool validation_enabled{}; + proxy camera{}; /* 非拥有观察;构建时的 Camera_3D 必须覆盖 Scene 生命周期。 */ + proxy axes{}; /* 非拥有观察;构建时的 Axes_3D 必须覆盖 Scene 生命周期。 */ + std::vector registrations{}; + proxy completion_graph{}; /* 复用的顺序回读/交付任务图。 */ + + [[nodiscard]] Prop current_properties(); + [[nodiscard]] detail::Scene_3D_Parameters current_parameters(const Prop& properties); + void initialize_or_validate_backend(const detail::Scene_3D_Parameters& parameters); + void build_frame_graph(Frame_Context& context, const detail::Scene_3D_Parameters& parameters); + void dispatch_events(Event_Batch& events) override; + void dispatch_datoviz(Event& event, Extent viewport); + void render_datoviz(Frame_Context& context, detail::Shared_Prepared_Visual_Batch visuals, detail::Scene_3D_Parameters parameters) noexcept; + void observe_datoviz_completion(Frame_Context& context, std::optional result, std::exception_ptr failure) noexcept; + void try_release_prepare(Frame_Context& context) noexcept; + void arm_completion() noexcept; + void consume_completion(); + void collect_datoviz(Frame_Context& context) noexcept; + void complete_frame(Frame_Context& context); + [[nodiscard]] bool completion_pending() const noexcept; + void fail_datoviz(std::exception_ptr failure) noexcept; +}; + +inline Render_Scene_3D::Private::Private(Camera_3D& camera_value, Axes_3D& axes_value, std::uint32_t gpu_index_value, bool validation_enabled_value) : gpu_index(gpu_index_value), validation_enabled(validation_enabled_value), camera(model_proxy_view(camera_value)), axes(model_proxy_view(axes_value)), completion_graph(scene::make_task_graph("render_3d.completion")) { + auto task = completion_graph->add("scene.complete", [this] { consume_completion(); }); + if (!task) throw std::logic_error("failed to create 3D completion task graph"); +} + +inline Render_Scene_3D::Prop Render_Scene_3D::Private::current_properties() { + auto& properties = this->template attachment(); + properties.internal.advance(); + return static_cast(*properties.internal.use()); +} + +inline detail::Scene_3D_Parameters Render_Scene_3D::Private::current_parameters(const Prop& properties) { + camera->advance(); + axes->advance(); + const auto axis = axes->use(); + return {properties.viewport, properties.clear_color, camera->use(), axis[0], axis[1], axis[2]}; +} + +inline void Render_Scene_3D::Private::initialize_or_validate_backend(const detail::Scene_3D_Parameters& parameters) { + std::vector current; + auto& renderables = relation(); + renderables.for_each_topological([&](Dag_Node_Id id, proxy& renderable) { + current.push_back({static_cast(id.value), std::addressof(renderable->use())}); + }); + if (current.empty()) throw std::invalid_argument("Render_Scene_3D requires at least one Visual"); + if (backend_initialized) { + const bool same = current.size() == registrations.size() && std::ranges::equal(current, registrations, [](const auto& left, const auto& right) { + return left.identity == right.identity && left.operations == right.operations; + }); + if (!same) throw std::logic_error("Render_Scene_3D Visual DAG cannot change after Datoviz initialization"); + return; + } + overlaps_gpu = std::ranges::all_of(current, [](const auto& visual) { + return visual.operations && visual.operations->overlaps_gpu; + }); + detail::Scene_Datoviz_State::initialize(gpu_index, validation_enabled, current, parameters); + registrations = std::move(current); + backend_initialized = true; +} + +inline void Render_Scene_3D::Private::build_frame_graph(Frame_Context& context, const detail::Scene_3D_Parameters& parameters) { + context.paint_context = std::make_shared(); + context.paint_context->parameters = parameters; + context.paint_context->visuals->reserve(registrations.size()); + for (const auto& registration : registrations) context.paint_context->visuals->push_back({registration.identity, {}}); + auto graph = scene::make_task_graph("render_3d.frame"); + auto begin = graph->add("scene.begin", [&context] { + if (!context.frame) throw std::logic_error("3D frame DAG lost its active frame"); + context.frame->mark(Frame_Trace_Marker::scene_render_started); + context.frame->mark(Frame_Trace_Marker::prepare_started); + context.frame->mark(Frame_Trace_Marker::prepare_finished); + context.frame->mark(Frame_Trace_Marker::paint_started); + }); + if (!begin) throw std::logic_error("failed to create 3D frame setup task"); + auto& renderables = relation(); + std::unordered_map> nodes; + std::size_t slot{}; + renderables.for_each_topological([&](Dag_Node_Id id, proxy& renderable) { + renderable->render(context.paint_context, slot++, &detail::publish_visual); + renderable->advance(); + auto component = renderable->taskflow(); + auto composed = graph->compose("renderable." + std::to_string(id.value), component); + if (!composed) throw std::logic_error("failed to compose a 3D Renderable task graph"); + if (graph->precede(*begin, *composed) != Precede_Task_Graph_Result::preceded) throw std::logic_error("failed to order 3D frame setup before a Renderable"); + nodes.emplace(id.value, std::move(*composed)); + }); + renderables.for_each_dependency([&](Dag_Node_Id target, Dag_Node_Id source) { + if (graph->precede(nodes.at(source.value), nodes.at(target.value)) != Precede_Task_Graph_Result::preceded) throw std::logic_error("failed to preserve a 3D Renderable DAG dependency"); + }); + auto backend = graph->add("scene.backend.prepare", [this, &context, paint = context.paint_context] { + if (!detail::prepared_visual_batch_complete(*paint->visuals)) throw std::logic_error("3D Scene published an incomplete Visual batch"); + render_datoviz(context, paint->visuals, paint->parameters); + }); + if (!backend) throw std::logic_error("failed to create 3D backend task"); + for (auto& [id, node] : nodes) if (graph->precede(node, *backend) != Precede_Task_Graph_Result::preceded) throw std::logic_error("failed to order a 3D Renderable before backend submission"); + context.graph = std::move(graph); + dispatch_collected_events(); +} + +inline void Render_Scene_3D::Private::dispatch_events(Event_Batch& events) { + auto& renderables = relation(); + aethera::detail::dispatch_scene_events(events, renderables); + if (!std::ranges::any_of(frame_contexts, [](const auto& context) { return context.phase.load(std::memory_order_acquire) == Frame_Phase::preparing; })) return; + for (auto& context : frame_contexts) { + if (context.phase.load(std::memory_order_acquire) != Frame_Phase::preparing || !context.frame) continue; + for (auto& event : events) context.events.push_back(std::move(event)); + return; + } +} + +inline void Render_Scene_3D::Private::dispatch_datoviz(Event& event, Extent viewport) { + if (event.is_accepted()) return; + const auto* pointer = dynamic_cast*>(std::addressof(event)); + const auto* wheel = dynamic_cast*>(std::addressof(event)); + const auto* key = dynamic_cast(std::addressof(event)); + const bool pointer_valid = pointer && std::isfinite(pointer->position.x) && std::isfinite(pointer->position.y) && pointer->position.x >= 0.0F && pointer->position.y >= 0.0F && pointer->position.x <= static_cast(viewport.width) && pointer->position.y <= static_cast(viewport.height); + if (wheel && pointer_valid) { + dispatch_wheel(pointer->position.x, pointer->position.y, detail::wheel_step(wheel->pixel_delta_x, wheel->angle_delta_x), detail::wheel_step(wheel->pixel_delta_y, wheel->angle_delta_y), pointer->modifiers, viewport, event.occurred_at.nanoseconds); + } + else if (pointer_valid && (event.type == Event_Type::pointer_move || event.type == Event_Type::pointer_press || event.type == Event_Type::pointer_release)) { + const auto button = event.type == Event_Type::pointer_move ? detail::held_button(pointer->buttons) : pointer->button; + dispatch_pointer(event.type, pointer->position.x, pointer->position.y, button, pointer->modifiers, viewport, event.occurred_at.nanoseconds); + } + else if (key) { + dispatch_key(*key); + } + if ((wheel && pointer_valid) || pointer_valid || key) event.accept(); +} + +inline void Render_Scene_3D::Private::render_datoviz(Frame_Context& context, detail::Shared_Prepared_Visual_Batch visuals, detail::Scene_3D_Parameters parameters) noexcept { + try { + if (!visuals || visuals->empty()) throw std::invalid_argument("3D Scene requires prepared Visual data"); + if (!backend_available.load(std::memory_order_acquire)) throw std::runtime_error("3D Scene resources are unavailable"); + auto* frame = context.frame; + if (!frame) throw std::logic_error("3D backend lost its Frame"); + auto reservation = detail::Gpu_Completion_Service::instance().prepare([this, &context](detail::Gpu_Completion_Service::Result result) { + observe_datoviz_completion(context, std::move(result), {}); + }, [this, &context](std::exception_ptr failure) { + observe_datoviz_completion(context, {}, std::move(failure)); + }, frame->taskflow_trace_requested()); + if (!reservation) throw std::runtime_error("GPU completion capacity is exhausted"); + const auto sequence = frame->identity().sequence; + const bool observe = frame->taskflow_trace_requested(); + const bool readback = frame->output() == Frame_3D_Output::pixels; + std::optional prepared; + if (context.events.empty()) { + frame->mark(Frame_Trace_Marker::backend_prepare_started); + prepared = reuse_recorded_target(parameters, *visuals, sequence, observe, readback); + } + if (!prepared) { + frame->mark(Frame_Trace_Marker::event_dispatch_started); + for (auto& event : context.events) if (event) dispatch_datoviz(*event, parameters.viewport); + frame->mark(Frame_Trace_Marker::event_dispatch_finished); + context.events.clear(); + frame->mark(Frame_Trace_Marker::backend_prepare_started); + prepared = prepare(parameters, *visuals, sequence, observe, readback); + } + if (!prepared) throw std::logic_error("Datoviz did not provide a frame target after Scene admission"); + frame->mark(Frame_Trace_Marker::backend_prepare_finished); + context.datoviz_frame.emplace(std::move(*prepared)); + context.datoviz_frame->observation.prepare_released_after_submission = overlaps_gpu; + frame->mark(Frame_Trace_Marker::backend_queue_entered); + frame->mark(Frame_Trace_Marker::backend_submit_queued); + auto shared_reservation = std::make_shared(std::move(reservation.reservation)); + submit(*context.datoviz_frame, [this, &context, frame, shared_reservation = std::move(shared_reservation)](std::exception_ptr submission_failure) mutable { + try { + frame->mark(Frame_Trace_Marker::backend_queue_left); + if (submission_failure) { + fail_datoviz(submission_failure); + if (context.datoviz_frame) { + discard(std::move(*context.datoviz_frame)); + context.datoviz_frame.reset(); + } + context.failure = std::move(submission_failure); + context.overlaps_gpu = false; + context.collection_completed.store(true, std::memory_order_release); + context.gpu_submitted.store(true, std::memory_order_release); + } + else { + frame->mark(Frame_Trace_Marker::gpu_submitted); + context.overlaps_gpu = overlaps_gpu; + shared_reservation->watch(context.datoviz_frame->device, context.datoviz_frame->fence); + context.gpu_submitted.store(true, std::memory_order_release); + } + try_release_prepare(context); + if (submission_failure) arm_completion(); + } + catch (...) { + observe_datoviz_completion(context, {}, std::current_exception()); + } + }); + } + catch (...) { + auto failure = std::current_exception(); + fail_datoviz(failure); + if (context.datoviz_frame) { + discard(std::move(*context.datoviz_frame)); + context.datoviz_frame.reset(); + } + context.failure = std::move(failure); + context.overlaps_gpu = false; + context.collection_completed.store(true, std::memory_order_release); + context.gpu_submitted.store(true, std::memory_order_release); + try_release_prepare(context); + arm_completion(); + } +} + +inline void Render_Scene_3D::Private::observe_datoviz_completion(Frame_Context& context, std::optional result, std::exception_ptr failure) noexcept { + if (!context.frame) return; + context.frame->mark(Frame_Trace_Marker::gpu_completed); + context.completion_result = std::move(result); + context.completion_failure = std::move(failure); + context.completion_observed_at = std::chrono::steady_clock::now(); + context.collection_ready.store(true, std::memory_order_release); + arm_completion(); +} + +inline void Render_Scene_3D::Private::try_release_prepare(Frame_Context& context) noexcept { + if (!context.cpu_finished.load(std::memory_order_acquire) || !context.gpu_submitted.load(std::memory_order_acquire) || (!context.overlaps_gpu && !context.collection_completed.load(std::memory_order_acquire))) return; + auto expected = Frame_Phase::preparing; + if (!context.phase.compare_exchange_strong(expected, Frame_Phase::submitted, std::memory_order_acq_rel, std::memory_order_acquire)) return; + prepare_active.store(false, std::memory_order_release); + if (context.callbacks.submitted) { + try { + context.callbacks.submitted(context.frame, context.overlaps_gpu); + } + catch (...) { + context.failure = detail::contextual_exception("delivering 3D submission", std::current_exception()); + } + } + if (context.collection_completed.load(std::memory_order_acquire)) arm_completion(); +} + +inline bool Render_Scene_3D::Private::completion_pending() const noexcept { + return std::ranges::any_of(frame_contexts, [](const auto& context) { + const auto phase = context.phase.load(std::memory_order_acquire); + return context.collection_ready.load(std::memory_order_acquire) || (phase == Frame_Phase::submitted && context.collection_completed.load(std::memory_order_acquire)); + }); +} + +inline void Render_Scene_3D::Private::arm_completion() noexcept { + bool expected{}; + if (!completion_busy.compare_exchange_strong(expected, true, std::memory_order_acq_rel, std::memory_order_acquire)) return; + const auto submitted = completion_graph->run([this](std::exception_ptr failure) { + completion_busy.store(false, std::memory_order_release); + if (completion_pending()) arm_completion(); + if (failure) Failure_Policy::handle_unknown_failure(std::move(failure)); + }); + if (submitted != Run_Taskflow_Result::submitted) { + completion_busy.store(false, std::memory_order_release); + fail_datoviz(std::make_exception_ptr(std::runtime_error("3D completion task runtime is unavailable"))); + } +} + +inline void Render_Scene_3D::Private::consume_completion() { + for (;;) { + Frame_Context* collecting{}; + for (auto& context : frame_contexts) { + if (!context.collection_ready.load(std::memory_order_acquire) || !context.frame) continue; + if (!collecting || context.frame->identity().sequence < collecting->frame->identity().sequence) collecting = std::addressof(context); + } + if (collecting) { + if (!collecting->collection_ready.exchange(false, std::memory_order_acq_rel)) continue; + collect_datoviz(*collecting); + continue; + } + Frame_Context* selected{}; + for (auto& context : frame_contexts) { + if (context.phase.load(std::memory_order_acquire) != Frame_Phase::submitted || !context.collection_completed.load(std::memory_order_acquire) || !context.frame) continue; + if (!selected || context.frame->identity().sequence < selected->frame->identity().sequence) selected = std::addressof(context); + } + if (!selected) return; + auto expected = Frame_Phase::submitted; + if (!selected->phase.compare_exchange_strong(expected, Frame_Phase::completing, std::memory_order_acq_rel, std::memory_order_acquire)) continue; + complete_frame(*selected); + } +} + +inline void Render_Scene_3D::Private::collect_datoviz(Frame_Context& context) noexcept { + try { + auto* frame = context.frame; + if (!frame || !context.datoviz_frame) throw std::logic_error("3D completion lost its Datoviz target"); + const auto completion_started = std::chrono::steady_clock::now(); + frame->mark(Frame_Trace_Marker::readback_started); + auto result = std::move(context.completion_result); + auto failure = std::move(context.completion_failure); + context.completion_result.reset(); + context.completion_failure = {}; + auto& observation = context.datoviz_frame->observation; + if (result) observation.gpu_completion_observation_ns = result->wait_duration_ns; + observation.completion_task_queue_ns = static_cast(std::max(0, std::chrono::duration_cast(completion_started - context.completion_observed_at).count())); + if (failure) std::rethrow_exception(std::move(failure)); + if (!result || result->error != detail::Gpu_Completion_Service::Completion_Error::none) throw std::runtime_error("Datoviz GPU completion failed"); + auto completed = collect(std::move(*context.datoviz_frame)); + context.datoviz_frame.reset(); + frame->mark(Frame_Trace_Marker::readback_finished); + detail::publish_datoviz_observation(frame, std::move(completed.observation)); + std::array outputs{frame}; + detail::Frame_3D_Access::assign_pixels(outputs, completed.extent, std::move(completed.pixels), frame->identity()); + } + catch (...) { + context.failure = std::current_exception(); + fail_datoviz(context.failure); + if (context.datoviz_frame) { + quarantine(std::move(*context.datoviz_frame)); + context.datoviz_frame.reset(); + } + } + context.collection_completed.store(true, std::memory_order_release); + try_release_prepare(context); +} + +inline void Render_Scene_3D::Private::complete_frame(Frame_Context& context) { + auto* frame = context.frame; + auto callback = std::move(context.callbacks.completed); + auto failure = std::move(context.failure); + frame->mark(Frame_Trace_Marker::paint_finished); + frame->mark(Frame_Trace_Marker::scene_render_finished); + frame->mark(Frame_Trace_Marker::frame_ready); + context.frame = nullptr; + context.events.clear(); + context.datoviz_frame.reset(); + context.paint_context.reset(); + context.graph = {}; + context.overlaps_gpu = false; + context.cpu_finished.store(false, std::memory_order_relaxed); + context.gpu_submitted.store(false, std::memory_order_relaxed); + context.collection_ready.store(false, std::memory_order_relaxed); + context.collection_completed.store(false, std::memory_order_relaxed); + context.completion_observed_at = {}; + context.completion_result.reset(); + context.completion_failure = {}; + context.failure = {}; + context.callbacks = {}; + context.phase.store(Frame_Phase::available, std::memory_order_release); + callback(frame, std::move(failure)); +} + +inline void Render_Scene_3D::Private::fail_datoviz(std::exception_ptr failure) noexcept { + backend_available.store(false, std::memory_order_release); + try { + if (failure) std::rethrow_exception(std::move(failure)); + } + catch (const std::exception& exception) { + std::fprintf(stderr, "Render_Scene_3D backend failure: %s\n", exception.what()); + } + catch (...) {} +} + +inline Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Frame_3D* frame, Frame_Callbacks callbacks) { + if (!frame) return Render_Result::frame_missing; + if (!callbacks.completed) return Render_Result::completion_missing; + frame->mark(Frame_Trace_Marker::scene_render_entered); + bool inactive{}; + if (!prepare_active.compare_exchange_strong(inactive, true, std::memory_order_acq_rel, std::memory_order_acquire)) return Render_Result::frame_pipeline_busy; + Frame_Context* context{}; + for (auto& candidate : frame_contexts) { + auto expected = Frame_Phase::available; + if (candidate.phase.compare_exchange_strong(expected, Frame_Phase::preparing, std::memory_order_acq_rel, std::memory_order_acquire)) { + context = std::addressof(candidate); + break; + } + } + if (!context) { + prepare_active.store(false, std::memory_order_release); + return Render_Result::frame_pipeline_busy; + } + context->frame = frame; + context->callbacks = std::move(callbacks); + context->failure = {}; + context->completion_failure = {}; + context->completion_result.reset(); + context->datoviz_frame.reset(); + context->events.clear(); + context->overlaps_gpu = false; + context->cpu_finished.store(false, std::memory_order_relaxed); + context->gpu_submitted.store(false, std::memory_order_relaxed); + context->collection_ready.store(false, std::memory_order_relaxed); + context->collection_completed.store(false, std::memory_order_relaxed); + const auto reject = [&](Render_Result result) { + context->frame = nullptr; + context->callbacks = {}; + context->phase.store(Frame_Phase::available, std::memory_order_release); + prepare_active.store(false, std::memory_order_release); + return result; + }; + try { + frame->mark(Frame_Trace_Marker::scene_advance_started); + commit(); + const auto properties = current_properties(); + if (!properties.view_active) return reject(Render_Result::view_inactive); + if (properties.viewport.empty()) return reject(Render_Result::empty_viewport); + if (!backend_available.load(std::memory_order_acquire)) return reject(Render_Result::backend_unavailable); + const auto parameters = current_parameters(properties); + initialize_or_validate_backend(parameters); + build_frame_graph(*context, parameters); + frame->mark(Frame_Trace_Marker::scene_advance_finished); + frame->mark(Frame_Trace_Marker::scene_render_requested); + } + catch (...) { + fail_datoviz(std::current_exception()); + return reject(Render_Result::backend_unavailable); + } + const auto submitted = context->graph->run([this, context](std::exception_ptr failure) { + const bool failed = static_cast(failure); + if (failed) { + context->failure = std::move(failure); + context->overlaps_gpu = false; + if (!context->gpu_submitted.load(std::memory_order_acquire)) { + context->gpu_submitted.store(true, std::memory_order_release); + context->collection_completed.store(true, std::memory_order_release); + } + } + context->cpu_finished.store(true, std::memory_order_release); + try_release_prepare(*context); + if (failed) arm_completion(); + }); + if (submitted != Run_Taskflow_Result::submitted) return reject(Render_Result::backend_unavailable); + return Render_Result::submitted; +} +} // namespace aethera::render_3d diff --git a/render_3D/render_3D/scene/detail/Datoviz_Frame_Pipeline.ipp b/render_3D/render_3D/scene/detail/Datoviz_Frame_Pipeline.ipp new file mode 100644 index 0000000..ac58fc1 --- /dev/null +++ b/render_3D/render_3D/scene/detail/Datoviz_Frame_Pipeline.ipp @@ -0,0 +1,494 @@ +#pragma once +#include "Datoviz_Scene_Resources.ipp" +#include "../../visual/detail/Datoviz_Visual_Set.ipp" +#include "Datoviz_Interaction.ipp" +namespace aethera::render_3d::detail { +namespace { +void accumulate_execution_timing(const DvzDrp2ExecutionTiming& source, Datoviz_Frame_Observation& target) noexcept { + const auto command = [&](DvzDrp2CommandType type) noexcept { + return source.command_ns[static_cast(type)]; + }; + const auto count = [&](DvzDrp2CommandType type) noexcept { + return source.command_count[static_cast(type)]; + }; + target.drp_validation_ns += source.validation_ns; + target.drp_state_ns += source.state_ensure_ns + source.state_commit_ns; + target.drp_buffer_create_ns += command(DVZ_DRP2_COMMAND_CREATE_BUFFER); + target.drp_texture_create_ns += command(DVZ_DRP2_COMMAND_CREATE_TEXTURE); + target.drp_shader_create_ns += command(DVZ_DRP2_COMMAND_CREATE_SHADER_MODULE); + target.drp_shader_compile_ns += source.shader_compile_ns; + target.drp_shader_module_create_ns += source.shader_module_create_ns; + target.drp_pipeline_create_ns += command(DVZ_DRP2_COMMAND_CREATE_RENDER_PIPELINE) + command(DVZ_DRP2_COMMAND_CREATE_COMPUTE_PIPELINE); + target.drp_pipeline_create_count += count(DVZ_DRP2_COMMAND_CREATE_RENDER_PIPELINE) + count(DVZ_DRP2_COMMAND_CREATE_COMPUTE_PIPELINE); + target.drp_binding_create_ns += command(DVZ_DRP2_COMMAND_CREATE_SAMPLER) + command(DVZ_DRP2_COMMAND_CREATE_BIND_GROUP_LAYOUT) + command(DVZ_DRP2_COMMAND_CREATE_BIND_GROUP); + target.drp_upload_ns += command(DVZ_DRP2_COMMAND_WRITE_BUFFER) + command(DVZ_DRP2_COMMAND_WRITE_TEXTURE); + target.drp_upload_decode_ns += source.upload_decode_ns; + target.drp_upload_vulkan_allocate_ns += source.upload_vulkan_allocate_ns; + target.drp_upload_host_copy_ns += source.upload_host_copy_ns; + target.drp_upload_command_allocate_ns += source.upload_command_allocate_ns; + target.drp_upload_command_record_ns += source.upload_command_record_ns; + target.drp_upload_fence_create_ns += source.upload_fence_create_ns; + target.drp_upload_submit_enqueue_ns += source.upload_submit_enqueue_ns; + target.drp_upload_submit_queue_wait_ns += source.upload_submit_queue_wait_ns; + target.drp_upload_queue_submit_ns += source.upload_queue_submit_ns; + target.drp_upload_fence_wait_ns += source.upload_fence_wait_ns; + target.drp_upload_retire_ns += source.upload_retire_ns; + target.drp_transfer_ns += command(DVZ_DRP2_COMMAND_COPY_BUFFER_TO_BUFFER) + command(DVZ_DRP2_COMMAND_COPY_BUFFER_TO_TEXTURE) + command(DVZ_DRP2_COMMAND_COPY_TEXTURE_TO_BUFFER) + command(DVZ_DRP2_COMMAND_COPY_TEXTURE_TO_TEXTURE); + constexpr std::array record_commands{DVZ_DRP2_COMMAND_BEGIN_RENDER_PASS, DVZ_DRP2_COMMAND_BEGIN_COMPUTE_PASS, DVZ_DRP2_COMMAND_SET_VIEWPORT, DVZ_DRP2_COMMAND_SET_SCISSOR, DVZ_DRP2_COMMAND_SET_PIPELINE, DVZ_DRP2_COMMAND_SET_BIND_GROUP, DVZ_DRP2_COMMAND_SET_VERTEX_BUFFER, DVZ_DRP2_COMMAND_SET_INDEX_BUFFER, DVZ_DRP2_COMMAND_DRAW, DVZ_DRP2_COMMAND_DRAW_INDEXED, DVZ_DRP2_COMMAND_DISPATCH_WORKGROUPS, DVZ_DRP2_COMMAND_END_RENDER_PASS, DVZ_DRP2_COMMAND_END_COMPUTE_PASS, DVZ_DRP2_COMMAND_RESOURCE_BARRIER}; + for (const auto type : record_commands) target.drp_record_ns += command(type); +} +} + +/* Datoviz Scene 根据当前权威资源生成 DRP2 命令流描述,不提交 GPU。 */ +DvzSceneFrameArtifact* Scene_Datoviz_State::emit(const Scene_3D_Parameters& scene, std::uint8_t target_index, DvzSceneFrameEmitTiming& timing) { + if (target_index >= runtime_slots_.size() || runtime_slots_[target_index].emitter == nullptr) throw std::logic_error("Datoviz frame target emitter is unavailable"); + DvzFramePlanEmitConfig configuration = dvz_frame_plan_emit_config(); + configuration.shader_format = DVZ_SCENE_SHADER_FORMAT_GLSL; + configuration.external_color_target = true; + configuration.color_target_id = color_target_id; + configuration.color_target_format = DVZ_FORMAT_R8G8B8A8_UNORM; + configuration.target_width = scene.viewport.width; + configuration.target_height = scene.viewport.height; + configuration.clear_color[0] = scene.clear_color.red; + configuration.clear_color[1] = scene.clear_color.green; + configuration.clear_color[2] = scene.clear_color.blue; + configuration.clear_color[3] = scene.clear_color.alpha; + configuration.timing = &timing; + const auto capabilities = offscreen_capabilities(); + DvzDiagnosticReport report{}; + dvz_diagnostic_report_init(&report); + /* Every frame target owns a DRP2 runtime and its matching retained emitter. + * A stream is incremental relative to exactly one runtime; sharing an emitter + * across the three targets makes target N receive commands that assume target + * N-1's resource state. Replaying retained payloads also compensates for the + * Scene-wide dirty flags committed by another target's preceding emission. */ + auto* artifact = dvz_figure_emit_frame_with_emitter(figure_, runtime_slots_[target_index].emitter, true, &capabilities, &report, &configuration); + if (artifact == nullptr) { + std::string message = "failed to emit Datoviz visual frame"; + const auto count = dvz_diagnostic_report_count(&report); + if (count != 0) { + if (const char* diagnostic = dvz_diagnostic_report_get(&report, 0)) message += ": " + std::string(diagnostic); + } + throw std::runtime_error(message); + } + return artifact; +} + +std::optional Scene_Datoviz_State::acquire_target(Extent extent) { + for (std::uint8_t index = 0; index < targets_->values.size(); ++index) { + auto& candidate = targets_->values[index]; + if (candidate && candidate->available() && candidate->extent() == extent) return index; + } + for (std::uint8_t index = 0; index < targets_->values.size(); ++index) { + auto& candidate = targets_->values[index]; + if (candidate && !candidate->available()) continue; + candidate = std::make_unique(render_context_->gpu_context(), reinterpret_cast(command_pool_), extent, ++target_generation_); + return index; + } + return std::nullopt; +} + +Datoviz_Frame_Target& Scene_Datoviz_State::target(const Datoviz_Pending_Frame& pending) { + if (pending.target_index >= targets_->values.size()) throw std::logic_error("Datoviz pending frame target index is invalid"); + auto& result = targets_->values[pending.target_index]; + if (!result || result->generation() != pending.target_generation) throw std::logic_error("Datoviz pending frame target no longer exists"); + return *result; +} + +std::optional Scene_Datoviz_State::reuse_recorded_target(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared, std::uint64_t frame_sequence, bool observe, bool readback) { + Datoviz_Frame_Observation observation; + observation.render_sequence = frame_sequence; + observation.path = Datoviz_Frame_Path::reused; + observation.gpu_timing_requested = observe; + observation.readback_requested = readback; + const std::uint64_t started = trace_now_ns(); + std::lock_guard target_lock(target_mutex_); + if (!matches_command_structure(scene, prepared)) return std::nullopt; + for (std::uint8_t target_index = 0; target_index < targets_->values.size(); ++target_index) { + auto& candidate = targets_->values[target_index]; + if (!candidate || candidate->extent() != scene.viewport || !candidate->can_reuse(command_revision_, controller_revision_, readback)) continue; + if (!target_runtime_resources_current(prepared, target_index)) continue; + /* The command buffer already binds this target's independent region. + * Only immutable frame payload bytes may change on this path. No + * Scene graph, emitter, runtime, allocator, camera or axis object is + * touched, so different Scene preparation workers can write their own + * mapped allocations in parallel without weakening the global API + * serialization used by structural Datoviz operations. */ + for (const auto& source : prepared) { + auto visual = std::ranges::find_if(visuals_, [&](const Datoviz_Visual_Instance& value) { + return value.identity == source.identity; + }); + if (visual == visuals_.end()) throw std::logic_error("recorded Datoviz frame contains an unknown Visual identity"); + if (source.visual.operations != visual->operations) throw std::logic_error("recorded Datoviz Visual operations do not match registration"); + if (visual->operations->external_attributes.empty()) continue; + if (!source.visual.has_payload()) throw std::logic_error("recorded Datoviz Visual has no immutable payload"); + if (source.visual.item_count() != 0 && visual->uploaded_data_revisions[target_index] != source.visual.data_revision) { + observation.uploaded_bytes += upload_external_attributes(*visual, source.visual, target_index); + visual->uploaded_data_revisions[target_index] = source.visual.data_revision; + } + visual->applied_revision = source.visual.revision; + visual->applied_data_revision = source.visual.data_revision; + } + candidate->reuse(command_revision_, controller_revision_, observe, readback); + observation.apply_ns = trace_now_ns() - started; + return Datoviz_Pending_Frame{reinterpret_cast(candidate->device()), reinterpret_cast(candidate->fence()), scene.viewport, target_index, candidate->generation(), std::move(observation)}; + } + return std::nullopt; +} + +/* 取得一个空闲目标槽,应用资源、生成命令并完成录制;没有空槽时立即返回。 */ +std::optional Scene_Datoviz_State::prepare(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback) { + if (scene.viewport.empty()) return std::nullopt; + if (auto reused = reuse_recorded_target(scene, visuals, frame_sequence, observe, readback)) return reused; + /* + * DRP2 runtime 的结构执行会创建/上传资源,并可能在内部使用共享主 + * VkQueue。它必须和 Render Domain 的显式 submit 使用同一外部同步域。 + * 热路径的已录制目标复用在此锁之前返回,仍可在各 Scene 准备线程并行。 + */ + Datoviz_Frame_Observation observation; + observation.render_sequence = frame_sequence; + observation.path = Datoviz_Frame_Path::recorded; + observation.gpu_timing_requested = observe; + observation.readback_requested = readback; + observation.controller_input_applied = input_changed_; + std::uint64_t phase_started = trace_now_ns(); + std::optional target_index; + { + std::lock_guard resource_lock(target_mutex_); + target_index = acquire_target(scene.viewport); + } + observation.target_acquire_ns = trace_now_ns() - phase_started; + if (!target_index) return std::nullopt; + phase_started = trace_now_ns(); + const bool content_changed = !matches_command_structure(scene, visuals); + observation.structure_check_ns = trace_now_ns() - phase_started; + if (content_changed) ++command_revision_; + auto& frame_target = *targets_->values[*target_index]; + const bool bind_target = !frame_target.can_reuse_commands(command_revision_, readback); + phase_started = trace_now_ns(); + observation.uploaded_bytes = apply(scene, visuals, *target_index, bind_target); + observation.apply_ns = trace_now_ns() - phase_started; + bool query_changed_scene{}; + phase_started = trace_now_ns(); + if (item_interaction_ != nullptr) { + static_cast(dvz_figure_process_queries(figure_, runtime_slots_[*target_index].runtime, nullptr)); + DvzQueryResult query{}; + bool resolved{}; + while (dvz_scene_poll_query(scene_, &query)) resolved = true; + if (resolved) { + query_changed_scene = true; + if (hover_readout_ != nullptr) { + dvz_pinned_readout_destroy(hover_readout_); + hover_readout_ = nullptr; + } + if (query.hit) { + if (query.value_kind == DVZ_QUERY_VALUE_NONE) { + if (query.has_data_position || query.has_visual_position) { + query.value_kind = DVZ_QUERY_VALUE_VEC3; + const auto& position = query.has_data_position + ? query.data_position + : query.visual_position; + std::ranges::copy(position, query.vector); + constexpr char position_label[] = "Position"; + std::ranges::copy(position_label, query.label); + } + else { + query.value_kind = DVZ_QUERY_VALUE_SCALAR; + query.scalar = static_cast(query.item_id); + constexpr char item_label[] = "Item"; + std::ranges::copy(item_label, query.label); + } + } + hover_readout_ = dvz_pinned_readout_query(panel_, &query); + } + } + } + observation.query_ns = trace_now_ns() - phase_started; + if (query_changed_scene) ++command_revision_; + bool runtime_resources_updated{}; + if (frame_target.can_reuse_commands(command_revision_, readback) && !target_runtime_resources_current(visuals, *target_index)) { + DvzDiagnosticReport report{}; + dvz_diagnostic_report_init(&report); + std::uint64_t uploaded_bytes{}; + bool command_recording_valid{}; + phase_started = trace_now_ns(); + const std::uint64_t cpu_started = trace_thread_cpu_ns(); + const auto replay_visuals = stale_runtime_visuals(visuals, *target_index); + std::unique_ptr update_stream{dvz_figure_prepare_runtime_resources(figure_, runtime_slots_[*target_index].emitter, replay_visuals.data(), static_cast(replay_visuals.size()), &uploaded_bytes, &command_recording_valid, &report), &dvz_drp2_stream_destroy}; + observation.runtime_plan_ns = trace_now_ns() - phase_started; + observation.runtime_plan_cpu_ns = trace_thread_cpu_ns() - cpu_started; + if (!update_stream) { + std::string message = "failed to prepare Datoviz retained runtime resources"; + if (dvz_diagnostic_report_count(&report) != 0) if (const char* diagnostic = dvz_diagnostic_report_get(&report, 0)) message += ": " + std::string(diagnostic); + throw std::runtime_error(std::move(message)); + } + phase_started = trace_now_ns(); + const std::uint64_t execute_cpu_started = trace_thread_cpu_ns(); + DvzDrp2ValidationResult update_result{}; + { + DvzDrp2ExecutionTiming timing{}; + update_result = dvz_drp2_runtime_execute_timed(runtime_slots_[*target_index].runtime, update_stream.get(), &timing); + accumulate_execution_timing(timing, observation); + } + observation.runtime_execute_ns = trace_now_ns() - phase_started; + observation.runtime_execute_cpu_ns = trace_thread_cpu_ns() - execute_cpu_started; + if (!update_result.ok) throw std::runtime_error("failed to execute Datoviz retained runtime resources"); + dvz_figure_commit_runtime_resources(figure_); + observation.uploaded_bytes += uploaded_bytes; + mark_target_runtime_resources_uploaded(*target_index); + runtime_resources_updated = true; + if (!command_recording_valid) frame_target.invalidate_recording(); + } + /* Controller input changes only mapped common MVP buffers. The selected + * target is available, so its own runtime allocation can be updated without + * rebuilding the Figure artifact or re-recording commands. Structural Scene + * changes keep using command_revision_ and fall through to full recording. */ + bool mvp_updated{}; + phase_started = trace_now_ns(); + if (frame_target.can_reuse_commands(command_revision_, readback) && !frame_target.can_reuse(command_revision_, controller_revision_, readback)) { + std::uint64_t mvp_uploaded_bytes{}; + if (dvz_figure_update_mvp_buffers(figure_, runtime_slots_[*target_index].emitter, runtime_slots_[*target_index].runtime, &mvp_uploaded_bytes)) { + frame_target.mark_mvp_uploaded(controller_revision_); + observation.uploaded_bytes += mvp_uploaded_bytes; + mvp_updated = true; + } + } + observation.mvp_update_ns = trace_now_ns() - phase_started; + if (frame_target.can_reuse(command_revision_, controller_revision_, readback)) { + { + std::lock_guard target_lock(target_mutex_); + frame_target.reuse(command_revision_, controller_revision_, observe, readback); + } + input_changed_ = false; + observation.path = (runtime_resources_updated || mvp_updated) + ? Datoviz_Frame_Path::updated + : Datoviz_Frame_Path::reused; + return Datoviz_Pending_Frame{reinterpret_cast(frame_target.device()), reinterpret_cast(frame_target.fence()), scene.viewport, *target_index, frame_target.generation(), std::move(observation)}; + } + { + phase_started = trace_now_ns(); + std::lock_guard recording_lock(target_mutex_); + frame_target.begin(observe, readback, command_revision_, controller_revision_); + observation.frame_begin_ns = trace_now_ns() - phase_started; + } + struct Recording_Scope { + Datoviz_Frame_Target& target; /* 异常退出时回收尚未发布的录制槽。 */ + std::mutex& target_mutex; /* 本 Scene Frame Target 的生命周期门。 */ + bool released{}; /* finish_recording 成功后禁止回滚。 */ + ~Recording_Scope() { + if (released) return; + try { + std::lock_guard recording_lock(target_mutex); + target.abort(); + } + catch (...) {} + } + } recording_scope{frame_target, target_mutex_}; + std::unique_ptr artifact{nullptr, &dvz_scene_frame_artifact_destroy}; + try { + phase_started = trace_now_ns(); + const std::uint64_t cpu_started = trace_thread_cpu_ns(); + DvzSceneFrameEmitTiming timing{}; + artifact.reset(emit(scene, *target_index, timing)); + observation.frame_plan_ns = trace_now_ns() - phase_started; + observation.frame_plan_cpu_ns = trace_thread_cpu_ns() - cpu_started; + observation.emit_replay_dirty_ns = timing.replay_dirty_ns; + observation.emit_layout_ns = timing.layout_ns; + observation.emit_prepare_ns = timing.prepare_ns; + observation.emit_plan_build_ns = timing.plan_build_ns; + observation.emit_contract_ns = timing.contract_ns; + observation.emit_stream_ns = timing.stream_emit_ns; + observation.emit_stream_freeze_ns = timing.stream_freeze_ns; + observation.emit_commit_ns = timing.commit_ns; + observation.emit_plan_reset_ns = timing.plan_reset_ns; + observation.emit_artifact_create_ns = timing.artifact_create_ns; + observation.emit_artifact_freeze_ns = timing.artifact_freeze_ns; + observation.emit_packet_encode_ns = timing.artifact_encode_ns; + } + catch (...) { + raise_context("preparing Datoviz frame", std::current_exception()); + } + if (observe) { + observation.artifact_status = static_cast(dvz_scene_frame_artifact_status(artifact.get())); + observation.artifact_resource_version = dvz_scene_frame_artifact_resource_version(artifact.get()); + observation.artifact_frame_index = dvz_scene_frame_artifact_frame_index(artifact.get()); + } + const DvzDrp2CommandStream* stream = dvz_scene_frame_artifact_stream(artifact.get()); + phase_started = trace_now_ns(); + register_external_attributes(stream, *target_index); + observation.external_register_ns = trace_now_ns() - phase_started; + const DvzStreamFrame target_frame = frame_target.stream_frame(); + bool attached{}; + DvzDrp2ValidationResult result{}; + phase_started = trace_now_ns(); + attached = stream != nullptr && dvz_drp2_runtime_attach_frame_target(runtime_slots_[*target_index].runtime, color_target_id, &target_frame); + observation.frame_attach_ns = trace_now_ns() - phase_started; + if (attached) { + phase_started = trace_now_ns(); + const std::uint64_t cpu_started = trace_thread_cpu_ns(); + DvzDrp2ExecutionTiming timing{}; + result = dvz_drp2_runtime_execute_timed(runtime_slots_[*target_index].runtime, stream, &timing); + accumulate_execution_timing(timing, observation); + observation.frame_execute_ns = trace_now_ns() - phase_started; + observation.frame_execute_cpu_ns = trace_thread_cpu_ns() - cpu_started; + } + observation.validation_performed = true; + observation.validation_ok = attached && result.ok; + observation.validation_code = static_cast(result.code); + observation.validation_command_index = result.command_index; + if (!observation.validation_ok) { + if (char* json = dvz_scene_frame_artifact_json(artifact.get(), "renderive_frame")) { + observation.artifact_json = json; + dvz_drp2_stream_json_destroy(json); + } + } + artifact.reset(); + if (!attached) { + throw std::runtime_error("failed to attach the Datoviz point frame target"); + } + if (!result.ok) { + std::string message = "failed to execute Datoviz point frame: validation code " + std::to_string(static_cast(result.code)) + ", command " + std::to_string(result.command_index); + if (!observation.artifact_json.empty()) message += ", failing command " + artifact_command_excerpt(observation.artifact_json, result.command_index); + throw std::runtime_error(std::move(message)); + } + mark_target_runtime_resources_uploaded(*target_index); + { + phase_started = trace_now_ns(); + std::lock_guard recording_lock(target_mutex_); + frame_target.finish_recording(); + observation.frame_finish_ns = trace_now_ns() - phase_started; + } + recording_scope.released = true; + input_changed_ = false; + return Datoviz_Pending_Frame{reinterpret_cast(frame_target.device()), reinterpret_cast(frame_target.fence()), scene.viewport, *target_index, frame_target.generation(), std::move(observation)}; +} + +/* 把已录制槽投递到 GPU 队列;这里只登记完成回调,不执行同步等待。 */ +void Scene_Datoviz_State::submit(Datoviz_Pending_Frame& pending, std::function completion) { + if (!completion) throw std::invalid_argument("Datoviz submission completion is empty"); + const std::uint64_t queued = trace_now_ns(); + render_context_->enqueue_submission([this, &pending, queued, completion = std::move(completion)]() mutable { + try { + pending.observation.queue_submit_wait_ns = trace_now_ns() - queued; + std::lock_guard target_lock(target_mutex_); + const std::uint64_t started = trace_now_ns(); + target(pending).submit(); + pending.observation.submit_ns = trace_now_ns() - started; + completion({}); + } + catch (...) { + completion(std::current_exception()); + } + }); +} + +/* Fence 已由完成服务确认后读取映射内存,发布像素并把槽恢复为空闲。 */ +Datoviz_Completed_Frame Scene_Datoviz_State::collect(Datoviz_Pending_Frame pending) { + /* Fence 已完成,读回缓冲与查询池只属于本目标槽;不同 Scene 的映射内存 + * 下载可以在共享 Task_Resource 上并行。 */ + std::lock_guard target_lock(target_mutex_); + const std::uint64_t readback_started = trace_now_ns(); + auto collection = target(pending).collect(); + pending.observation.readback_ns = trace_now_ns() - readback_started; + pending.observation.readback_bytes = collection.pixels.size(); + pending.observation.gpu = std::move(collection.gpu_timing); + return {pending.extent, std::move(collection.pixels), std::move(pending.observation)}; +} + +void Scene_Datoviz_State::discard(Datoviz_Pending_Frame pending) { + std::lock_guard target_lock(target_mutex_); + target(pending).discard_after_completion(); +} + +void Scene_Datoviz_State::quarantine(Datoviz_Pending_Frame pending) noexcept { + quarantined_.store(true, std::memory_order_release); + try { + std::lock_guard target_lock(target_mutex_); + if (pending.target_index < targets_->values.size()) { + auto& frame_target = targets_->values[pending.target_index]; + if (frame_target && frame_target->generation() == pending.target_generation) frame_target->quarantine_after_submission(); + } + } + catch (...) {} +} + +void Scene_Datoviz_State::abandon_resources() noexcept { + quarantined_.store(true, std::memory_order_release); + static_cast(targets_.release()); + render_context_.reset(); +} + +/* 必须在无在途帧时调用;按 Runtime -> Scene -> Vulkan 池 -> GPU 上下文逆序释放。 */ +void Scene_Datoviz_State::destroy() { + if (quarantined_.load(std::memory_order_acquire)) { + abandon_resources(); + return; + } + auto context = render_context_; + { + std::lock_guard target_lock(target_mutex_); + for (auto& slot : runtime_slots_) { + if (slot.runtime != nullptr) { + dvz_drp2_runtime_destroy(slot.runtime); + slot.runtime = nullptr; + } + if (slot.emitter != nullptr) { + dvz_frame_plan_emitter_destroy(slot.emitter); + slot.emitter = nullptr; + } + } + for (auto& target : targets_->values) target.reset(); + for (auto* buffer : external_buffers_) { + if (buffer == nullptr) continue; + dvz_buffer_destroy(buffer); + dvz_buffer_free(buffer); + } + external_buffers_.clear(); + if (camera_controller_ != nullptr) { + dvz_controller_destroy(camera_controller_); + camera_controller_ = nullptr; + } + if (item_interaction_ != nullptr) { + dvz_item_interaction_destroy(item_interaction_); + item_interaction_ = nullptr; + } + if (hover_readout_ != nullptr) { + dvz_pinned_readout_destroy(hover_readout_); + hover_readout_ = nullptr; + } + if (panel_&& input_router_ != nullptr) (void)dvz_panel_connect_input(panel_, nullptr); + if (gesture_handler_ != nullptr) { + dvz_pointer_gesture_handler_destroy(gesture_handler_); + gesture_handler_ = nullptr; + } + if (input_router_ != nullptr) { + dvz_input_router_destroy(input_router_); + input_router_ = nullptr; + } + visuals_.clear(); + axes_visual_ = nullptr; + axes_text_ = nullptr; + applied_camera_.reset(); + applied_axes_.reset(); + panel_ = nullptr; + figure_ = nullptr; + if (scene_ != nullptr) { + dvz_scene_destroy(scene_); + scene_ = nullptr; + } + if (context) { + VkDevice device = dvz_device_handle(dvz_gpu_ctx_device(context->gpu_context())); + if (descriptor_pool_ != 0) { + vkDestroyDescriptorPool(device, reinterpret_cast(descriptor_pool_), nullptr); + descriptor_pool_ = 0; + } + if (command_pool_ != 0) { + vkDestroyCommandPool(device, reinterpret_cast(command_pool_), nullptr); + command_pool_ = 0; + } + } + } + render_context_.reset(); + context.reset(); +} +} // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/scene/detail/Datoviz_Frame_Target.ipp b/render_3D/render_3D/scene/detail/Datoviz_Frame_Target.ipp new file mode 100644 index 0000000..03b8249 --- /dev/null +++ b/render_3D/render_3D/scene/detail/Datoviz_Frame_Target.ipp @@ -0,0 +1,424 @@ +#pragma once +#include "Datoviz_Scene_Common.ipp" +namespace aethera::render_3d::detail { +/* + * 单个离屏帧槽拥有从 GPU 颜色附件到 CPU RGBA 读回的完整资源链。 + * 状态只在 free -> recording -> submitted -> free 间推进;Fence 由外部完成服务观察,槽内绝不等待。 + */ +struct Datoviz_Frame_Target final { + public: + struct Collection { + std::vector pixels; + std::optional gpu_timing; + }; + + Datoviz_Frame_Target(DvzGpuCtx* gpu_context, VkCommandPool command_pool, Extent extent, std::uint64_t generation) : gpu_context_(gpu_context), extent_(extent), generation_(generation) { + if (extent.empty()) throw std::invalid_argument("invalid Datoviz point frame target"); + const std::uint64_t byte_size = static_cast(extent.width) * extent.height * 4ULL; + if (byte_size > std::numeric_limits::max()) throw std::length_error("Datoviz point frame target is too large"); + byte_size_ = static_cast(byte_size); + DvzDevice* device = dvz_gpu_ctx_device(gpu_context_); + DvzVma* allocator = dvz_gpu_ctx_alloc(gpu_context_); + DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN); + if (device == nullptr || allocator == nullptr || queue == nullptr) throw std::runtime_error("Datoviz GPU context is incomplete"); + try { + image_ = allocate_wrapper(dvz_images_create_wrapper, "failed to allocate Datoviz image"); + dvz_images(device, allocator, VK_IMAGE_TYPE_2D, 1, image_); + dvz_images_format(image_, VK_FORMAT_R8G8B8A8_UNORM); + dvz_images_size(image_, extent.width, extent.height, 1); + dvz_images_tiling(image_, VK_IMAGE_TILING_OPTIMAL); + dvz_images_usage(image_, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + dvz_images_alloc_flags(image_, DVZ_ALLOC_FLAGS_NONE); + if (dvz_images_create(image_) != 0) throw std::runtime_error("failed to create Datoviz point image"); + view_ = allocate_wrapper(dvz_image_views_create_wrapper, "failed to allocate Datoviz image view"); + dvz_image_views(image_, view_); + dvz_image_views_type(view_, VK_IMAGE_VIEW_TYPE_2D); + dvz_image_views_aspect(view_, VK_IMAGE_ASPECT_COLOR_BIT); + dvz_image_views_mip(view_, 0, 1); + dvz_image_views_layers(view_, 0, 1); + if (dvz_image_views_create(view_) != 0) throw std::runtime_error("failed to create Datoviz point image view"); + commands_ = allocate_wrapper(dvz_commands_create_wrapper, "failed to allocate Datoviz commands"); + dvz_commands_pool(device, queue, command_pool, 1, commands_); + if (dvz_commands_handle(commands_) == VK_NULL_HANDLE) throw std::runtime_error("failed to create Datoviz command buffer"); + fence_ = allocate_wrapper(dvz_fence_create_wrapper, "failed to allocate Datoviz fence"); + dvz_fence(device, true, fence_); + if (dvz_fence_handle(fence_) == VK_NULL_HANDLE) throw std::runtime_error("failed to create Datoviz fence"); + submit_ = allocate_wrapper(dvz_submit_create_wrapper, "failed to allocate Datoviz submit"); + readback_ = allocate_wrapper(dvz_buffer_create_wrapper, "failed to allocate Datoviz readback"); + dvz_buffer(device, allocator, readback_); + dvz_buffer_size(readback_, byte_size_); + dvz_buffer_flags(readback_, DVZ_ALLOC_HOST_ACCESS_RANDOM | DVZ_ALLOC_MAPPED); + dvz_buffer_usage(readback_, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + if (dvz_buffer_create(readback_) != 0) throw std::runtime_error("failed to create Datoviz readback buffer"); + initialize_timestamps(device, queue); + } + catch (...) { + destroy(); + raise_context("creating Datoviz frame target", std::current_exception()); + } + } + + ~Datoviz_Frame_Target() noexcept { + try { + if (quarantined_) release_without_destroy(); + else destroy(); + } + catch (...) { + release_without_destroy(); + } + } + + [[nodiscard]] bool can_reuse_commands(std::uint64_t command_revision, bool readback) const noexcept { + return available() && recorded_ && recorded_command_revision_ == command_revision && recorded_readback_ == readback; + } + + [[nodiscard]] bool can_reuse(std::uint64_t command_revision, std::uint64_t controller_revision, bool readback) const noexcept { + return can_reuse_commands(command_revision, readback) && recorded_controller_revision_ == controller_revision; + } + + void mark_mvp_uploaded(std::uint64_t controller_revision) { + if (!available() || !recorded_) throw std::logic_error("Datoviz frame target cannot publish an MVP revision"); + recorded_controller_revision_ = controller_revision; + } + + void invalidate_recording() { + if (!available()) throw std::logic_error("Datoviz frame target cannot invalidate a busy recording"); + recorded_ = false; + } + + void reuse(std::uint64_t command_revision, std::uint64_t controller_revision, bool observe, bool readback) { + if (!can_reuse(command_revision, controller_revision, readback)) throw std::logic_error("Datoviz frame target command recording cannot be reused"); + observing_ = observe; + readback_requested_ = readback; + dvz_fence_reset(fence_); + prepared_ = true; + } + + void begin(bool observe, bool readback, std::uint64_t command_revision, std::uint64_t controller_revision) { + if (recording_ || prepared_ || in_flight_) throw std::logic_error("Datoviz frame target is not available"); + observing_ = observe; + readback_requested_ = readback; + recording_command_revision_ = command_revision; + recording_controller_revision_ = controller_revision; + /* Reset invalidates the previous recording immediately. Only + * finish_recording() may publish the new cache identity. */ + recorded_ = false; + try { + dvz_cmd_reset(commands_); + if (dvz_cmd_begin_result(commands_) != 0) throw std::runtime_error("failed to begin Datoviz command buffer"); + recording_ = true; + DvzBarriers barriers{}; + dvz_barriers(&barriers); + auto* image_barrier = dvz_barriers_image(&barriers, dvz_image_handle(image_, 0)); + if (image_barrier == nullptr) throw std::runtime_error("failed to allocate Datoviz image barrier"); + if (completed_layout_ == VK_IMAGE_LAYOUT_UNDEFINED) { + dvz_barrier_image_stage(image_barrier, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + dvz_barrier_image_access(image_barrier, 0, VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + } + else if (completed_layout_ == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) { + dvz_barrier_image_stage(image_barrier, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + dvz_barrier_image_access(image_barrier, VK_ACCESS_2_TRANSFER_READ_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + } + else { + dvz_barrier_image_stage(image_barrier, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + dvz_barrier_image_access(image_barrier, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + } + dvz_barrier_image_layout(image_barrier, completed_layout_, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + dvz_barrier_image_aspect(image_barrier, VK_IMAGE_ASPECT_COLOR_BIT); + dvz_barrier_image_mip(image_barrier, 0, 1); + dvz_barrier_image_layers(image_barrier, 0, 1); + dvz_cmd_barriers(commands_, &barriers); + if (timestamps_supported_) { + const VkCommandBuffer command_buffer = dvz_commands_handle(commands_); + vkCmdResetQueryPool(command_buffer, query_pool_, 0, 4); + vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, query_pool_, 0); + } + } + catch (...) { + abort(); + throw; + } + } + + [[nodiscard]] DvzStreamFrame stream_frame() const { + DvzStreamFrame frame{}; + frame.image = dvz_image_handle(image_, 0); + frame.command_buffer = dvz_commands_handle(commands_); + frame.image_view = dvz_image_views_handle(view_, 0); + frame.extent = {extent_.width, extent_.height}; + frame.color_format = VK_FORMAT_R8G8B8A8_UNORM; + frame.image_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + frame.usage = DVZ_STREAM_FRAME_USAGE_RENDER_TARGET | DVZ_STREAM_FRAME_USAGE_COPY_SRC; + frame.command_buffer_recording = recording_; + frame.image_borrowed = true; + frame.image_view_borrowed = true; + frame.command_buffer_borrowed = true; + frame.handles_dirty = true; + frame.resource_generation = generation_; + frame.image_valid = true; + frame.memory_fd = -1; + frame.wait_semaphore_fd = -1; + return frame; + } + + void finish_recording() { + if (!recording_) throw std::logic_error("Datoviz frame target is not recording"); + const VkCommandBuffer command_buffer = dvz_commands_handle(commands_); + if (timestamps_supported_) { + vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, query_pool_, 1); + } + if (readback_requested_) { + DvzBarriers image_barriers{}; + dvz_barriers(&image_barriers); + auto* image_barrier = dvz_barriers_image(&image_barriers, dvz_image_handle(image_, 0)); + dvz_barrier_image_stage(image_barrier, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT); + dvz_barrier_image_access(image_barrier, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); + dvz_barrier_image_layout(image_barrier, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + dvz_barrier_image_aspect(image_barrier, VK_IMAGE_ASPECT_COLOR_BIT); + dvz_barrier_image_mip(image_barrier, 0, 1); + dvz_barrier_image_layers(image_barrier, 0, 1); + dvz_cmd_barriers(commands_, &image_barriers); + if (timestamps_supported_) { + vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, query_pool_, 2); + } + DvzImageRegion region{}; + dvz_image_region(®ion); + dvz_image_region_extent(®ion, extent_.width, extent_.height, 1); + dvz_cmd_copy_image_to_buffer(commands_, dvz_image_handle(image_, 0), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, ®ion, dvz_buffer_handle(readback_), 0); + DvzBarriers buffer_barriers{}; + dvz_barriers(&buffer_barriers); + auto* buffer_barrier = dvz_barriers_buffer(&buffer_barriers, dvz_buffer_handle(readback_), 0, byte_size_); + dvz_barrier_buffer_stage(buffer_barrier, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_PIPELINE_STAGE_2_HOST_BIT); + dvz_barrier_buffer_access(buffer_barrier, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_ACCESS_2_HOST_READ_BIT); + dvz_cmd_barriers(commands_, &buffer_barriers); + } + else if (timestamps_supported_) { + vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, query_pool_, 2); + } + if (timestamps_supported_) { + vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, query_pool_, 3); + } + if (dvz_cmd_end_result(commands_) != 0) throw std::runtime_error("failed to end Datoviz command buffer"); + recording_ = false; + dvz_fence_reset(fence_); + dvz_submit(submit_); + dvz_submit_command(submit_, dvz_commands_handle(commands_)); + recorded_command_revision_ = recording_command_revision_; + recorded_controller_revision_ = recording_controller_revision_; + recorded_readback_ = readback_requested_; + recorded_ = true; + prepared_ = true; + } + + void submit() { + if (!prepared_ || in_flight_) throw std::logic_error("Datoviz frame target has no prepared submission"); + DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN); + if (dvz_submit_send(submit_, dvz_queue_handle(queue), dvz_fence_handle(fence_)) != VK_SUCCESS) throw std::runtime_error("failed to submit Datoviz point frame"); + prepared_ = false; + in_flight_ = true; + completed_layout_ = readback_requested_ + ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } + + [[nodiscard]] Collection collect() { + if (!in_flight_) throw std::logic_error("Datoviz frame target has no pending frame"); + Collection result; + try { + if (readback_requested_) { + result.pixels.resize(static_cast(byte_size_)); + dvz_buffer_download(readback_, 0, byte_size_, result.pixels.data()); + } + result.gpu_timing = collect_gpu_timing(); + } + catch (...) { + in_flight_ = false; + observing_ = false; + readback_requested_ = false; + raise_context("submitting Datoviz frame target", std::current_exception()); + } + in_flight_ = false; + observing_ = false; + readback_requested_ = false; + return result; + } + + void discard_after_completion() { + if (prepared_) { + prepared_ = false; + observing_ = false; + readback_requested_ = false; + return; + } + if (!in_flight_) throw std::logic_error("Datoviz frame target has no pending frame"); + in_flight_ = false; + observing_ = false; + readback_requested_ = false; + } + + [[nodiscard]] VkDevice device() const { + return dvz_device_handle(dvz_gpu_ctx_device(gpu_context_)); + } + + [[nodiscard]] VkFence fence() const { + return dvz_fence_handle(fence_); + } + + [[nodiscard]] std::uint64_t generation() const noexcept { + return generation_; + } + + void quarantine_after_submission() noexcept { + recording_ = false; + prepared_ = false; + in_flight_ = false; + observing_ = false; + readback_requested_ = false; + recorded_ = false; + quarantined_ = true; + } + + [[nodiscard]] Extent extent() const noexcept { + return extent_; + } + + [[nodiscard]] bool available() const noexcept { + return !quarantined_ && !recording_ && !prepared_ && !in_flight_; + } + + void abort() noexcept { + if (recording_ && commands_ != nullptr) dvz_cmd_reset(commands_); + recording_ = false; + prepared_ = false; + observing_ = false; + readback_requested_ = false; + } + private: + void release_without_destroy() noexcept { + query_pool_ = VK_NULL_HANDLE; + readback_ = nullptr; + fence_ = nullptr; + submit_ = nullptr; + commands_ = nullptr; + view_ = nullptr; + image_ = nullptr; + } + + void initialize_timestamps(DvzDevice* device, DvzQueue* queue) noexcept { + if (vkGetPhysicalDeviceQueueFamilyProperties == nullptr || vkGetPhysicalDeviceProperties == nullptr || vkCreateQueryPool == nullptr || vkCmdResetQueryPool == nullptr || vkCmdWriteTimestamp == nullptr || vkGetQueryPoolResults == nullptr) return; + const VkPhysicalDevice physical = dvz_device_physical_device(device); + const VkDevice logical = dvz_device_handle(device); + if (physical == VK_NULL_HANDLE || logical == VK_NULL_HANDLE) return; + std::uint32_t family_count{}; + vkGetPhysicalDeviceQueueFamilyProperties(physical, &family_count, nullptr); + if (family_count == 0) return; + std::vector families(family_count); + vkGetPhysicalDeviceQueueFamilyProperties(physical, &family_count, families.data()); + const std::uint32_t family = dvz_queue_family(queue); + if (family >= family_count || families[family].timestampValidBits == 0) return; + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(physical, &properties); + VkQueryPoolCreateInfo configuration{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + configuration.queryType = VK_QUERY_TYPE_TIMESTAMP; + configuration.queryCount = 4; + if (vkCreateQueryPool(logical, &configuration, nullptr, &query_pool_) != VK_SUCCESS) { + query_pool_ = VK_NULL_HANDLE; + return; + } + timestamp_period_ns_ = properties.limits.timestampPeriod; + timestamp_valid_bits_ = families[family].timestampValidBits; + timestamps_supported_ = timestamp_period_ns_ > 0.0F; + } + + [[nodiscard]] std::optional collect_gpu_timing() const noexcept { + if (!observing_ || !timestamps_supported_ || query_pool_ == VK_NULL_HANDLE) return std::nullopt; + const VkDevice device = dvz_device_handle(dvz_gpu_ctx_device(gpu_context_)); + std::array timestamps{}; + if (vkGetQueryPoolResults(device, query_pool_, 0, static_cast(timestamps.size()), sizeof(timestamps), timestamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT) != VK_SUCCESS) return std::nullopt; + const auto elapsed = [this](std::uint64_t begin, std::uint64_t end) noexcept { + std::uint64_t ticks = end - begin; + if (timestamp_valid_bits_ < 64) { + const std::uint64_t mask = (std::uint64_t{1} << timestamp_valid_bits_) - 1; + ticks &= mask; + } + const long double nanoseconds = static_cast(ticks) * timestamp_period_ns_; + return nanoseconds >= static_cast(std::numeric_limits::max()) + ? std::numeric_limits::max() + : static_cast(nanoseconds); + }; + auto timing = Datoviz_Gpu_Timing{elapsed(timestamps[0], timestamps[1]), elapsed(timestamps[1], timestamps[2]), elapsed(timestamps[2], timestamps[3]), elapsed(timestamps[0], timestamps[3])}; + if (!readback_requested_) timing.copy_ns = 0; + return timing; + } + + void destroy() { + if (recording_ || prepared_ || in_flight_) throw std::logic_error("Datoviz frame target is busy during destruction"); + if (query_pool_ != VK_NULL_HANDLE && gpu_context_ != nullptr) { + vkDestroyQueryPool(dvz_device_handle(dvz_gpu_ctx_device(gpu_context_)), query_pool_, nullptr); + query_pool_ = VK_NULL_HANDLE; + } + if (readback_ != nullptr) { + dvz_buffer_destroy(readback_); + dvz_buffer_free(readback_); + readback_ = nullptr; + } + if (fence_ != nullptr) { + dvz_fence_destroy(fence_); + dvz_fence_free(fence_); + fence_ = nullptr; + } + if (submit_ != nullptr) { + dvz_submit_free(submit_); + submit_ = nullptr; + } + if (commands_ != nullptr) { + dvz_commands_destroy(commands_); + dvz_commands_free(commands_); + commands_ = nullptr; + } + if (view_ != nullptr) { + dvz_image_views_destroy(view_); + dvz_image_views_free(view_); + view_ = nullptr; + } + if (image_ != nullptr) { + dvz_images_destroy(image_); + dvz_images_free(image_); + image_ = nullptr; + } + } + + DvzGpuCtx* gpu_context_; /* Required non-owning borrow; the process Datoviz render context outlives every target. */ + Extent extent_{}; + std::uint64_t generation_{}; + DvzSize byte_size_{}; + DvzImages* image_{}; /* Nullable owned wrapper; destroy() releases it unless a submitted target is quarantined. */ + DvzImageViews* view_{}; /* Nullable owned wrapper; destroy() releases it unless a submitted target is quarantined. */ + DvzCommands* commands_{}; /* Nullable owned wrapper; destroy() releases it unless a submitted target is quarantined. */ + DvzFence* fence_{}; /* Nullable owned wrapper; destroy() releases it unless a submitted target is quarantined. */ + DvzSubmit* submit_{}; /* Nullable owned wrapper; destroy() releases it unless a submitted target is quarantined. */ + DvzBuffer* readback_{}; /* Nullable owned wrapper; destroy() releases it unless a submitted target is quarantined. */ + VkQueryPool query_pool_{VK_NULL_HANDLE}; + VkImageLayout completed_layout_{VK_IMAGE_LAYOUT_UNDEFINED}; + float timestamp_period_ns_{}; + std::uint32_t timestamp_valid_bits_{}; + bool recording_{}; + bool prepared_{}; + bool in_flight_{}; + bool observing_{}; + bool readback_requested_{}; + bool timestamps_supported_{}; + std::uint64_t recording_command_revision_{}; + std::uint64_t recorded_command_revision_{}; + std::uint64_t recording_controller_revision_{}; + std::uint64_t recorded_controller_revision_{}; + bool recorded_{}; + bool recorded_readback_{}; + bool quarantined_{}; +}; + +struct Datoviz_Frame_Target_Set { + static constexpr std::size_t count = 3; + std::array, count> values{}; /* 固定三槽,槽地址在后端生命周期内稳定。 */ +}; +} // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/scene/detail/Datoviz_Interaction.ipp b/render_3D/render_3D/scene/detail/Datoviz_Interaction.ipp new file mode 100644 index 0000000..030ba9c --- /dev/null +++ b/render_3D/render_3D/scene/detail/Datoviz_Interaction.ipp @@ -0,0 +1,59 @@ +#pragma once +#include "Datoviz_Scene_Common.ipp" +namespace aethera::render_3d::detail { +void Scene_Datoviz_State::mark_controller_input_applied() noexcept { + input_changed_ = true; + ++controller_revision_; + if (controller_revision_ == 0) controller_revision_ = 1; +} + +void Scene_Datoviz_State::dispatch_pointer(::aethera::Event_Type event, float x, float y, ::aethera::Mouse_Button mouse_button, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport, std::uint64_t occurred_at_ns) { + if (applied_camera_&& applied_camera_->controller == Camera_Controller::turntable) + { + if (mouse_button == ::aethera::Mouse_Button::left && !applied_camera_->turntable_control.rotate_enabled) return; + if ((mouse_button == ::aethera::Mouse_Button::middle || mouse_button == ::aethera::Mouse_Button::right) && !applied_camera_->turntable_control.pan_enabled) return; + } + const float width = static_cast(viewport.width); + const float height = static_cast(viewport.height); + const DvzPointerEventType type = event == ::aethera::Event_Type::pointer_move + ? DVZ_POINTER_EVENT_MOVE + : event == ::aethera::Event_Type::pointer_press + ? DVZ_POINTER_EVENT_PRESS + : DVZ_POINTER_EVENT_RELEASE; + dvz_pointer_emit_position(input_router_, type, x, y, width, height, button(mouse_button), modifiers(keyboard_modifiers), 1.0F, occurred_at_ns, nullptr); + mark_controller_input_applied(); +} + +void Scene_Datoviz_State::dispatch_wheel(float x, float y, float delta_x, float delta_y, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport, std::uint64_t occurred_at_ns) { + if (applied_camera_&& applied_camera_->controller == Camera_Controller::turntable && !applied_camera_->turntable_control.zoom_enabled) return; + dvz_pointer_emit_wheel(input_router_, x, y, static_cast(viewport.width), static_cast(viewport.height), delta_x, delta_y, modifiers(keyboard_modifiers), 1.0F, occurred_at_ns, nullptr); + mark_controller_input_applied(); +} + +void Scene_Datoviz_State::dispatch_key(const ::aethera::Key_Event& event) { + if (event.key == ::aethera::Key::home && event.type == ::aethera::Event_Type::key_press) { + DvzResult reset = DVZ_ERROR; + switch (dvz_controller_type(camera_controller_)) { + case DVZ_CONTROLLER_TYPE_TURNTABLE: reset = dvz_turntable_reset(dvz_controller_turntable(camera_controller_)); + break; + case DVZ_CONTROLLER_TYPE_ARCBALL: reset = dvz_arcball_reset(dvz_controller_arcball(camera_controller_)); + break; + case DVZ_CONTROLLER_TYPE_FLY: reset = dvz_fly_reset(dvz_controller_fly(camera_controller_)); + break; + case DVZ_CONTROLLER_TYPE_PANZOOM: reset = dvz_panzoom_reset(dvz_controller_panzoom(camera_controller_)); + break; + default: break; + } + if (reset != DVZ_OK) throw std::runtime_error("failed to reset Datoviz camera controller"); + mark_controller_input_applied(); + return; + } + const DvzKeyboardEventType type = event.type == ::aethera::Event_Type::key_release + ? DVZ_KEYBOARD_EVENT_RELEASE + : event.auto_repeat + ? DVZ_KEYBOARD_EVENT_REPEAT + : DVZ_KEYBOARD_EVENT_PRESS; + dvz_keyboard_emit(input_router_, type, key_code(event.key, event.native_key), modifiers(event.modifiers), nullptr); + mark_controller_input_applied(); +} +} // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp b/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp new file mode 100644 index 0000000..9069415 --- /dev/null +++ b/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp @@ -0,0 +1,390 @@ +#pragma once +#include "../Render_Scene_3D.hpp" +#include "../../detail/Exception.hpp" +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace aethera::render_3d::detail { +namespace { +constexpr std::uint64_t color_target_id = 0x5256504f494e54ULL; + +DvzCapabilitySnapshot offscreen_capabilities() { + auto capabilities = dvz_capability_snapshot(); + capabilities.supports_color_blending = true; + return capabilities; +} + +std::uint64_t trace_now_ns() noexcept { + return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); +} + +std::uint64_t trace_thread_cpu_ns() noexcept { +#if defined(_WIN32) + FILETIME created{}, exited{}, kernel{}, user{}; if (!GetThreadTimes(GetCurrentThread(), &created, &exited, &kernel, &user)) return 0; const auto ticks = [](FILETIME value) noexcept { + ULARGE_INTEGER result{}; + result.LowPart = value.dwLowDateTime; + result.HighPart = value.dwHighDateTime; + return result.QuadPart; + }; return (ticks(kernel) + ticks(user)) * 100ULL; +#elif defined(CLOCK_THREAD_CPUTIME_ID) + timespec value{}; if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &value) != 0) return 0; return static_cast(value.tv_sec) * 1'000'000'000ULL + static_cast(value.tv_nsec); +#else + return 0; +#endif +} + +template +Resource* allocate_wrapper(Allocate allocate, const char* message) { + Resource* resource = allocate(); + if (resource == nullptr) throw std::runtime_error(message); + return resource; +} + +int modifiers(::aethera::Keyboard_Modifier value) { + const auto bits = static_cast(value); + int result = DVZ_KEY_MODIFIER_NONE; + if ((bits & static_cast(::aethera::Keyboard_Modifier::shift)) != 0) result |= DVZ_KEY_MODIFIER_SHIFT; + if ((bits & static_cast(::aethera::Keyboard_Modifier::control)) != 0) result |= DVZ_KEY_MODIFIER_CONTROL; + if ((bits & static_cast(::aethera::Keyboard_Modifier::alt)) != 0) result |= DVZ_KEY_MODIFIER_ALT; + if ((bits & static_cast(::aethera::Keyboard_Modifier::meta)) != 0) result |= DVZ_KEY_MODIFIER_SUPER; + return result; +} + +DvzPointerButton button(::aethera::Mouse_Button value) { + switch (value) { + case ::aethera::Mouse_Button::left: return DVZ_POINTER_BUTTON_LEFT; + case ::aethera::Mouse_Button::middle: return DVZ_POINTER_BUTTON_MIDDLE; + case ::aethera::Mouse_Button::right: return DVZ_POINTER_BUTTON_RIGHT; + case ::aethera::Mouse_Button::none: return DVZ_POINTER_BUTTON_NONE; + } + return DVZ_POINTER_BUTTON_NONE; +} + +DvzKeyCode key_code(::aethera::Key key, std::uint32_t native_key) { + switch (key) { + case ::aethera::Key::escape: return DVZ_KEY_ESCAPE; + case ::aethera::Key::enter: return DVZ_KEY_ENTER; + case ::aethera::Key::space: return DVZ_KEY_SPACE; + case ::aethera::Key::delete_key: return DVZ_KEY_DELETE; + case ::aethera::Key::backspace: return DVZ_KEY_BACKSPACE; + case ::aethera::Key::left: return DVZ_KEY_LEFT; + case ::aethera::Key::right: return DVZ_KEY_RIGHT; + case ::aethera::Key::up: return DVZ_KEY_UP; + case ::aethera::Key::down: return DVZ_KEY_DOWN; + case ::aethera::Key::home: return DVZ_KEY_HOME; + case ::aethera::Key::unknown: break; + } + return native_key <= static_cast(DVZ_KEY_LAST) + ? static_cast(native_key) + : DVZ_KEY_UNKNOWN; +} + +float axis_position(const plot::Axis_Descriptor& axis, double coordinate) { + const auto origin = axis.range.origin; + const auto target = axis.range.target; + if (!std::isfinite(origin) || !std::isfinite(target) || origin == target) throw std::invalid_argument("3D axis range must be finite and non-empty"); + double ratio{}; + if (axis.scale == plot::Axis_Scale::logarithmic) { + if (!(origin > 0.0) || !(target > 0.0) || !(coordinate > 0.0)) throw std::invalid_argument("logarithmic 3D axis coordinates must be positive"); + ratio = (std::log10(coordinate) - std::log10(origin)) / (std::log10(target) - std::log10(origin)); + } + else { + ratio = (coordinate - origin) / (target - origin); + } + return static_cast(-1.0 + 2.0 * ratio); +} + +std::string axis_title(const plot::Axis_Descriptor& axis) { + if (axis.label.empty()) return axis.unit; + if (axis.unit.empty()) return axis.label; + return axis.label + " (" + axis.unit + ")"; +} + +std::string artifact_command_excerpt(const std::string& json, std::uint32_t command_index) { + constexpr std::string_view marker{"{ \"cmd\":"}; + const auto first_index = command_index > 12 + ? command_index - 12 + : 0; + std::size_t position{}; + std::size_t first_position{}; + for (std::uint32_t index = 0; index <= command_index; ++index) { + position = json.find(marker, position); + if (position == std::string::npos) return {}; + if (index == first_index) first_position = position; + if (index != command_index) position += marker.size(); + } + const auto end = json.find('\n', position); + return json.substr(first_position, end == std::string::npos + ? 2048 + : end - first_position); +} +} // namespace +struct Datoviz_Render_Context final : std::enable_shared_from_this { + public: + [[nodiscard]] static std::shared_ptr acquire(std::uint32_t gpu_index, bool validation_enabled); + + ~Datoviz_Render_Context() { + if (gpu_context_ != nullptr) dvz_gpu_ctx_destroy(gpu_context_); + } + + [[nodiscard]] DvzGpuCtx* gpu_context() const noexcept { + return gpu_context_; + } + + void enqueue_submission(std::function command) { + if (!command) throw std::invalid_argument("empty Datoviz queue command"); + if (!submissions_.enqueue(std::move(command))) throw std::bad_alloc{}; + submission_generation_.fetch_add(1, std::memory_order_release); + try { + arm_submission_drain(); + } + catch (...) { + /* 命令已经进入唯一队列,不能让调用方按“未提交”路径释放其资源。 */ + std::terminate(); + } + } + + void route_immediate_submissions(DvzDrp2RuntimeConfig& configuration) noexcept { + dvz_drp2_runtime_vklite_submit_strategy(&configuration, &Datoviz_Render_Context::submit_immediate, this); + } + private: + struct Registry_Entry; + struct Registry; + static Registry registry_; + + struct Immediate_Submission final { + explicit Immediate_Submission(VkDevice device_value) noexcept : device(device_value) {} + + ~Immediate_Submission() { + if (fence != VK_NULL_HANDLE) vkDestroyFence(device, fence, nullptr); + } + + VkDevice device{VK_NULL_HANDLE}; /* 非拥有;Datoviz_Render_Context 保证其生命周期。 */ + VkFence fence{VK_NULL_HANDLE}; /* 本次即时提交独占所有权。 */ + std::array commands{}; + VkSubmitInfo2 submit{}; + std::atomic_bool submitted{}; + std::atomic_int32_t submission_result{VK_NOT_READY}; + std::atomic_int32_t completion_result{VK_NOT_READY}; + std::uint64_t queue_started_ns{}; + std::uint64_t queue_finished_ns{}; + }; + + static std::int32_t submit_immediate(void* user_data, VkQueue queue, const VkSubmitInfo2* submit_info, DvzCommandSubmitTiming* timing) noexcept { + auto* context = static_cast(user_data); + if (context == nullptr || queue == VK_NULL_HANDLE || submit_info == nullptr) return VK_ERROR_INITIALIZATION_FAILED; + try { + return context->submit_immediate(queue, *submit_info, timing); + } + catch (const std::bad_alloc&) { + return VK_ERROR_OUT_OF_HOST_MEMORY; + } + catch (...) { + return VK_ERROR_INITIALIZATION_FAILED; + } + } + + std::int32_t submit_immediate(VkQueue queue, const VkSubmitInfo2& source, DvzCommandSubmitTiming* timing) { + if (source.commandBufferInfoCount == 0 || source.commandBufferInfoCount > DVZ_MAX_SWAPCHAIN_IMAGES || source.waitSemaphoreInfoCount != 0 || source.signalSemaphoreInfoCount != 0) return VK_ERROR_INITIALIZATION_FAILED; + auto submission = std::make_shared(dvz_device_handle(dvz_gpu_ctx_device(gpu_context_))); + std::copy_n(source.pCommandBufferInfos, source.commandBufferInfoCount, submission->commands.begin()); + submission->submit = source; + submission->submit.pCommandBufferInfos = submission->commands.data(); + const VkFenceCreateInfo fence_info{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + const auto fence_started = trace_now_ns(); + const VkResult fence_result = vkCreateFence(submission->device, &fence_info, nullptr, &submission->fence); + if (timing != nullptr) timing->fence_create_ns = trace_now_ns() - fence_started; + if (fence_result != VK_SUCCESS) return fence_result; + const auto enqueue_started = trace_now_ns(); + try { + enqueue_submission([submission, queue]() noexcept { + submission->queue_started_ns = trace_now_ns(); + const VkResult result = vkQueueSubmit2(queue, 1, &submission->submit, submission->fence); + submission->queue_finished_ns = trace_now_ns(); + submission->submission_result.store(result, std::memory_order_relaxed); + submission->submitted.store(true, std::memory_order_release); + }); + } + catch (...) { + return VK_ERROR_OUT_OF_HOST_MEMORY; + } + const auto enqueue_finished = trace_now_ns(); + if (timing != nullptr) timing->enqueue_ns = enqueue_finished - enqueue_started; + try { + /* + * 一个协作等待同时覆盖“排到唯一 VkQueue 提交入口”和“Fence 完成”。 + * corun_until 会让当前 Worker 执行其它就绪节点,观察器把整段标为 + * cooperative wait;命令与 Fence 在返回前仍由 submission 独占。 + */ + submission_taskflow_->corun_until([submission]() noexcept { + if (!submission->submitted.load(std::memory_order_acquire)) return false; + const auto submitted = static_cast(submission->submission_result.load(std::memory_order_relaxed)); + if (submitted != VK_SUCCESS) { + submission->completion_result.store(submitted, std::memory_order_relaxed); + return true; + } + const VkResult completed = vkGetFenceStatus(submission->device, submission->fence); + if (completed == VK_NOT_READY) return false; + submission->completion_result.store(completed, std::memory_order_relaxed); + return true; + }); + } + catch (...) { + /* 已入队命令的栈外资源不能提前释放;违反 Worker 前置条件时立即终止。 */ + std::terminate(); + } + if (timing != nullptr) { + const auto queue_started = submission->queue_started_ns; + const auto queue_finished = submission->queue_finished_ns; + timing->queue_wait_ns = queue_started > enqueue_finished + ? queue_started - enqueue_finished + : 0; + timing->queue_submit_ns = queue_finished > queue_started + ? queue_finished - queue_started + : 0; + const auto completion_observed = trace_now_ns(); + timing->fence_wait_ns = queue_finished != 0 && completion_observed > queue_finished + ? completion_observed - queue_finished + : 0; + } + return submission->completion_result.load(std::memory_order_relaxed); + } + + Datoviz_Render_Context(std::uint32_t gpu_index, bool validation_enabled) { + DvzGpuCtxConfig configuration = dvz_gpu_ctx_config(); + dvz_gpu_ctx_config_validation(&configuration, validation_enabled); + dvz_gpu_ctx_config_gpu(&configuration, gpu_index); + dvz_gpu_ctx_config_enable_canvas_extensions(&configuration, false); + gpu_context_ = dvz_gpu_ctx(&configuration); + if (gpu_context_ == nullptr) throw std::runtime_error("failed to create Scene Datoviz GPU context"); + } + + void arm_submission_drain() { + if (submission_drain_active_.exchange(true, std::memory_order_acq_rel)) return; + auto lifetime = shared_from_this(); + submission_taskflow_ = task_flow::make_task_graph("datoviz.queue.submit"); + auto task = submission_taskflow_->add("drain", [lifetime] { + lifetime->drain_submissions(); + }); + if (!task) throw std::logic_error("failed to create Datoviz submission drain task"); + const auto submitted = submission_taskflow_->run([lifetime = std::move(lifetime)](std::exception_ptr failure) { + if (failure) Failure_Policy::handle_unknown_failure(std::move(failure)); + }); + if (submitted != Run_Taskflow_Result::submitted) throw std::runtime_error("Datoviz submission task runtime is unavailable"); + } + + void drain_submissions() noexcept { + const auto observed = submission_generation_.load(std::memory_order_acquire); + std::function < void() > command; + while (submissions_.try_dequeue(command)) { + try { + command(); + } + catch (...) {} + command = {}; + } + submission_drain_active_.store(false, std::memory_order_release); + if (submission_generation_.load(std::memory_order_acquire) != observed) arm_submission_drain(); + } + + DvzGpuCtx* gpu_context_{}; /* 同 GPU 配置的 Scene 共享的进程级 Device 与分配器。 */ + moodycamel::ConcurrentQueue> submissions_{}; + std::atomic_uint64_t submission_generation_{}; + std::atomic_bool submission_drain_active_{}; + proxy submission_taskflow_{}; /* 拥有当前唯一异步提交队列任务图。 */ +}; + +/* + * Sole process authority for a Datoviz GPU configuration. Entries and their + * contexts are immutable after publication, so readers neither lock nor wait. + */ +struct Datoviz_Render_Context::Registry_Entry final { + Registry_Entry(std::uint32_t gpu_index_value, bool validation_value, std::shared_ptr context_value) noexcept : gpu_index(gpu_index_value), validation_enabled(validation_value), context(std::move(context_value)) {} + + std::uint32_t gpu_index{}; + bool validation_enabled{}; + std::shared_ptr context{}; + Registry_Entry* next{}; /* Nullable, non-owning link; Registry::head owns every published entry. */ +}; + +struct Datoviz_Render_Context::Registry final { + Registry() = default; + + ~Registry() { + Registry_Entry* entry = head.exchange(nullptr, std::memory_order_acq_rel); + while (entry != nullptr) { + Registry_Entry* next = entry->next; + delete entry; + entry = next; + } + } + + Registry(const Registry&) = delete; + Registry& operator=(const Registry&) = delete; + + [[nodiscard]] std::shared_ptr acquire(std::uint32_t gpu_index, bool validation_enabled) { + const auto find = [gpu_index, validation_enabled](Registry_Entry* first) noexcept -> Registry_Entry* { + for (auto* entry = first; entry != nullptr; entry = entry->next) { + if (entry->gpu_index == gpu_index && entry->validation_enabled == validation_enabled) return entry; + } + return nullptr; + }; + if (auto* entry = find(head.load(std::memory_order_acquire))) return entry->context; + auto context = std::shared_ptr(new Datoviz_Render_Context(gpu_index, validation_enabled)); + auto candidate = std::make_unique(gpu_index, validation_enabled, context); + for (;;) { + Registry_Entry* first = head.load(std::memory_order_acquire); + if (auto* entry = find(first)) return entry->context; + candidate->next = first; + Registry_Entry* published = candidate.get(); + if (head.compare_exchange_weak(first, published, std::memory_order_release, std::memory_order_acquire)) { + static_cast(candidate.release()); + return context; + } + } + } + + std::atomic head{}; +}; + +inline Datoviz_Render_Context::Registry Datoviz_Render_Context::registry_{}; + +std::shared_ptr Datoviz_Render_Context::acquire(std::uint32_t gpu_index, bool validation_enabled) { + return registry_.acquire(gpu_index, validation_enabled); +} +} // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/scene/detail/Datoviz_Scene_Resources.ipp b/render_3D/render_3D/scene/detail/Datoviz_Scene_Resources.ipp new file mode 100644 index 0000000..251dfb7 --- /dev/null +++ b/render_3D/render_3D/scene/detail/Datoviz_Scene_Resources.ipp @@ -0,0 +1,319 @@ +#pragma once +#include "Datoviz_Frame_Target.ipp" +namespace aethera::render_3d::detail { +Scene_Datoviz_State::Scene_Datoviz_State() : targets_(std::make_unique()) {} + +Scene_Datoviz_State::~Scene_Datoviz_State() { + try { + destroy(); + } + catch (...) { + abandon_resources(); + } +} + +void Scene_Datoviz_State::initialize(std::uint32_t gpu_index, bool validation_enabled, const std::vector& visuals, const Scene_3D_Parameters& initial_scene) { + try { + render_context_ = Datoviz_Render_Context::acquire(gpu_index, validation_enabled); + const auto gpu_context = render_context_->gpu_context(); + DvzDevice* device = dvz_gpu_ctx_device(gpu_context); + DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context, DVZ_QUEUE_MAIN); + VkDevice vk_device = dvz_device_handle(device); + VkCommandPoolCreateInfo command_pool_info{.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = dvz_queue_family(queue)}; + VkCommandPool command_pool{VK_NULL_HANDLE}; + if (vkCreateCommandPool(vk_device, &command_pool_info, nullptr, &command_pool) != VK_SUCCESS) throw std::runtime_error("failed to create Scene command pool"); + command_pool_ = reinterpret_cast(command_pool); + std::array pool_sizes{{{VK_DESCRIPTOR_TYPE_SAMPLER, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, DVZ_MAX_DESCRIPTOR_SETS}, {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, DVZ_MAX_DESCRIPTOR_SETS}}}; + VkDescriptorPoolCreateInfo descriptor_pool_info{.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, .maxSets = DVZ_MAX_DESCRIPTOR_SETS * static_cast(pool_sizes.size()), .poolSizeCount = static_cast(pool_sizes.size()), .pPoolSizes = pool_sizes.data()}; + VkDescriptorPool descriptor_pool{VK_NULL_HANDLE}; + if (vkCreateDescriptorPool(vk_device, &descriptor_pool_info, nullptr, &descriptor_pool) != VK_SUCCESS) throw std::runtime_error("failed to create Scene descriptor pool"); + descriptor_pool_ = reinterpret_cast(descriptor_pool); + DvzDrp2RuntimeConfig runtime_configuration = dvz_drp2_runtime_vklite_config(device, dvz_gpu_ctx_alloc(gpu_context)); + dvz_drp2_runtime_vklite_pools(&runtime_configuration, command_pool_, descriptor_pool_); + render_context_->route_immediate_submissions(runtime_configuration); + for (auto& slot : runtime_slots_) { + slot.runtime = dvz_drp2_runtime_vklite(&runtime_configuration); + slot.emitter = dvz_frame_plan_emitter(); + if (slot.runtime == nullptr || slot.emitter == nullptr) throw std::runtime_error("failed to create a Datoviz frame-target runtime"); + } + create_scene(visuals, initial_scene); + } + catch (...) { + const auto failure = std::current_exception(); + try { + destroy(); + } + catch (...) {} + raise_context("creating Datoviz backend", failure); + } +} + +void Scene_Datoviz_State::create_scene(const std::vector& registrations, const Scene_3D_Parameters& initial_scene) { + if (registrations.empty()) throw std::invalid_argument("Datoviz backend requires at least one Visual registration"); + scene_ = dvz_scene(); + if (scene_ == nullptr) throw std::runtime_error("failed to create Datoviz scene"); + const auto capabilities = offscreen_capabilities(); + if (dvz_scene_set_capabilities(scene_, &capabilities) != DVZ_OK) throw std::runtime_error("failed to configure Datoviz scene capabilities"); + auto* figure = dvz_figure(scene_, initial_scene.viewport.width, initial_scene.viewport.height, 0); + if (figure == nullptr) throw std::runtime_error("failed to create Datoviz figure"); + auto* panel = dvz_panel_full(figure); + if (panel == nullptr) throw std::runtime_error("failed to create Datoviz panel"); + figure_ = figure; + panel_ = panel; + bool wants_item_interaction{}; + visuals_.reserve(registrations.size()); + for (const auto& registration : registrations) { + if (registration.identity == 0 || std::ranges::any_of(visuals_, [&](const Datoviz_Visual_Instance& value) { + return value.identity == registration.identity; + })) + throw std::invalid_argument("Datoviz backend received duplicate Visual identity"); + const auto native = registration.operations->create(scene_, panel); + auto visual = native.visual; + if (dvz_visual_set_alpha_mode(visual, DVZ_ALPHA_BLENDED) != DVZ_OK || dvz_panel_add_visual(panel, visual, nullptr) != DVZ_OK) throw std::runtime_error("failed to attach Datoviz Visual to panel"); + if (registration.operations->supports_item_interaction) { + if (dvz_visual_set_query_capabilities(visual, DVZ_QUERY_CAPABILITY_ITEM) != DVZ_OK) throw std::runtime_error("failed to enable Datoviz item queries"); + wants_item_interaction = true; + } + visuals_.push_back({registration.identity, registration.operations, visual, native.field, native.font, native.coordinate_text, native.field_extent, 0}); + } + if (wants_item_interaction) { + item_interaction_ = dvz_item_interaction(panel, nullptr); + if (item_interaction_ == nullptr) throw std::runtime_error("failed to create Datoviz item interaction"); + } + auto* axes_visual = dvz_segment(scene_, 0); + auto* axes_text = dvz_text(panel, 0); + if (axes_visual == nullptr || axes_text == nullptr || dvz_segment_set_caps(axes_visual, DVZ_SEGMENT_CAP_BUTT, DVZ_SEGMENT_CAP_BUTT) != DVZ_OK || dvz_visual_set_alpha_mode(axes_visual, DVZ_ALPHA_OPAQUE) != DVZ_OK || dvz_panel_add_visual(panel, axes_visual, nullptr) != DVZ_OK) throw std::runtime_error("failed to create Datoviz 3D axes visuals"); + axes_visual_ = axes_visual; + axes_text_ = axes_text; + DvzTextPlacement axes_placement = dvz_text_placement(); + axes_placement.mode = DVZ_TEXT_PLACEMENT_DATA; + axes_placement.anchor = DVZ_SCENE_ANCHOR_DATA; + axes_placement.depth_test = false; + DvzTextStyle axes_style = dvz_text_style(); + axes_style.renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS; + axes_style.size_px = 12.0F; + if (dvz_text_set_placement(axes_text, &axes_placement) != DVZ_OK || dvz_text_set_style(axes_text, &axes_style) != DVZ_OK) throw std::runtime_error("failed to configure Datoviz 3D axes text"); + apply_axes(initial_scene); + input_router_ = dvz_input_router(); + gesture_handler_ = input_router_ != nullptr + ? dvz_pointer_gesture_handler(input_router_) + : nullptr; + if (input_router_ == nullptr || gesture_handler_ == nullptr || dvz_panel_connect_input(panel, input_router_) != DVZ_OK) throw std::runtime_error("failed to connect Datoviz point input"); + apply_camera(initial_scene.camera); + DvzInputResizeEvent resize{initial_scene.viewport.width, initial_scene.viewport.height, initial_scene.viewport.width, initial_scene.viewport.height, 1.0F, 1.0F}; + dvz_input_emit_resize(input_router_, &resize); +} + +void Scene_Datoviz_State::apply_axes(const Scene_3D_Parameters& scene) { + const std::array descriptors{scene.x_axis, scene.y_axis, scene.z_axis}; + if (applied_axes_&& * applied_axes_ == descriptors) return; + using Position = std::array; + std::vector starts; + std::vector ends; + std::vector colors; + std::vector widths; + const DvzColor axis_color{190, 207, 226, 255}; + const DvzColor grid_color{58, 70, 86, 255}; + const DvzColor tick_color{145, 164, 188, 255}; + const auto segment = [&](Position start, Position end, DvzColor color, float width) { + starts.push_back(start); + ends.push_back(end); + colors.push_back(color); + widths.push_back(width); + }; + std::vector strings; + std::vector text_items; + const auto text = [&](std::string value, Position position, std::array offset, std::array anchor, float size, DvzColor color) { + strings.push_back(std::move(value)); + DvzTextItem item{}; + item.struct_size = sizeof(DvzTextItem); + item.position[0] = position[0]; + item.position[1] = position[1]; + item.position[2] = position[2]; + item.offset[0] = offset[0]; + item.offset[1] = offset[1]; + item.anchor[0] = anchor[0]; + item.anchor[1] = anchor[1]; + item.size_px = size; + item.color = color; + text_items.push_back(item); + }; + const auto append_axis = [&](const plot::Axis_Descriptor& axis, std::size_t dimension) { + if (!axis.visible) return; + const auto ticks = plot::axis_ticks(axis); + if (dimension == 0) segment({-1, -1, -1}, {1, -1, -1}, axis_color, 2.2F); + else if (dimension == 1) segment({-1, -1, -1}, {-1, 1, -1}, axis_color, 2.2F); + else segment({-1, -1, -1}, {-1, -1, 1}, axis_color, 2.2F); + for (const auto& tick : ticks) { + const auto position = axis_position(axis, tick.coordinate); + if (dimension == 0) { + segment({position, -1, -1}, {position, -1.055F, -1}, tick_color, 1.4F); + if (axis.grid_visible) segment({position, -1, -1}, {position, 1, -1}, grid_color, 1.0F); + if (axis.labels_visible) text(tick.label, {position, -1, -1}, {0, 12}, {.5F, 0}, 11, tick_color); + } + else if (dimension == 1) { + segment({-1, position, -1}, {-1.055F, position, -1}, tick_color, 1.4F); + if (axis.grid_visible) segment({-1, position, -1}, {1, position, -1}, grid_color, 1.0F); + if (axis.labels_visible) text(tick.label, {-1, position, -1}, {-10, 0}, {1, .5F}, 11, tick_color); + } + else { + segment({-1, -1, position}, {-1.055F, -1, position}, tick_color, 1.4F); + if (axis.grid_visible) { + segment({-1, -1, position}, {1, -1, position}, grid_color, 1.0F); + segment({-1, -1, position}, {-1, 1, position}, grid_color, 1.0F); + } + if (axis.labels_visible) text(tick.label, {-1, -1, position}, {-10, 0}, {1, .5F}, 11, tick_color); + } + } + const auto title = axis_title(axis); + if (title.empty()) return; + if (dimension == 0) text(title, {0, -1, -1}, {0, 34}, {.5F, 0}, 14, axis_color); + else if (dimension == 1) text(title, {-1, 0, -1}, {-78, 0}, {1, .5F}, 14, axis_color); + else text(title, {-1, -1, 0}, {-78, 0}, {1, .5F}, 14, axis_color); + }; + append_axis(descriptors[0], 0); + append_axis(descriptors[1], 1); + append_axis(descriptors[2], 2); + const auto segment_count = static_cast(starts.size()); + const std::array updates{{{"position_start", starts.data(), segment_count}, {"position_end", ends.data(), segment_count}, {"color", colors.data(), segment_count}, {"stroke_width_px", widths.data(), segment_count}}}; + if (dvz_visual_set_data_many(axes_visual_, updates.data(), static_cast(updates.size())) != DVZ_OK || dvz_visual_set_visible(axes_visual_, !starts.empty()) != DVZ_OK) throw std::runtime_error("failed to upload Datoviz 3D axes geometry"); + for (std::size_t index = 0; index < text_items.size(); ++index) text_items[index].string = strings[index].c_str(); + if (dvz_text_set_items(axes_text_, text_items.empty() + ? nullptr + : text_items.data(), static_cast(text_items.size())) != DVZ_OK) + throw std::runtime_error("failed to upload Datoviz 3D axes labels"); + applied_axes_ = descriptors; +} + +void Scene_Datoviz_State::apply_camera(const Camera_Descriptor& source) { + if (applied_camera_&& * applied_camera_ == source) return; + if (camera_controller_ != nullptr) { + dvz_controller_destroy(camera_controller_); + camera_controller_ = nullptr; + } + DvzCameraDesc camera = dvz_camera_desc(); + const auto assign = [](vec3 target, Spatial_Point value) { + target[0] = static_cast(value.x); + target[1] = static_cast(value.y); + target[2] = static_cast(value.z); + }; + assign(camera.view.eye, source.initial_view.eye); + assign(camera.view.target, source.initial_view.target); + assign(camera.view.up, source.initial_view.up); + camera.projection.type = source.projection == Camera_Projection::orthographic + ? DVZ_CAMERA_ORTHOGRAPHIC + : DVZ_CAMERA_PERSPECTIVE; + camera.projection.fov_y = static_cast(source.vertical_field_of_view_degrees * std::numbers::pi / 180.0); + camera.projection.near_clip = static_cast(source.near_plane); + camera.projection.far_clip = static_cast(source.far_plane); + const auto dx = source.initial_view.eye.x - source.initial_view.target.x; + const auto dy = source.initial_view.eye.y - source.initial_view.target.y; + const auto dz = source.initial_view.eye.z - source.initial_view.target.z; + const auto distance = std::sqrt(dx * dx + dy * dy + dz * dz); + camera.projection.ortho_height = static_cast(2.0 * distance * std::tan(source.vertical_field_of_view_degrees * std::numbers::pi / 360.0)); + if (dvz_panel_set_camera_desc(panel_, &camera) != DVZ_OK) throw std::runtime_error("failed to apply Datoviz Camera component"); + DvzDimMask dimensions = DVZ_DIM_MASK_XYZ; + switch (source.controller) { + case Camera_Controller::turntable: { + DvzTurntableDesc descriptor = dvz_turntable_desc(); + descriptor.initial_view = camera.view; + descriptor.yaw_speed = static_cast(source.turntable_control.yaw_speed); + descriptor.pitch_speed = static_cast(source.turntable_control.pitch_speed); + descriptor.zoom_speed = static_cast(source.turntable_control.zoom_speed); + descriptor.pan_speed = static_cast(source.turntable_control.pan_speed); + descriptor.min_pitch = static_cast(source.turntable_control.minimum_pitch); + descriptor.max_pitch = static_cast(source.turntable_control.maximum_pitch); + descriptor.min_distance = static_cast(source.turntable_control.minimum_distance); + descriptor.max_distance = static_cast(source.turntable_control.maximum_distance); + descriptor.controller_flags = DVZ_TURNTABLE_FLAGS_WRAP_YAW | DVZ_TURNTABLE_FLAGS_CLAMP_DISTANCE; + if (source.turntable_control.pan_enabled) descriptor.controller_flags |= DVZ_TURNTABLE_FLAGS_ALLOW_PAN; + if (source.turntable_control.invert_y) descriptor.controller_flags |= DVZ_TURNTABLE_FLAGS_INVERT_Y; + camera_controller_ = dvz_turntable(scene_, &descriptor); + break; + } + case Camera_Controller::arcball: { + DvzArcballDesc descriptor = dvz_arcball_desc(); + if (source.arcball_control.constrain_rotation) descriptor.controller_flags |= DVZ_ARCBALL_FLAGS_CONSTRAIN; + camera_controller_ = dvz_arcball(scene_, &descriptor); + if (camera_controller_ != nullptr && source.arcball_control.constrain_rotation) { + auto* arcball = dvz_controller_arcball(camera_controller_); + vec3 axis{static_cast(source.arcball_control.constraint_axis.x), static_cast(source.arcball_control.constraint_axis.y), static_cast(source.arcball_control.constraint_axis.z)}; + if (arcball == nullptr || dvz_arcball_constrain(arcball, axis) != DVZ_OK) throw std::runtime_error("failed to configure Datoviz Arcball constraint"); + } + break; + } + case Camera_Controller::fly: { + DvzFlyDesc descriptor = dvz_fly_desc(); + descriptor.initial_view = camera.view; + descriptor.mode = source.fly_control.mode == Camera_Fly_Mode::plane + ? DVZ_FLY_MODE_PLANE + : DVZ_FLY_MODE_FREE; + descriptor.speed = static_cast(source.fly_control.movement_speed); + descriptor.fast_multiplier = static_cast(source.fly_control.fast_multiplier); + descriptor.slow_multiplier = static_cast(source.fly_control.slow_multiplier); + descriptor.look_speed = static_cast(source.fly_control.look_speed); + descriptor.wheel_speed = static_cast(source.fly_control.wheel_speed); + if (source.fly_control.invert_y) descriptor.controller_flags |= DVZ_FLY_FLAGS_INVERT_Y; + if (source.fly_control.fixed_up) descriptor.controller_flags |= DVZ_FLY_FLAGS_FIXED_UP; + if (source.fly_control.disable_roll) descriptor.controller_flags |= DVZ_FLY_FLAGS_DISABLE_ROLL; + camera_controller_ = dvz_fly(scene_, &descriptor); + break; + } + case Camera_Controller::panzoom: { + DvzPanzoomDesc descriptor = dvz_panzoom_desc(); + if (source.panzoom_control.fixed_x) descriptor.controller_flags |= DVZ_PANZOOM_FLAGS_FIXED_X; + if (source.panzoom_control.fixed_y) descriptor.controller_flags |= DVZ_PANZOOM_FLAGS_FIXED_Y; + if (source.panzoom_control.keep_aspect) descriptor.controller_flags |= DVZ_PANZOOM_FLAGS_KEEP_ASPECT; + camera_controller_ = dvz_panzoom(scene_, &descriptor); + dimensions = DVZ_DIM_MASK_XY; + break; + } + } + if (camera_controller_ == nullptr || dvz_panel_bind_controller(panel_, camera_controller_, dimensions) != DVZ_OK) throw std::runtime_error("failed to bind Datoviz camera controller"); + applied_camera_ = source; +} + +bool Scene_Datoviz_State::matches_command_structure(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared) const { + if (figure_extent_ != scene.viewport || !applied_camera_ || *applied_camera_ != scene.camera || !applied_axes_ || *applied_axes_ != std::array{scene.x_axis, scene.y_axis, scene.z_axis} || prepared.size() != visuals_.size()) return false; + for (const auto& source : prepared) { + const auto target = std::ranges::find_if(visuals_, [&](const Datoviz_Visual_Instance& value) { + return value.identity == source.identity; + }); + if (target == visuals_.end()) return false; + if (target->applied_revision != source.visual.revision) return false; + } + return true; +} + +bool Scene_Datoviz_State::target_runtime_resources_current(const Prepared_Visual_Batch& prepared, std::uint8_t target_index) const { + if (target_index >= runtime_slots_.size()) throw std::out_of_range("Datoviz runtime target index is invalid"); + for (const auto& source : prepared) { + const auto target = std::ranges::find_if(visuals_, [&](const Datoviz_Visual_Instance& value) { + return value.identity == source.identity; + }); + if (target == visuals_.end()) throw std::logic_error("Datoviz runtime update contains an unknown Visual identity"); + if (target->operations->external_attributes.empty() && target->uploaded_data_revisions[target_index] != source.visual.data_revision) return false; + } + return true; +} + +std::vector Scene_Datoviz_State::stale_runtime_visuals(const Prepared_Visual_Batch& prepared, std::uint8_t target_index) const { + if (target_index >= runtime_slots_.size()) throw std::out_of_range("Datoviz runtime target index is invalid"); + std::vector result; + result.reserve(prepared.size()); + for (const auto& source : prepared) { + const auto target = std::ranges::find_if(visuals_, [&](const Datoviz_Visual_Instance& value) { + return value.identity == source.identity; + }); + if (target == visuals_.end()) throw std::logic_error("Datoviz runtime update contains an unknown Visual identity"); + if (target->operations->external_attributes.empty() && target->uploaded_data_revisions[target_index] != source.visual.data_revision) result.push_back(target->visual); + } + return result; +} + +void Scene_Datoviz_State::mark_target_runtime_resources_uploaded(std::uint8_t target_index) { + if (target_index >= runtime_slots_.size()) throw std::out_of_range("Datoviz runtime target index is invalid"); + for (auto& visual : visuals_) + if (visual.operations->external_attributes.empty()) visual.uploaded_data_revisions[target_index] = visual.applied_data_revision; +} +} // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/scene/rely_facade.h b/render_3D/render_3D/scene/rely_facade.h new file mode 100644 index 0000000..98d6641 --- /dev/null +++ b/render_3D/render_3D/scene/rely_facade.h @@ -0,0 +1,33 @@ +#pragma once +#include "../axis/Axis.hpp" +#include "../camera/Camera.hpp" +#include "../visual/Prepared_Visual.hpp" +#include "../visual/detail/Datoviz_Visual_Operations.hpp" +#include "global.hpp" +#include +#include +#include +namespace aethera::render_3d::scene { +/* Scene 只依赖 3D Renderable 的帧绑定、后端描述及内核 Renderable 能力。 */ +struct Renderable : facade_builder +::add_facade_with_substitution +::add_convention<_render, void(std::shared_ptr, std::size_t, detail::Visual_Publish)> +::add_convention<_use, const detail::Datoviz_Visual_Operations&() const noexcept> +::support_relocation +::build {}; + +struct Camera : facade_builder +::add_convention<_advance, void()> +::add_convention<_use, Camera_Descriptor() const> +::support_relocation +::build {}; + +struct Axes : facade_builder +::add_convention<_advance, void()> +::add_convention<_use, std::array() const> +::support_relocation +::build {}; + +using Task_Graph = task_flow::Task_Graph_Facade; +proxy make_task_graph(std::string name); +} // namespace aethera::render_3d::scene diff --git a/render_3D/render_3D/visual/Basic_Visual.cpp b/render_3D/render_3D/visual/Basic_Visual.cpp new file mode 100644 index 0000000..f4378f5 --- /dev/null +++ b/render_3D/render_3D/visual/Basic_Visual.cpp @@ -0,0 +1,5 @@ +#include "Basic_Visual.hpp" +namespace aethera::render_3d { +bool Basic_Visual::Prop::operator==(const Prop&) const = default; +bool Basic_Visual::State::operator==(const State&) const = default; +} diff --git a/render_3D/render_3D/visual/Basic_Visual.hpp b/render_3D/render_3D/visual/Basic_Visual.hpp new file mode 100644 index 0000000..e7c87ea --- /dev/null +++ b/render_3D/render_3D/visual/Basic_Visual.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "Prepared_Visual.hpp" +#include "model/attachment/dirty/Dirty_Concurrent.hpp" +#include "model/attachment/structure/Struct_Concurrent.hpp" +#include "renderable/src/renderable.hpp" +#include +#include +namespace aethera::render_3d { +/* 所有 3D Visual 共享变换、可见性、深度测试和异步发布能力。 */ +struct Basic_Visual : Def, Export_Struct_Concurrent> { + struct Prop : Prev { + Matrix4 transform{}; /* 本地坐标到 Scene 坐标的变换。 */ + bool visible{true}; /* 是否向后端提交图元。 */ + bool depth_test{true}; /* 是否启用深度测试。 */ + bool operator==(const Prop&) const; + }; + + struct State : Prev { + bool operator==(const State&) const; + }; + + struct Private; +}; +} // namespace aethera::render_3d +#include "Basic_Visual.ipp" diff --git a/render_3D/render_3D/visual/Basic_Visual.ipp b/render_3D/render_3D/visual/Basic_Visual.ipp new file mode 100644 index 0000000..21f020f --- /dev/null +++ b/render_3D/render_3D/visual/Basic_Visual.ipp @@ -0,0 +1,94 @@ +#pragma once +#include "Error_handling_specification/Failure_Policy.hpp" +#include +#include +#include +namespace aethera::render_3d { +struct Basic_Visual::Private : Prev_Private { + struct Paint_Target { + std::weak_ptr context{}; /* Scene 拥有;Visual 仅观察发布上下文的寿命。 */ + std::size_t slot{}; /* Scene 预分配且在其注册期内稳定的发布槽。 */ + detail::Visual_Publish publish{}; /* 只写 slot 对应元素,不扩容共享容器。 */ + }; + + void render(std::shared_ptr context, std::size_t slot, detail::Visual_Publish publish) { + paint_target = {std::move(context), slot, publish}; + } + +protected: + template + void advance_visual(Prop_Attachment& properties, Frame_Attachment& frame, State_Attachment& state); +private: + template + void prepare_data(Prop_Attachment& properties, Frame_Attachment& frame, State_Attachment& state); + Paint_Target paint_target{}; + detail::Prepared_Visual prepared{}; + std::uint64_t next_revision{1}; + std::uint64_t next_data_revision{1}; + std::uint64_t property_revision{}; + std::uint64_t frame_revision{}; + bool data_changed{true}; +}; + +template +void Basic_Visual::Private::advance_visual(Prop_Attachment& properties, Frame_Attachment& frame, State_Attachment& state) { + properties.internal.advance(); + frame.internal.advance(); + const auto next_property_revision = [&] { + if constexpr (requires { &Visual::Prop::style; }) return properties.internal.property_dirty_revision(properties.internal.internal_read, &Visual::Prop::style); + else return std::uint64_t{}; + }(); + const auto next_frame_revision = frame.internal.dirty_revision(); + data_changed = data_changed || next_property_revision != property_revision || next_frame_revision != frame_revision; + property_revision = next_property_revision; + frame_revision = next_frame_revision; + internal_taskflow()->clear(); + const auto task = internal_taskflow()->add("prepare_and_publish", [this, &properties, &frame, &state] { + prepare_data(properties, frame, state); + if (auto context = paint_target.context.lock(); context && paint_target.publish) paint_target.publish(context.get(), paint_target.slot, prepared); + }); + if (!task) Failure_Policy::handle_unknown_failure(std::make_exception_ptr(std::logic_error("3D visual task is invalid"))); +} + +template +void Basic_Visual::Private::prepare_data(Prop_Attachment& properties, Frame_Attachment& frame, State_Attachment& state) { + using Specification = typename Visual::Specification; + const auto& common = static_cast(*properties.internal.use()); + const auto& prop = static_cast(*properties.internal.use()); + const auto& input = *frame.internal.use(); + if (!std::ranges::all_of(input.items, [](const auto& item) { return Specification::valid(item); }) || !detail::finite(common.transform)) throw std::invalid_argument("3D visual property contains invalid data"); + auto data = prepared.data; + auto data_revision = prepared.data_revision; + if (std::exchange(data_changed, false) || !data) { + auto updated = std::make_shared(); + Specification::prepare(input.items, *updated); + if constexpr (requires { updated->style = prop.style; }) updated->style = prop.style; + if constexpr (requires { updated->field_width = input.field_width; }) { + updated->field_width = input.field_width; + updated->field_height = input.field_height; + } + if constexpr (requires { updated->field_pixels; input.field_pixels; }) { + updated->field_pixels.reserve(input.field_pixels.size()); + for (const auto pixel : input.field_pixels) updated->field_pixels.push_back({pixel.red, pixel.green, pixel.blue, pixel.alpha}); + if (updated->field_width == 0 || updated->field_height == 0 || static_cast(updated->field_width) * updated->field_height != updated->field_pixels.size()) throw std::invalid_argument("3D image pixel count does not match field dimensions"); + } + if constexpr (requires { updated->field_labels = input.field_labels; }) { + updated->field_labels = input.field_labels; + if (updated->field_width == 0 || updated->field_height == 0 || static_cast(updated->field_width) * updated->field_height != updated->field_labels.size()) throw std::invalid_argument("3D label count does not match field dimensions"); + } + if constexpr (requires { updated->field_depth = input.field_depth; }) updated->field_depth = input.field_depth; + if constexpr (requires { updated->values; updated->field_depth; }) { + const auto expected = static_cast(updated->field_width) * updated->field_height * updated->field_depth; + if (updated->field_width == 0 || updated->field_height == 0 || updated->field_depth == 0 || expected != updated->values.size()) throw std::invalid_argument("3D volume voxel count does not match field dimensions"); + } + data = std::move(updated); + data_revision = next_data_revision++; + } + prepared = {common.transform, common.visible, common.depth_test, next_revision++, data_revision, &Specification::datoviz_operations(), std::move(data)}; + auto& published = *state.internal.use(); + published.item_count = input.items.size(); + published.prepared_item_count = input.items.size(); + published.prepared_revision = prepared.revision; + state.internal.advance(); +} +} // namespace aethera::render_3d diff --git a/render_3D/render_3D/visual/Glyph_Visual.cpp b/render_3D/render_3D/visual/Glyph_Visual.cpp new file mode 100644 index 0000000..ccbcb85 --- /dev/null +++ b/render_3D/render_3D/visual/Glyph_Visual.cpp @@ -0,0 +1,6 @@ +#include "Glyph_Visual.hpp" +namespace aethera::render_3d { +bool Glyph_Frame::operator==(const Glyph_Frame&) const = default; +bool Glyph_Visual::Prop::operator==(const Prop&) const = default; +bool Glyph_Visual::State::operator==(const State&) const = default; +} diff --git a/render_3D/render_3D/visual/Glyph_Visual.hpp b/render_3D/render_3D/visual/Glyph_Visual.hpp new file mode 100644 index 0000000..01e2108 --- /dev/null +++ b/render_3D/render_3D/visual/Glyph_Visual.hpp @@ -0,0 +1,48 @@ +#pragma once +#include "Basic_Visual.hpp" +#include +namespace aethera::render_3d { +struct Glyph { + Vec3 position{}; + Vec4 bounds{}; + Vec4 texture_coordinates{}; + Color color{Color::white()}; + Coordinate_3D angle{}; + bool operator==(const Glyph&) const = default; +}; + +struct Glyph_Frame_Tag { + template + using Value = typename Layer::Frame; +}; + +struct Glyph_Frame { + std::vector items{}; /* 本次提交的完整 Glyph 集合。 */ + bool operator==(const Glyph_Frame&) const; +}; + +namespace detail { +struct Glyph_Spec; +} + +struct Glyph_Visual : Def> { + using Frame_Tag = Glyph_Frame_Tag; + using Frame = Glyph_Frame; + using Specification = detail::Glyph_Spec; + using Item = Glyph; + + struct Prop : Prev { + bool operator==(const Prop&) const; + }; + + struct State : Prev { + std::size_t item_count{}; /* 最近一次发布的输入项数。 */ + std::size_t prepared_item_count{}; /* 最近一次 Prepare 的后端项数。 */ + std::uint64_t prepared_revision{}; /* 最近一次 Prepare 的提交版本。 */ + bool operator==(const State&) const; + }; + + struct Private; +}; +} +#include "Glyph_Visual.ipp" diff --git a/render_3D/render_3D/visual/Glyph_Visual.ipp b/render_3D/render_3D/visual/Glyph_Visual.ipp new file mode 100644 index 0000000..b235de0 --- /dev/null +++ b/render_3D/render_3D/visual/Glyph_Visual.ipp @@ -0,0 +1,53 @@ +#pragma once +#include "detail/Visual_Prepare.hpp" +namespace aethera::render_3d::detail { +/* 后端转换结构与 Datoviz 分派契约仅属于实现,不进入 Visual 公共头文件。 */ +struct Glyph_Prepared_Data { + std::vector positions{}; /* Datoviz 顶点位置。 */ + std::vector colors{}; /* Datoviz 顶点颜色。 */ + std::vector angles{}; /* 每个 Glyph 的旋转角。 */ + std::vector> bounds{}; /* 每个 Glyph 的局部边界。 */ + std::vector> texture_coordinates{}; /* 每个 Glyph 的纹理坐标。 */ +}; + +struct Glyph_Spec { + using Item = Glyph; + using Prepared_Data = Glyph_Prepared_Data; + [[nodiscard]] static const Datoviz_Visual_Operations& datoviz_operations(); + [[nodiscard]] static bool valid(const Item&) noexcept; + static void prepare(const std::vector&, Prepared_Data&); +}; + +inline bool Glyph_Spec::valid(const Item& item) noexcept { + return finite(item.position) && finite(item.bounds) && finite(item.texture_coordinates) && finite(item.angle); +} +} // namespace aethera::render_3d::detail +namespace aethera::render_3d { +/* 具体 Visual 复用 Basic_Visual 的 Prepare、发布及缓冲推进钩子。 */ +struct Glyph_Visual::Private : Prev_Private { + void advance() override { + auto& properties = this->template attachment(); + auto& frame = this->template attachment(); + auto& state = this->template attachment(); + advance_visual(properties, frame, state); + } + [[nodiscard]] const detail::Datoviz_Visual_Operations& use() const noexcept { + return detail::Glyph_Spec::datoviz_operations(); + } +}; +} +namespace aethera::render_3d::detail { +inline void Glyph_Spec::prepare(const std::vector& items, Prepared_Data& output) { + reserve_position_color(output, items.size()); + output.angles.reserve(items.size()); + output.bounds.reserve(items.size()); + output.texture_coordinates.reserve(items.size()); + for (const auto& item : items) { + output.positions.push_back({item.position.x, item.position.y, item.position.z}); + output.colors.push_back(channels(item.color)); + output.angles.push_back(item.angle); + output.bounds.push_back({item.bounds.x, item.bounds.y, item.bounds.z, item.bounds.w}); + output.texture_coordinates.push_back({item.texture_coordinates.x, item.texture_coordinates.y, item.texture_coordinates.z, item.texture_coordinates.w}); + } +} +} diff --git a/render_3D/render_3D/visual/Image_Visual.cpp b/render_3D/render_3D/visual/Image_Visual.cpp new file mode 100644 index 0000000..319ce9c --- /dev/null +++ b/render_3D/render_3D/visual/Image_Visual.cpp @@ -0,0 +1,6 @@ +#include "Image_Visual.hpp" +namespace aethera::render_3d { +bool Image_Frame::operator==(const Image_Frame&) const = default; +bool Image_Visual::Prop::operator==(const Prop&) const = default; +bool Image_Visual::State::operator==(const State&) const = default; +} diff --git a/render_3D/render_3D/visual/Image_Visual.hpp b/render_3D/render_3D/visual/Image_Visual.hpp new file mode 100644 index 0000000..0770964 --- /dev/null +++ b/render_3D/render_3D/visual/Image_Visual.hpp @@ -0,0 +1,49 @@ +#pragma once +#include "Basic_Visual.hpp" +#include +namespace aethera::render_3d { +struct Image { + Vec3 position{}; + Vec2 extent{1.0F, 1.0F}; + Vec4 texture_rectangle{0.0F, 0.0F, 1.0F, 1.0F}; + bool operator==(const Image&) const = default; +}; + +struct Image_Frame_Tag { + template + using Value = typename Layer::Frame; +}; + +struct Image_Frame { + std::vector items{}; /* 本次提交的完整 Image 集合。 */ + std::uint32_t field_width{}; /* 本帧采样场宽度,单位为像素。 */ + std::uint32_t field_height{}; /* 本帧采样场高度,单位为像素。 */ + std::vector field_pixels{}; /* 本帧采样场的 RGBA 像素。 */ + bool operator==(const Image_Frame&) const; +}; + +namespace detail { +struct Image_Spec; +} + +struct Image_Visual : Def> { + using Frame_Tag = Image_Frame_Tag; + using Frame = Image_Frame; + using Specification = detail::Image_Spec; + using Item = Image; + + struct Prop : Prev { + bool operator==(const Prop&) const; + }; + + struct State : Prev { + std::size_t item_count{}; /* 最近一次发布的输入项数。 */ + std::size_t prepared_item_count{}; /* 最近一次 Prepare 的后端项数。 */ + std::uint64_t prepared_revision{}; /* 最近一次 Prepare 的提交版本。 */ + bool operator==(const State&) const; + }; + + struct Private; +}; +} +#include "Image_Visual.ipp" diff --git a/render_3D/render_3D/visual/Image_Visual.ipp b/render_3D/render_3D/visual/Image_Visual.ipp new file mode 100644 index 0000000..68d22a3 --- /dev/null +++ b/render_3D/render_3D/visual/Image_Visual.ipp @@ -0,0 +1,50 @@ +#pragma once +namespace aethera::render_3d::detail { +/* 后端转换结构与 Datoviz 分派契约仅属于实现,不进入 Visual 公共头文件。 */ +struct Image_Prepared_Data { + std::vector positions{}; /* 图像实例位置。 */ + std::vector> extents{}; /* 图像实例尺寸。 */ + std::vector> texture_rectangles{}; /* 图像纹理采样矩形。 */ + std::vector field_pixels{}; /* 本帧 RGBA 采样场。 */ + std::uint32_t field_width{}; /* 采样场宽度,单位为像素。 */ + std::uint32_t field_height{}; /* 采样场高度,单位为像素。 */ +}; + +struct Image_Spec { + using Item = Image; + using Prepared_Data = Image_Prepared_Data; + [[nodiscard]] static const Datoviz_Visual_Operations& datoviz_operations(); + [[nodiscard]] static bool valid(const Item&) noexcept; + static void prepare(const std::vector&, Prepared_Data&); +}; + +inline bool Image_Spec::valid(const Item& item) noexcept { + return finite(item.position) && finite(item.extent) && item.extent.x > 0.0F && item.extent.y > 0.0F && finite(item.texture_rectangle); +} +} // namespace aethera::render_3d::detail +namespace aethera::render_3d { +/* 具体 Visual 复用 Basic_Visual 的 Prepare、发布及缓冲推进钩子。 */ +struct Image_Visual::Private : Prev_Private { + void advance() override { + auto& properties = this->template attachment(); + auto& frame = this->template attachment(); + auto& state = this->template attachment(); + advance_visual(properties, frame, state); + } + [[nodiscard]] const detail::Datoviz_Visual_Operations& use() const noexcept { + return detail::Image_Spec::datoviz_operations(); + } +}; +} +namespace aethera::render_3d::detail { +inline void Image_Spec::prepare(const std::vector& items, Prepared_Data& output) { + output.positions.reserve(items.size()); + output.extents.reserve(items.size()); + output.texture_rectangles.reserve(items.size()); + for (const auto& item : items) { + output.positions.push_back({item.position.x, item.position.y, item.position.z}); + output.extents.push_back({item.extent.x, item.extent.y}); + output.texture_rectangles.push_back({item.texture_rectangle.x, item.texture_rectangle.y, item.texture_rectangle.z, item.texture_rectangle.w}); + } +} +} diff --git a/render_3D/render_3D/visual/Labels_Visual.cpp b/render_3D/render_3D/visual/Labels_Visual.cpp new file mode 100644 index 0000000..1bbda15 --- /dev/null +++ b/render_3D/render_3D/visual/Labels_Visual.cpp @@ -0,0 +1,6 @@ +#include "Labels_Visual.hpp" +namespace aethera::render_3d { +bool Labels_Frame::operator==(const Labels_Frame&) const = default; +bool Labels_Visual::Prop::operator==(const Prop&) const = default; +bool Labels_Visual::State::operator==(const State&) const = default; +} diff --git a/render_3D/render_3D/visual/Labels_Visual.hpp b/render_3D/render_3D/visual/Labels_Visual.hpp new file mode 100644 index 0000000..4150a56 --- /dev/null +++ b/render_3D/render_3D/visual/Labels_Visual.hpp @@ -0,0 +1,49 @@ +#pragma once +#include "Basic_Visual.hpp" +#include +namespace aethera::render_3d { +struct Label { + Vec3 position{}; + Vec2 extent{32.0F, 16.0F}; + Vec4 texture_rectangle{0.0F, 0.0F, 1.0F, 1.0F}; + bool operator==(const Label&) const = default; +}; + +struct Labels_Frame_Tag { + template + using Value = typename Layer::Frame; +}; + +struct Labels_Frame { + std::vector