From fc68247cbcb79e61f9545db6bfccd52023756ec0 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 28 Aug 2026 01:07:54 +0800 Subject: [PATCH] =?UTF-8?q?3D=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/kernel/Frame_Policy/Frame_Policy.hpp | 2 +- .../kernel/Frame_Policy/Frame_Policy_3D.cpp | 65 ++++ .../kernel/Frame_Policy/Frame_Policy_3D.hpp | 53 +++ kernel/src/test/Frame_Policy_3D_Test.cpp | 58 +++ render_3D/render_3D/base/Types.hpp | 20 +- .../render_3D/detail/Async_Render_Backend.cpp | 301 +++++++--------- .../render_3D/detail/Async_Render_Backend.hpp | 13 +- render_3D/render_3D/detail/Backend_Types.hpp | 3 +- .../detail/Datoviz_Visual_Backend.cpp | 302 +++++++++++++--- .../detail/Datoviz_Visual_Backend.hpp | 6 +- render_3D/render_3D/detail/Render_Domain.cpp | 15 +- render_3D/render_3D/detail/Render_Domain.hpp | 18 - render_3D/render_3D/detail/Render_Domain.ipp | 49 --- render_3D/render_3D/scene/Render_Scene_3D.cpp | 5 + render_3D/render_3D/scene/Render_Scene_3D.hpp | 9 +- render_3D/render_3D/scene/Render_Scene_3D.ipp | 333 ++++++++++++++---- render_3D/render_3D/visual/Basic_Visual.ipp | 63 +++- .../render_3D/visual/Prepared_Visual.hpp | 1 + render_3D/render_3D/visual/Visuals.hpp | 29 +- render_3D/render_3D/visual/Visuals.ipp | 12 +- render_3D/tests/Visual_Tests.cpp | 89 ++--- web_server/src/Gallery_Plots.cpp | 49 +++ web_server/src/Gallery_Plots.hpp | 22 ++ web_server/src/Gallery_Plots_3D.cpp | 81 ++++- web_server/src/Plot.cpp | 202 +++++++---- web_server/src/Web_Server.cpp | 47 +-- web_server/tests/Gallery_Plots_Tests.cpp | 31 ++ webapp_gallery/src/app.tsx | 12 +- 28 files changed, 1317 insertions(+), 573 deletions(-) create mode 100644 kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp create mode 100644 kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp create mode 100644 kernel/src/test/Frame_Policy_3D_Test.cpp delete mode 100644 render_3D/render_3D/detail/Render_Domain.ipp create mode 100644 web_server/src/Gallery_Plots.cpp create mode 100644 web_server/tests/Gallery_Plots_Tests.cpp diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp b/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp index 5e52ae1..69ce88e 100644 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp @@ -52,7 +52,7 @@ struct Frame_Policy_Event { * 任意线程只向 MPMC 事件流提交事实;Plot tick consumer 是唯一归并写者。 * 查询方只读取 current State,不参与事件归并,也不获取 Plot/Scene 锁。 */ -struct Frame_Policy final : double_buffer::Def< +struct Frame_Policy : double_buffer::Def< Frame_Policy, double_buffer::Root, double_buffer::Mpmc_Triple_Buffer< detail::Frame_Policy_Event_Stream, detail::Frame_Policy_Event>> { diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp new file mode 100644 index 0000000..72929f6 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp @@ -0,0 +1,65 @@ +#include "Frame_Policy_3D.hpp" +#include +#include +#include + +namespace aethera { +void Frame_Policy_3D::submit_3d(detail::Frame_Policy_3D_Event event) { + submit_stream(std::move(event)); +} + +void Frame_Policy_3D::record_gpu_submitted( + std::uint64_t sequence, bool overlaps_gpu) { + submit_3d({detail::Frame_Policy_3D_Event_Type::submitted, + sequence, overlaps_gpu}); +} + +void Frame_Policy_3D::record_gpu_completed(std::uint64_t sequence) { + submit_3d({detail::Frame_Policy_3D_Event_Type::completed, sequence}); +} + +void Frame_Policy_3D::reset_3d_statistics() { + submit_3d({detail::Frame_Policy_3D_Event_Type::reset}); +} + +bool Frame_Policy_3D::consume_3d_events() { + exchange_stream(); + bool consumed{}; + auto& state = static_cast( + double_buffer::detail::Internal_Access::pending_state(this)); + access_rendering_stream( + [&](std::span events) { + for (const auto& event : events) { + consumed = true; + switch (event.type) { + case detail::Frame_Policy_3D_Event_Type::submitted: + ++state.gpu_submission_count; + ++state.gpu_in_flight; + state.peak_gpu_in_flight = std::max( + state.peak_gpu_in_flight, state.gpu_in_flight); + state.last_submitted_sequence = event.sequence; + if (event.overlaps_gpu) + ++state.overlapped_release_count; + else + ++state.completion_gated_release_count; + break; + case detail::Frame_Policy_3D_Event_Type::completed: + ++state.gpu_completion_count; + if (state.gpu_in_flight != 0) --state.gpu_in_flight; + state.last_completed_sequence = event.sequence; + break; + case detail::Frame_Policy_3D_Event_Type::reset: { + const auto in_flight = state.gpu_in_flight; + state = {}; + state.pipeline_capacity = 3; + state.gpu_in_flight = in_flight; + state.peak_gpu_in_flight = in_flight; + break; + } + } + } + }); + if (consumed) publish_state(); + return consumed; +} +} diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp new file mode 100644 index 0000000..69c52f4 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp @@ -0,0 +1,53 @@ +#pragma once +#include "Frame_Policy.hpp" +#include + +namespace aethera { +namespace detail { +struct Frame_Policy_3D_Event_Stream; +enum struct Frame_Policy_3D_Event_Type : std::uint8_t { + submitted, + completed, + reset +}; +struct Frame_Policy_3D_Event { + Frame_Policy_3D_Event_Type type{}; + std::uint64_t sequence{}; + bool overlaps_gpu{}; +}; +} + +/* + * 3D 策略是 Frame_Policy 的独立状态层。它复用公共请求/吞吐事实,但不改变 + * 2D Frame_Policy 的默认模式和完成释放语义。GPU 提交与退休由独立 MPMC + * 事件流进入,Plot 的唯一 tick consumer 归并并通过 double-buffer 发布。 + */ +struct Frame_Policy_3D final : double_buffer::Def< + Frame_Policy_3D, Frame_Policy, + double_buffer::Mpmc_Triple_Buffer< + detail::Frame_Policy_3D_Event_Stream, + detail::Frame_Policy_3D_Event>> { + struct Prop : Prev_Prop {}; + struct State : Prev_State { + std::size_t pipeline_capacity{3}; /* Scene 可绑定的物理 3D 帧槽数量。 */ + std::uint64_t gpu_submission_count{}; /* Vulkan 实际提交的累计帧数。 */ + std::uint64_t overlapped_release_count{}; /* 提交后即开放下一次 Prepare 的累计帧数。 */ + std::uint64_t completion_gated_release_count{}; /* 因共享资源而等 GPU 退休后开放的累计帧数。 */ + std::uint64_t gpu_completion_count{}; /* fence 正常或失败退休的累计帧数。 */ + std::uint64_t gpu_in_flight{}; /* 已实际提交且尚未退休的当前帧数。 */ + std::uint64_t peak_gpu_in_flight{}; /* 本统计代次内 gpu_in_flight 峰值。 */ + std::uint64_t last_submitted_sequence{}; /* 最近实际提交的帧序号;未提交时为 0。 */ + std::uint64_t last_completed_sequence{}; /* 最近退休的帧序号;未退休时为 0。 */ + bool operator==(const State&) const = default; + }; + struct Private : Prev_Private {}; + + void record_gpu_submitted(std::uint64_t sequence, bool overlaps_gpu); + void record_gpu_completed(std::uint64_t sequence); + void reset_3d_statistics(); + [[nodiscard]] bool consume_3d_events(); + +private: + void submit_3d(detail::Frame_Policy_3D_Event event); +}; +} diff --git a/kernel/src/test/Frame_Policy_3D_Test.cpp b/kernel/src/test/Frame_Policy_3D_Test.cpp new file mode 100644 index 0000000..fd2d320 --- /dev/null +++ b/kernel/src/test/Frame_Policy_3D_Test.cpp @@ -0,0 +1,58 @@ +#include "Frame_Policy/Frame_Policy_3D.hpp" +#include +#include + +namespace { + +TEST(frame_policy_3d, publishes_gpu_pipeline_facts_without_changing_common_policy) { + auto built = aethera::Frame_Policy_3D::Builder< + aethera::Frame_Policy_3D>{}.build(); + ASSERT_TRUE(built.has_value()); + auto policy = std::move(*built); + + policy->record_gpu_submitted(11, true); + policy->record_gpu_submitted(12, false); + policy->record_gpu_completed(11); + ASSERT_TRUE(policy->consume_3d_events()); + + const auto& pipeline = policy->read_state< + aethera::Frame_Policy_3D::Base_Tag>(); + EXPECT_EQ(pipeline.pipeline_capacity, 3u); + EXPECT_EQ(pipeline.gpu_submission_count, 2u); + EXPECT_EQ(pipeline.overlapped_release_count, 1u); + EXPECT_EQ(pipeline.completion_gated_release_count, 1u); + EXPECT_EQ(pipeline.gpu_completion_count, 1u); + EXPECT_EQ(pipeline.gpu_in_flight, 1u); + EXPECT_EQ(pipeline.peak_gpu_in_flight, 2u); + EXPECT_EQ(pipeline.last_submitted_sequence, 12u); + EXPECT_EQ(pipeline.last_completed_sequence, 11u); + + const auto& common = policy->read_state(); + EXPECT_EQ(common.submitted_frame_count, 0u); + EXPECT_EQ(common.completed_frame_count, 0u); + EXPECT_EQ(common.mode, aethera::Frame_Pacing_Mode::fixed_rate); +} + +TEST(frame_policy_3d, reset_preserves_only_current_in_flight_ownership) { + auto built = aethera::Frame_Policy_3D::Builder< + aethera::Frame_Policy_3D>{}.build(); + ASSERT_TRUE(built.has_value()); + auto policy = std::move(*built); + + policy->record_gpu_submitted(21, true); + ASSERT_TRUE(policy->consume_3d_events()); + policy->reset_3d_statistics(); + ASSERT_TRUE(policy->consume_3d_events()); + + const auto& state = policy->read_state< + aethera::Frame_Policy_3D::Base_Tag>(); + EXPECT_EQ(state.pipeline_capacity, 3u); + EXPECT_EQ(state.gpu_submission_count, 0u); + EXPECT_EQ(state.gpu_completion_count, 0u); + EXPECT_EQ(state.gpu_in_flight, 1u); + EXPECT_EQ(state.peak_gpu_in_flight, 1u); + EXPECT_EQ(state.last_submitted_sequence, 0u); + EXPECT_EQ(state.last_completed_sequence, 0u); +} + +} diff --git a/render_3D/render_3D/base/Types.hpp b/render_3D/render_3D/base/Types.hpp index 617e101..b651a36 100644 --- a/render_3D/render_3D/base/Types.hpp +++ b/render_3D/render_3D/base/Types.hpp @@ -48,22 +48,26 @@ struct Linear_Color { bool operator==(const Linear_Color&) const = default; }; enum struct Visual_Family : std::uint8_t { point, splat, pixel, marker, sphere, segment, vector, primitive, mesh, path, image, labels, glyph, text, volume }; -enum struct Primitive_Topology : std::uint8_t { point_list, line_list, line_strip, triangle_list, triangle_strip }; enum struct Marker_Shape : std::uint8_t { disc, square, triangle, diamond, cross }; enum struct Point_Aspect : std::uint8_t { filled, stroke, outline }; struct Visual_Settings { bool operator==(const Visual_Settings&) const = default; }; -struct Primitive_Settings { - Primitive_Topology topology{Primitive_Topology::triangle_list}; /* 顶点装配拓扑。 */ - bool operator==(const Primitive_Settings&) const = default; -}; -struct Texture_Field_Settings { +struct Image_Field_Settings { std::uint32_t field_width{}; /* 纹理字段宽度;零值表示尚未提供纹理。 */ std::uint32_t field_height{}; /* 纹理字段高度;必须与宽度同时为零或非零。 */ - bool operator==(const Texture_Field_Settings&) const = default; + std::vector field_pixels{}; /* 行优先 RGBA8 像素;数量必须等于宽乘高。 */ + bool operator==(const Image_Field_Settings&) const = default; }; -struct Volume_Field_Settings : Texture_Field_Settings { +struct Labels_Field_Settings { + std::uint32_t field_width{}; /* 分类字段宽度,单位为样本。 */ + std::uint32_t field_height{}; /* 分类字段高度,单位为样本。 */ + std::vector field_labels{}; /* 行优先分类 ID;数量必须等于宽乘高。 */ + bool operator==(const Labels_Field_Settings&) const = default; +}; +struct Volume_Field_Settings { + std::uint32_t field_width{}; /* 体字段宽度,单位为体素。 */ + std::uint32_t field_height{}; /* 体字段高度,单位为体素。 */ std::uint32_t field_depth{}; /* 体数据深度;三个维度必须同时为零或非零。 */ bool operator==(const Volume_Field_Settings&) const = default; }; diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp index 1d4c53a..d333f0a 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.cpp @@ -8,13 +8,10 @@ #include #include #include -#include #include -#include #include #include #include -#include #include #include namespace aethera::render_3d::detail { @@ -83,31 +80,29 @@ std::string failure_description(const std::exception_ptr& failure) { struct Async_Render_Backend::Implementation : std::enable_shared_from_this { struct Submission { - Shared_Prepared_Visual_Batch visuals{}; /* Scene CPU Prepare 发布的大批量不可变快照。 */ - Scene_3D_Parameters parameters{}; /* 本帧 Scene、Camera 与轴参数快照。 */ + Shared_Prepared_Visual_Batch visuals{}; /* Scene CPU Prepare 发布的本帧数据句柄。 */ + Scene_3D_Parameters parameters{}; /* 本帧 Scene、Camera 与轴参数。 */ Scene::Event_Batch events{}; /* Prepare 边界已交换的完整事件批次。 */ }; struct Pending { Datoviz_Visual_Backend::Pending_Frame backend_frame{}; /* 三缓冲目标对应的已录制提交。 */ - Frame_3D* output{}; /* 调用方持有到 render() 返回。 */ - Extent extent{}; /* 失败时生成空结果所需的尺寸。 */ + Frame_3D* output{}; /* 调用方持有到 completed 返回同一地址。 */ std::optional completion_reservation{}; + Async_Render_Backend::Submitted submitted_callback{}; + Async_Render_Backend::Completed completed_callback{}; bool prepared{}; std::atomic_bool submitted{}; /* 区分提交前取消与 GPU 已拥有资源后的终止。 */ + std::atomic_bool completion_enqueued{}; /* reservation 与提交失败只允许一个退休事实。 */ }; struct Gpu_Completion { std::shared_ptr pending{}; /* 完成前保持帧目标和回调集合存活。 */ std::optional result{}; /* fence 正常交付时的结果。 */ std::exception_ptr failure{}; /* 提交域或完成服务的 Unknown Failure。 */ }; - std::shared_ptr render_domain; /* 同 GPU 唯一的轻量 Queue Submit 域。 */ + std::shared_ptr render_domain; /* 同 GPU 唯一的 Datoviz/Vulkan 写入域。 */ std::unique_ptr backend{}; /* Builder build() 创建;随后由本 Scene 串行使用。 */ - std::mutex failure_mutex{}; /* 保护跨线程传播的最后 Unknown Failure。 */ - std::exception_ptr failure{}; /* 后端不可用的根因。 */ - std::mutex admission_mutex{}; /* render/event 与 stop 的原子准入边界。 */ - std::atomic_size_t active_preparations{}; /* 已获准且尚未退出 CPU/Datoviz Prepare 的调用数。 */ - std::atomic_bool available{true}; /* 对 Scene 发布的后端可接收状态。 */ - std::atomic_bool stop_requested{}; /* 拒绝新命令并等待在途槽结清。 */ + bool overlaps_gpu{}; /* 全部 Visual 是否拥有逐目标独立上传资源。 */ + std::atomic_bool available{true}; /* 是否仍接受新帧;停止或故障后永久为 false。 */ Implementation(std::uint32_t gpu_index_value, bool validation_enabled_value, @@ -115,7 +110,30 @@ struct Async_Render_Backend::Implementation const Scene_3D_Parameters& initial_scene) : render_domain(Render_Domain::acquire(gpu_index_value)), backend(std::make_unique( - gpu_index_value, validation_enabled_value, visuals, initial_scene)) {} + gpu_index_value, validation_enabled_value, visuals, initial_scene)) { + overlaps_gpu = std::ranges::all_of(visuals, [](const auto& visual) { + switch (visual.family) { + case Visual_Family::point: + case Visual_Family::splat: + case Visual_Family::pixel: + case Visual_Family::sphere: + case Visual_Family::primitive: + case Visual_Family::mesh: + case Visual_Family::path: + return true; + case Visual_Family::marker: + case Visual_Family::segment: + case Visual_Family::vector: + case Visual_Family::image: + case Visual_Family::labels: + case Visual_Family::glyph: + case Visual_Family::text: + case Visual_Family::volume: + return false; + } + return false; + }); + } ~Implementation(); void stop() noexcept; @@ -123,13 +141,14 @@ struct Async_Render_Backend::Implementation void render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame, Scene::Event_Batch events, - std::function completion_callback); - void accept(Submission submission, std::shared_ptr pending, - std::shared_ptr> completion); - void prepare(Submission submission, std::shared_ptr pending, - std::shared_ptr> completion); - void submit(std::shared_ptr pending, - std::shared_ptr> completion); + Async_Render_Backend::Submitted submitted_callback, + Async_Render_Backend::Completed completed_callback); + void queue_prepare(Submission submission, std::shared_ptr pending); + void prepare_and_submit(Submission submission, + std::shared_ptr pending); + void queue_finish(std::shared_ptr pending, + std::optional result, + std::exception_ptr failure); void finish(Gpu_Completion completion); void resolve(std::shared_ptr pending, Datoviz_Visual_Backend::Completed_Frame completed); @@ -151,15 +170,7 @@ Async_Render_Backend::Implementation::~Implementation() { } void Async_Render_Backend::Implementation::stop() noexcept { - if (stop_requested.exchange(true, std::memory_order_acq_rel)) return; - try { - std::lock_guard lock(admission_mutex); - available.store(false, std::memory_order_release); - if (active_preparations.load(std::memory_order_acquire) == 0 && - backend && backend->idle()) - backend.reset(); - } - catch (...) { fail(std::current_exception()); } + available.store(false, std::memory_order_release); } void Async_Render_Backend::Implementation::fail( std::exception_ptr value) noexcept { @@ -171,47 +182,31 @@ void Async_Render_Backend::Implementation::fail( std::fflush(stderr); } catch (...) {} - try { - std::lock_guard lock(failure_mutex); - if (!failure) failure = std::move(value); - } - catch (...) {} } void Async_Render_Backend::Implementation::render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame, Scene::Event_Batch events, - std::function completion_callback) { + Async_Render_Backend::Submitted submitted_callback, + Async_Render_Backend::Completed completed_callback) { if (!frame) throw std::invalid_argument("3D backend requires a non-null external frame"); if (!visuals || visuals->empty()) - throw std::invalid_argument("3D backend requires prepared Visual snapshots"); - if (!completion_callback) + throw std::invalid_argument("3D backend requires prepared Visual data"); + if (!submitted_callback || !completed_callback) throw std::invalid_argument("3D backend requires a completion callback"); - { - std::lock_guard admission_lock(admission_mutex); - if (!available.load(std::memory_order_acquire) || - stop_requested.load(std::memory_order_acquire)) - throw std::runtime_error("3D backend is unavailable"); - active_preparations.fetch_add(1, std::memory_order_release); - } - struct Preparation_Admission { - std::atomic_size_t& count; - ~Preparation_Admission() { - count.fetch_sub(1, std::memory_order_acq_rel); - } - } preparation_admission{active_preparations}; + if (!available.load(std::memory_order_acquire)) + throw std::runtime_error("3D backend is unavailable"); auto pending = std::make_shared(); pending->output = frame; - pending->extent = parameters.viewport; - auto completion = std::make_shared< - std::promise>(); - auto completed = completion->get_future(); + pending->submitted_callback = std::move(submitted_callback); + pending->completed_callback = std::move(completed_callback); + auto self = shared_from_this(); auto reservation = Gpu_Completion_Service::instance().prepare( - [completion](Gpu_Completion_Service::Result result) { - completion->set_value(std::move(result)); + [self, pending](Gpu_Completion_Service::Result result) { + self->queue_finish(pending, std::move(result), {}); }, - [completion](std::exception_ptr value) { - completion->set_exception(std::move(value)); + [self, pending](std::exception_ptr value) { + self->queue_finish(pending, {}, std::move(value)); }, frame->identity().sequence == 1 || frame->identity().sequence % backend_observation_period == 0); if (!reservation) { @@ -221,87 +216,57 @@ void Async_Render_Backend::Implementation::render( throw std::runtime_error("GPU completion service is unavailable"); } pending->completion_reservation.emplace(std::move(reservation.reservation)); - try { - /* CPU 事件处理、Datoviz apply/emit/execute 与帧目标准备属于调用 Scene - * 的 frame Taskflow。这里先完成可并行的重工作,Render Domain 只接收 - * 已录制 Pending_Frame 的轻量 Vulkan submit。 */ - accept(Submission{std::move(visuals), std::move(parameters), - std::move(events)}, pending, completion); - Task_Graph::corun_until([&completed] { - return completed.wait_for(std::chrono::seconds(0)) == - std::future_status::ready; - }); - std::optional result; - std::exception_ptr completion_failure; - try { result.emplace(completed.get()); } - catch (...) { completion_failure = std::current_exception(); } - finish(Gpu_Completion{pending, std::move(result), - std::move(completion_failure)}); - completion_callback(frame); - } - catch (...) { - fail(std::current_exception()); - throw; - } + queue_prepare(Submission{std::move(visuals), std::move(parameters), + std::move(events)}, std::move(pending)); } -void Async_Render_Backend::Implementation::accept( - Submission submission, std::shared_ptr pending, - std::shared_ptr> completion) { - if (stop_requested.load(std::memory_order_acquire) || - !available.load(std::memory_order_acquire)) { - completion->set_exception(std::make_exception_ptr( - std::runtime_error("3D backend stopped before frame preparation"))); - return; - } - if (backend && - !backend->can_prepare(submission.parameters, *submission.visuals)) { - completion->set_exception(std::make_exception_ptr(std::logic_error( - "3D Scene admitted a new frame before its previous frame completed"))); - return; - } - prepare(std::move(submission), std::move(pending), std::move(completion)); -} -void Async_Render_Backend::Implementation::prepare( - Submission submission, std::shared_ptr pending, - std::shared_ptr> completion) { - std::optional prepared; +void Async_Render_Backend::Implementation::queue_prepare( + Submission submission, std::shared_ptr pending) { + auto self = shared_from_this(); + Render_Domain::Post_Result queued{}; try { - const auto sequence = pending->output->identity().sequence; - for (const auto& event : submission.events) { - if (event) event->mark_dispatch_started(sequence); - dispatch(event, submission.parameters.viewport); - } - submission.events.clear(); - pending->output->mark(Frame_Trace_Marker::backend_prepare_started); - const bool readback = pending->output->output() == Frame_3D_Output::pixels; - const bool observe = sequence == 1 || - sequence % backend_observation_period == 0; - /* - * Datoviz apply/emit/DRP2 execute shares one GPU-context API and queue - * synchronization domain. Running it on Scene workers makes all other - * workers wait on the same mutex. Transfer that section to Render Domain; - * cooperative_invoke releases this worker segment and lets Taskflow run - * unrelated 2D/3D work until the prepared target is returned. - */ - auto invocation = render_domain->cooperative_invoke( - [this, parameters = submission.parameters, - visuals = submission.visuals, sequence, observe, readback] { - return backend->prepare(parameters, *visuals, sequence, - observe, readback); + queued = render_domain->post( + [self, submission = std::move(submission), pending]() mutable { + self->prepare_and_submit(std::move(submission), pending); + }, + [self, pending](std::exception_ptr value) { + self->queue_finish(pending, {}, std::move(value)); }); - if (!invocation) - throw std::runtime_error( - "render domain stopped before Datoviz frame preparation"); - prepared = std::move(*invocation.value); - } - catch (...) { - completion->set_exception(std::current_exception()); + } catch (...) { + auto failure_value = std::current_exception(); + fail(failure_value); + pending->completion_reservation.reset(); + pending->completed_callback(pending->output, std::move(failure_value)); return; } + if (queued == Render_Domain::Post_Result::queued) return; + auto failure_value = std::make_exception_ptr(std::runtime_error( + "render domain stopped before 3D frame preparation")); + pending->completion_reservation.reset(); + pending->completed_callback(pending->output, std::move(failure_value)); +} +void Async_Render_Backend::Implementation::prepare_and_submit( + Submission submission, std::shared_ptr pending) { + std::optional prepared; + const auto sequence = pending->output->identity().sequence; + for (const auto& event : submission.events) { + if (event) event->mark_dispatch_started(sequence); + dispatch(event, submission.parameters.viewport); + } + submission.events.clear(); + pending->output->mark(Frame_Trace_Marker::backend_prepare_started); + const bool readback = pending->output->output() == Frame_3D_Output::pixels; + const bool observe = sequence == 1 || + sequence % backend_observation_period == 0; + /* + * Datoviz 对同一 GPU context 的 apply/emit/submit/collect 只有 Render Domain + * 这一位写者。本任务已经位于该域,直接完成 Prepare 后提交,不把中间结果 + * 往返给 Scene worker,也不为资源串行化再增加第二把锁。 + */ + prepared = backend->prepare(submission.parameters, *submission.visuals, + sequence, observe, readback); if (!prepared) { - completion->set_exception(std::make_exception_ptr(std::logic_error( - "Datoviz did not provide a frame target after Scene admission"))); - return; + throw std::logic_error( + "Datoviz did not provide a frame target after Scene admission"); } pending->output->mark(Frame_Trace_Marker::backend_prepare_finished); record_datoviz_trace(pending->output, prepared->trace); @@ -309,39 +274,46 @@ void Async_Render_Backend::Implementation::prepare( pending->output->mark(Frame_Trace_Marker::backend_submit_queued); pending->backend_frame = std::move(*prepared); pending->prepared = true; - try { - submit(pending, completion); - } - catch (...) { - backend->discard(std::move(pending->backend_frame)); - pending->prepared = false; - completion->set_exception(std::current_exception()); - } + pending->output->mark(Frame_Trace_Marker::backend_queue_left); + backend->submit(pending->backend_frame); + pending->submitted.store(true, std::memory_order_release); + pending->output->mark(Frame_Trace_Marker::gpu_submitted); + pending->completion_reservation->watch( + pending->backend_frame.device, pending->backend_frame.fence); + pending->completion_reservation.reset(); + pending->submitted_callback(pending->output, overlaps_gpu); } -void Async_Render_Backend::Implementation::submit( +void Async_Render_Backend::Implementation::queue_finish( std::shared_ptr pending, - std::shared_ptr> completion) { + std::optional result, + std::exception_ptr failure_value) { + if (pending->completion_enqueued.exchange(true, std::memory_order_acq_rel)) + return; auto self = shared_from_this(); - /* Completion 槽位分配、回调所有权建立和服务登记没有 GPU 线程亲和性。 - * 它们在当前 Scene Taskflow worker 完成;唯一 Render Domain 只保留 - * vkQueueSubmit 与 fence 交付。Reservation 随 Pending 存活,提交前失败 - * 会由析构自动取消,不产生第二套生命周期状态。 */ const auto queued = render_domain->post( - [self, pending] { - pending->output->mark(Frame_Trace_Marker::backend_queue_left); - self->backend->submit(pending->backend_frame); - pending->submitted.store(true, std::memory_order_release); - pending->output->mark(Frame_Trace_Marker::gpu_submitted); - pending->completion_reservation->watch( - pending->backend_frame.device, pending->backend_frame.fence); - pending->completion_reservation.reset(); + [self, pending, result = std::move(result), + failure_value = std::move(failure_value)]() mutable { + std::exception_ptr completion_failure = std::move(failure_value); + try { + self->finish(Gpu_Completion{ + pending, std::move(result), completion_failure}); + } + catch (...) { + completion_failure = std::current_exception(); + self->fail(completion_failure); + } + pending->completed_callback(pending->output, + std::move(completion_failure)); }, - [completion](std::exception_ptr value) { - completion->set_exception(std::move(value)); + [self, pending](std::exception_ptr value) mutable { + self->fail(value); + pending->completed_callback(pending->output, std::move(value)); }); if (queued != Render_Domain::Post_Result::queued) { - completion->set_exception(std::make_exception_ptr(std::runtime_error( - "render domain stopped before 3D queue submission"))); + auto value = std::make_exception_ptr(std::runtime_error( + "render domain stopped before 3D frame retirement")); + fail(value); + pending->completed_callback(pending->output, std::move(value)); } } void Async_Render_Backend::Implementation::resolve( @@ -393,8 +365,6 @@ void Async_Render_Backend::Implementation::finish(Gpu_Completion completion) { std::move(completion.pending->backend_frame)); completion.pending->prepared = false; resolve(std::move(completion.pending), std::move(completed)); - if (stop_requested.load(std::memory_order_acquire) && backend && backend->idle()) - backend.reset(); } void Async_Render_Backend::Implementation::dispatch( const std::shared_ptr& event, Extent viewport) { @@ -440,9 +410,10 @@ void Async_Render_Backend::Implementation::dispatch( void Async_Render_Backend::render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame, Scene::Event_Batch events, - std::function completion) { + Submitted submitted, Completed completed) { implementation_->render(std::move(visuals), parameters, frame, - std::move(events), std::move(completion)); + std::move(events), std::move(submitted), + std::move(completed)); } bool Async_Render_Backend::available() const noexcept { return implementation_->available.load(std::memory_order_acquire); diff --git a/render_3D/render_3D/detail/Async_Render_Backend.hpp b/render_3D/render_3D/detail/Async_Render_Backend.hpp index f7aad97..bce1a58 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.hpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.hpp @@ -2,6 +2,7 @@ #include "../base/Frame_3D.hpp" #include "Backend_Types.hpp" #include +#include #include #include namespace aethera::render_3d::detail { @@ -13,16 +14,18 @@ public: ~Async_Render_Backend(); Async_Render_Backend(const Async_Render_Backend&) = delete; Async_Render_Backend& operator=(const Async_Render_Backend&) = delete; + using Submitted = std::function; + using Completed = std::function; /* - * 当前 Scene Taskflow 节点完成 CPU/Datoviz Prepare 后,只把轻量提交 - * 转移给 GPU Render Domain。fence 回调恢复当前节点;等待期间 Worker - * 通过 Taskflow corun_until 执行同一 Executor 的其他任务。completion - * 返回后才执行 Scene completion DAG,帧及像素所有权始终属于该帧。 + * render() 只把本帧不可变输入句柄移交给 Render Domain,不等待 Prepare、 + * vkQueueSubmit 或 GPU。Datoviz Prepare、实际提交和 fence 注册由同一个 GPU + * 单写者顺序完成;submitted 在真实提交后发布资源重叠能力,completed 在该域 + * collect 完成后发布。调用方线程与 Taskflow worker 全程没有同步等待节点。 */ void render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame, Scene::Event_Batch events, - std::function completion); + Submitted submitted, Completed completed); [[nodiscard]] bool available() const noexcept; private: struct Implementation; diff --git a/render_3D/render_3D/detail/Backend_Types.hpp b/render_3D/render_3D/detail/Backend_Types.hpp index 166e65c..ca171ad 100644 --- a/render_3D/render_3D/detail/Backend_Types.hpp +++ b/render_3D/render_3D/detail/Backend_Types.hpp @@ -31,12 +31,13 @@ struct Prepared_Visual { bool visible{true}; bool depth_test{true}; std::uint64_t revision{}; + std::uint64_t data_revision{}; Prepared_Visual_Payload data{}; }; template Prepared_Visual erase_prepared_visual(const Prepared_Visual_For& source) { return {source.transform, source.visible, source.depth_test, source.revision, - source.data}; + source.data_revision, source.data}; } using Visual_Identity = std::uintptr_t; struct Visual_Registration { diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp index 0376dc7..522a3e8 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include namespace aethera::render_3d::detail { namespace { @@ -103,7 +104,7 @@ std::string axis_title(const plot::Axis_Descriptor& axis) { if (axis.unit.empty()) return axis.label; return axis.label + " (" + axis.unit + ")"; } -void configure_glyph_text(DvzScene* scene, DvzVisual* visual, const char* text) { +DvzFont* configure_glyph_text(DvzScene* scene, DvzVisual* visual, const char* text) { auto font_descriptor = dvz_font_desc(); font_descriptor.family = "Roboto"; font_descriptor.style = "Regular"; @@ -168,6 +169,116 @@ void configure_glyph_text(DvzScene* scene, DvzVisual* visual, const char* text) static_cast(updates.size())) != DVZ_OK || dvz_visual_set_depth_test(visual, false) != DVZ_OK) throw std::runtime_error("failed to upload Datoviz text geometry"); + return font; +} + +std::vector utf8_codepoints(std::string_view text) { + std::vector result; + result.reserve(text.size()); + for (std::size_t index = 0; index < text.size();) { + const auto lead = static_cast(text[index]); + std::uint32_t codepoint{}; + std::size_t length{}; + if (lead < 0x80U) { + codepoint = lead; + length = 1; + } + else if ((lead & 0xE0U) == 0xC0U) { + codepoint = lead & 0x1FU; + length = 2; + } + else if ((lead & 0xF0U) == 0xE0U) { + codepoint = lead & 0x0FU; + length = 3; + } + else if ((lead & 0xF8U) == 0xF0U) { + codepoint = lead & 0x07U; + length = 4; + } + else { + throw std::invalid_argument("3D text contains invalid UTF-8"); + } + if (index + length > text.size()) + throw std::invalid_argument("3D text contains truncated UTF-8"); + for (std::size_t offset = 1; offset < length; ++offset) { + const auto continuation = + static_cast(text[index + offset]); + if ((continuation & 0xC0U) != 0x80U) + throw std::invalid_argument("3D text contains invalid UTF-8"); + codepoint = (codepoint << 6U) | (continuation & 0x3FU); + } + if ((length == 2 && codepoint < 0x80U) || + (length == 3 && codepoint < 0x800U) || + (length == 4 && codepoint < 0x10000U) || + codepoint > 0x10FFFFU || + (codepoint >= 0xD800U && codepoint <= 0xDFFFU)) + throw std::invalid_argument("3D text contains non-canonical UTF-8"); + result.push_back(codepoint); + index += length; + } + return result; +} + +void upload_text(DvzVisual* visual, DvzFont* font, + const Text_Prepared_Data& data) { + if (font == nullptr) throw std::logic_error("Datoviz text visual has no font"); + auto atlas_specification = + dvz_text_atlas_spec(DVZ_TEXT_RENDERER_MSDF_ATLAS, 64.0F); + for (const auto& text : data.strings) + if (!dvz_font_atlas_ensure_string( + font, &atlas_specification, text.c_str())) + throw std::runtime_error("failed to grow Datoviz text atlas"); + const auto* atlas = dvz_font_atlas(font, &atlas_specification); + if (atlas == nullptr || dvz_glyph_set_atlas(visual, atlas) != DVZ_OK) + throw std::runtime_error("failed to bind Datoviz text atlas"); + + std::vector> positions; + std::vector> bounds; + std::vector> texture_coordinates; + std::vector> colors; + std::vector angles; + for (std::size_t item_index = 0; item_index < data.strings.size(); + ++item_index) { + const auto& text = data.strings[item_index]; + const auto codepoints = utf8_codepoints(text); + float line_width{}; + for (const auto codepoint : codepoints) { + const auto* glyph = dvz_text_atlas_glyph(atlas, codepoint); + if (glyph != nullptr) line_width += glyph->advance; + } + const float scale = data.sizes[item_index] / 64.0F; + float cursor_x{}; + for (const auto codepoint : codepoints) { + const auto* glyph = dvz_text_atlas_glyph(atlas, codepoint); + if (glyph == nullptr) continue; + const float x0 = (cursor_x + glyph->xoff - 0.5F * line_width) * scale; + const float y0 = glyph->yoff * scale; + const std::array glyph_bounds{ + x0, y0, x0 + glyph->width * scale, + y0 + glyph->height * scale}; + const std::array glyph_texture{ + glyph->uv[0], glyph->uv[1], glyph->uv[2], glyph->uv[3]}; + for (std::uint32_t vertex = 0; vertex < 6; ++vertex) { + positions.push_back(data.positions[item_index]); + bounds.push_back(glyph_bounds); + texture_coordinates.push_back(glyph_texture); + colors.push_back(data.colors[item_index]); + angles.push_back(0.0F); + } + cursor_x += glyph->advance; + } + } + const auto count = static_cast(positions.size()); + const std::array updates{{ + {"position", positions.data(), count}, + {"bounds", bounds.data(), count}, + {"texcoords", texture_coordinates.data(), count}, + {"color", colors.data(), count}, + {"angle", angles.data(), count}}}; + if (dvz_visual_set_data_many( + visual, updates.data(), + static_cast(updates.size())) != DVZ_OK) + throw std::runtime_error("failed to upload Datoviz text payload"); } bool supports_item_interaction(Visual_Family family) noexcept { return family == Visual_Family::point || family == Visual_Family::pixel || @@ -872,6 +983,8 @@ void Datoviz_Visual_Backend::create_scene( throw std::invalid_argument("Datoviz backend received duplicate Visual identity"); const auto family = registration.family; DvzVisual* visual{}; + DvzSampledField* sampled_field{}; + DvzFont* font{}; switch (family) { case Visual_Family::point: visual = dvz_point(scene_, 0); break; @@ -905,21 +1018,19 @@ void Datoviz_Visual_Backend::create_scene( break; } if (visual != nullptr && - dvz_visual_set_alpha_mode(visual, DVZ_ALPHA_OPAQUE) != DVZ_OK) + dvz_visual_set_alpha_mode(visual, DVZ_ALPHA_BLENDED) != DVZ_OK) throw std::runtime_error("failed to configure Datoviz visual alpha mode"); - if (visual != nullptr && family == Visual_Family::glyph) configure_glyph_text(scene_, visual, "GLYPH"); - if (visual != nullptr && family == Visual_Family::text) configure_glyph_text(scene_, visual, "TEXT"); - if (visual != nullptr && - (family == Visual_Family::image || family == Visual_Family::glyph || - family == Visual_Family::text)) { + if (visual != nullptr && family == Visual_Family::glyph) + font = configure_glyph_text(scene_, visual, "GLYPH"); + if (visual != nullptr && family == Visual_Family::text) + font = configure_glyph_text(scene_, visual, "TEXT"); + if (visual != nullptr && family == Visual_Family::image) { constexpr std::uint32_t width = 32; constexpr std::uint32_t height = 32; std::vector> pixels(width * height); for (std::uint32_t y = 0; y < height; ++y) { for (std::uint32_t x = 0; x < width; ++x) { - const bool stroke = family == Visual_Family::text - ? (y < 6 || (x >= 13 && x <= 18)) - : ((x / 8 + y / 8) % 2 == 0); + const bool stroke = (x / 8 + y / 8) % 2 == 0; pixels[y * width + x] = stroke ? std::array{40, 235, 205, 255} : std::array{18, 42, 72, 255}; @@ -933,13 +1044,14 @@ void Datoviz_Visual_Backend::create_scene( descriptor.width = width; descriptor.height = height; descriptor.depth = 1; - auto* field = dvz_sampled_field(scene_, &descriptor); + sampled_field = dvz_sampled_field(scene_, &descriptor); auto view = dvz_field_data_view(); view.data = pixels.data(); view.bytes_per_row = width * sizeof(pixels.front()); view.rows_per_image = height; - if (field == nullptr || dvz_sampled_field_set_data(field, &view) != DVZ_OK || - dvz_visual_set_field(visual, "field", field) != DVZ_OK) + if (sampled_field == nullptr || + dvz_sampled_field_set_data(sampled_field, &view) != DVZ_OK || + dvz_visual_set_field(visual, "field", sampled_field) != DVZ_OK) throw std::runtime_error("failed to create Datoviz 2D sampled field"); } if (visual != nullptr && family == Visual_Family::labels) { @@ -955,7 +1067,7 @@ void Datoviz_Visual_Backend::create_scene( descriptor.width = width; descriptor.height = height; descriptor.depth = 1; - auto* field = dvz_sampled_field(scene_, &descriptor); + sampled_field = dvz_sampled_field(scene_, &descriptor); auto view = dvz_field_data_view(); view.data = labels.data(); view.bytes_per_row = width * sizeof(labels.front()); @@ -971,9 +1083,9 @@ void Datoviz_Visual_Backend::create_scene( {.category_id = 3, .order = 3, .label = "south east", .color = {245, 210, 55, 255}}, } }; - if (field == nullptr || scale == nullptr || - dvz_sampled_field_set_data(field, &view) != DVZ_OK || - dvz_visual_set_field(visual, "field", field) != DVZ_OK || + if (sampled_field == nullptr || scale == nullptr || + dvz_sampled_field_set_data(sampled_field, &view) != DVZ_OK || + dvz_visual_set_field(visual, "field", sampled_field) != DVZ_OK || dvz_scale_set_categories(scale, categories.data(), static_cast(categories.size())) != DVZ_OK || dvz_visual_set_scale(visual, "labels", scale) != DVZ_OK) @@ -981,7 +1093,7 @@ void Datoviz_Visual_Backend::create_scene( } if (visual != nullptr && family == Visual_Family::volume) { constexpr std::uint32_t side = 16; - std::vector voxels(side * side * side); + std::vector voxels(side * side * side); for (std::uint32_t z = 0; z < side; ++z) { for (std::uint32_t y = 0; y < side; ++y) { for (std::uint32_t x = 0; x < side; ++x) { @@ -990,25 +1102,26 @@ void Datoviz_Visual_Backend::create_scene( const float dz = static_cast(z) - 7.5F; const float distance = std::sqrt(dx * dx + dy * dy + dz * dz); voxels[(z * side + y) * side + x] = - distance < 6.5F ? static_cast(255 - distance * 22) : 0; + distance < 6.5F ? 1.0F - distance / 6.5F : 0.0F; } } } auto descriptor = dvz_sampled_field_desc(); descriptor.dim = DVZ_FIELD_DIM_3D; - descriptor.format = DVZ_FIELD_FORMAT_R8_UNORM; + descriptor.format = DVZ_FIELD_FORMAT_R32_FLOAT; descriptor.semantic = DVZ_FIELD_SEMANTIC_SCALAR; descriptor.color_role = DVZ_COLOR_ROLE_DATA; descriptor.width = side; descriptor.height = side; descriptor.depth = side; - auto* field = dvz_sampled_field(scene_, &descriptor); + sampled_field = dvz_sampled_field(scene_, &descriptor); auto view = dvz_field_data_view(); view.data = voxels.data(); - view.bytes_per_row = side; + view.bytes_per_row = side * sizeof(voxels.front()); view.rows_per_image = side; - if (field == nullptr || dvz_sampled_field_set_data(field, &view) != DVZ_OK || - dvz_visual_set_field(visual, "field", field) != DVZ_OK || + if (sampled_field == nullptr || + dvz_sampled_field_set_data(sampled_field, &view) != DVZ_OK || + dvz_visual_set_field(visual, "field", sampled_field) != DVZ_OK || dvz_volume_set_render_mode(visual, DVZ_VOLUME_RENDER_MIP) != DVZ_OK || dvz_volume_set_step_count(visual, 48) != DVZ_OK) throw std::runtime_error("failed to create Datoviz volume field"); @@ -1035,7 +1148,17 @@ void Datoviz_Visual_Backend::create_scene( dvz_text_set_style(coordinate_text, &style) != DVZ_OK) throw std::runtime_error("failed to create Datoviz marker coordinate labels"); } - visuals_.push_back({registration.identity, family, visual, coordinate_text, 0}); + visuals_.push_back( + {registration.identity, family, visual, sampled_field, font, + coordinate_text, + family == Visual_Family::volume + ? std::array{16, 16, 16} + : family == Visual_Family::labels + ? std::array{8, 8, 1} + : family == Visual_Family::image + ? std::array{32, 32, 1} + : std::array{}, + 0}); } if (wants_item_interaction) { item_interaction_ = dvz_item_interaction(panel_, nullptr); @@ -1351,8 +1474,9 @@ void Datoviz_Visual_Backend::ensure_external_attributes( break; case Visual_Family::vector: expected_count = 4; break; - case Visual_Family::primitive: - case Visual_Family::mesh: expected_count = 3; + case Visual_Family::primitive: expected_count = 3; + break; + case Visual_Family::mesh: expected_count = 4; break; case Visual_Family::path: expected_count = 3; break; @@ -1434,10 +1558,14 @@ void Datoviz_Visual_Backend::ensure_external_attributes( create("color", sizeof(Prepared_Color)); create("stroke_width_px", sizeof(float)); break; - case Visual_Family::primitive: + case Visual_Family::primitive: create("position", sizeof(Prepared_Position)); + create("color", sizeof(Prepared_Color)); + create("normal", sizeof(Prepared_Position)); + break; case Visual_Family::mesh: create("position", sizeof(Prepared_Position)); create("color", sizeof(Prepared_Color)); create("normal", sizeof(Prepared_Position)); + create("texcoords", sizeof(std::array)); break; case Visual_Family::path: create("position", sizeof(Prepared_Position)); create("color", sizeof(Prepared_Color)); @@ -1451,6 +1579,7 @@ void Datoviz_Visual_Backend::ensure_external_attributes( "external attribute layout requested for an unsupported Visual family"); } target.attributes = std::move(attributes); + target.uploaded_data_revisions = {}; } void Datoviz_Visual_Backend::upload_external_attributes( Visual_Instance& target, const Prepared_Visual& visual, @@ -1533,6 +1662,7 @@ void Datoviz_Visual_Backend::upload_external_attributes( upload(*attribute++, data.positions.data(), data.positions.size()); upload(*attribute++, data.colors.data(), data.colors.size()); upload(*attribute++, data.normals.data(), data.normals.size()); + upload(*attribute++, data.texture_coordinates.data(), data.texture_coordinates.size()); break; } case Visual_Family::path: { @@ -1689,7 +1819,12 @@ void Datoviz_Visual_Backend::apply_visual( */ ensure_external_attributes(target, point); if (bind_target) bind_external_attributes(target, target_index, count); - upload_external_attributes(target, point, target_index); + if (target.uploaded_data_revisions[target_index] != + point.data_revision) { + upload_external_attributes(target, point, target_index); + target.uploaded_data_revisions[target_index] = + point.data_revision; + } if (structure_changed && dvz_visual_set_visible(visual, point.visible) != DVZ_OK) throw std::runtime_error( @@ -1698,6 +1833,11 @@ void Datoviz_Visual_Backend::apply_visual( target.applied = point; return; } + if (target.applied && target.applied->data_revision == point.data_revision) { + target.applied_revision = point.revision; + target.applied = point; + return; + } DvzResult result = DVZ_OK; switch (target.family) { case Visual_Family::point: { @@ -1803,14 +1943,15 @@ void Datoviz_Visual_Backend::apply_visual( } case Visual_Family::mesh: { const auto& data = prepared_data(point); - const std::array updates{ + const std::array updates{ { {"position", data.positions.data(), count}, {"color", data.colors.data(), count}, - {"normal", data.normals.data(), count} + {"normal", data.normals.data(), count}, + {"texcoords", data.texture_coordinates.data(), count} } }; - result = dvz_visual_set_data_many(visual, updates.data(), 3); + result = dvz_visual_set_data_many(visual, updates.data(), 4); break; } case Visual_Family::path: { @@ -1827,29 +1968,101 @@ void Datoviz_Visual_Backend::apply_visual( } case Visual_Family::image: { const auto& data = prepared_data(point); - const std::array updates{ + if (target.field == nullptr) + throw std::logic_error("Datoviz image has no sampled field"); + auto view = dvz_field_data_view(); + view.data = data.field_pixels.data(); + view.bytes_per_row = static_cast(data.field_width) * + sizeof(data.field_pixels.front()); + view.rows_per_image = data.field_height; + const std::array extent{data.field_width, data.field_height, 1U}; + result = extent == target.field_extent + ? dvz_sampled_field_set_data(target.field, &view) + : dvz_sampled_field_resize( + target.field, extent[0], extent[1], extent[2], &view); + if (result != DVZ_OK) break; + target.field_extent = extent; + const std::array updates{ { {"position", data.positions.data(), count}, - {"extent", data.extents.data(), count} + {"extent", data.extents.data(), count}, + {"tex_rect", data.texture_rectangles.data(), count} } }; - result = dvz_visual_set_data_many(visual, updates.data(), 2); + result = dvz_visual_set_data_many(visual, updates.data(), 3); break; } case Visual_Family::labels: { const auto& data = prepared_data(point); - const std::array updates{ + if (target.field == nullptr) + throw std::logic_error("Datoviz labels visual has no sampled field"); + auto view = dvz_field_data_view(); + view.data = data.field_labels.data(); + view.bytes_per_row = static_cast(data.field_width) * + sizeof(data.field_labels.front()); + view.rows_per_image = data.field_height; + const std::array extent{data.field_width, data.field_height, 1U}; + result = extent == target.field_extent + ? dvz_sampled_field_set_data(target.field, &view) + : dvz_sampled_field_resize( + target.field, extent[0], extent[1], extent[2], &view); + if (result != DVZ_OK) break; + target.field_extent = extent; + const std::array updates{ { {"position", data.positions.data(), count}, - {"extent", data.extents.data(), count} + {"extent", data.extents.data(), count}, + {"tex_rect", data.texture_rectangles.data(), count} } }; - result = dvz_visual_set_data_many(visual, updates.data(), 2); + result = dvz_visual_set_data_many(visual, updates.data(), 3); + break; + } + case Visual_Family::glyph: { + const auto& data = prepared_data(point); + const std::array updates{{ + {"position", data.positions.data(), count}, + {"bounds", data.bounds.data(), count}, + {"texcoords", data.texture_coordinates.data(), count}, + {"color", data.colors.data(), count}, + {"angle", data.angles.data(), count} + }}; + result = dvz_visual_set_data_many(visual, updates.data(), + static_cast(updates.size())); + break; + } + case Visual_Family::text: { + upload_text(visual, target.font, + prepared_data(point)); + break; + } + case Visual_Family::volume: { + const auto& data = prepared_data(point); + if (data.field_width == 0 || data.field_height == 0 || + data.field_depth == 0) + throw std::invalid_argument( + "3D volume field dimensions must be non-zero"); + const auto expected = static_cast(data.field_width) * + data.field_height * data.field_depth; + if (expected != data.values.size()) + throw std::invalid_argument( + "3D volume voxel count does not match field dimensions"); + if (target.field == nullptr) + throw std::logic_error("Datoviz volume has no sampled field"); + auto view = dvz_field_data_view(); + view.data = data.values.data(); + view.bytes_per_row = static_cast(data.field_width) * + sizeof(data.values.front()); + view.rows_per_image = data.field_height; + const std::array extent{ + data.field_width, data.field_height, data.field_depth}; + result = extent == target.field_extent + ? dvz_sampled_field_set_data(target.field, &view) + : dvz_sampled_field_resize( + target.field, extent[0], extent[1], extent[2], &view); + if (result == DVZ_OK) target.field_extent = extent; break; } - case Visual_Family::glyph: - case Visual_Family::text: - case Visual_Family::volume: break; } if (result != DVZ_OK || dvz_visual_set_visible(visual, point.visible) != DVZ_OK) @@ -2020,9 +2233,14 @@ std::optional Datoviz_Visual_Backend::reu if (!has_payload(source.visual)) throw std::logic_error( "recorded Datoviz Visual has no immutable payload"); - if (prepared_item_count(source.visual) != 0) + if (prepared_item_count(source.visual) != 0 && + visual->uploaded_data_revisions[target_index] != + source.visual.data_revision) { 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 = source.visual; } diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp index 8a878f0..ed7aaf1 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp @@ -74,8 +74,12 @@ private: Visual_Identity identity{}; /* Scene 注册的稳定身份。 */ Visual_Family family{Visual_Family::point}; /* 原生 Visual 的确定 family。 */ DvzVisual* visual{}; /* 由 Datoviz Scene 拥有。 */ + DvzSampledField* field{}; /* 仅纹理和体数据 family 使用的 Scene 字段资源。 */ + DvzFont* font{}; /* Glyph/Text 使用的 Scene 字体与可增长 atlas 来源。 */ DvzText* coordinate_text{}; /* Marker 的数据坐标 XYZ 标注。 */ + std::array field_extent{}; /* field 当前样本尺寸。 */ std::uint64_t applied_revision{}; /* 此 Visual 已上传的 Prepare 版本。 */ + std::array uploaded_data_revisions{}; /* 各物理帧区已写入的数据版本。 */ std::optional applied{}; /* Last applied metadata and immutable payload snapshot. */ std::vector attributes{}; /* Dynamic payload buffers, one triple region per field. */ }; @@ -140,5 +144,3 @@ private: bool quarantined_{}; /* GPU 已提交后失效时,整套资源停止销毁与复用。 */ }; } // namespace aethera::render_3d::detail - - diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp index a1ec043..b99fc20 100644 --- a/render_3D/render_3D/detail/Render_Domain.cpp +++ b/render_3D/render_3D/detail/Render_Domain.cpp @@ -34,23 +34,22 @@ std::shared_ptr Render_Domain::acquire(std::uint32_t gpu_index) { Render_Domain::Render_Domain() : thread_([this] { run(); }) {} Render_Domain::~Render_Domain() { request_stop(); - if (thread_.joinable()) thread_.join(); + if (thread_.joinable()) thread_.detach(); } void Render_Domain::destroy(Render_Domain* domain) noexcept { if (!domain) return; - if (current_domain_ != domain) { - delete domain; - return; - } - domain->stopping_.store(true, std::memory_order_release); + /* + * 最后一个外部引用只关闭准入,不等待线程。run() 会消费完停止边界前已经 + * 入队的命令,再由域线程 detach 并 delete 自身;因此调用方析构路径没有 + * join、自旋或同步栅栏,Datoviz/Vulkan 对象也始终在其唯一写者线程销毁。 + */ + domain->request_stop(); domain->destroy_on_exit_.store(true, std::memory_order_release); } void Render_Domain::request_stop() noexcept { bool expected = false; if (!stopping_.compare_exchange_strong( expected, true, std::memory_order_acq_rel)) return; - while (active_posters_.load(std::memory_order_acquire) != 0) - std::this_thread::yield(); } Render_Domain::Post_Result Render_Domain::post( std::function function, Exception_Handler on_exception) { diff --git a/render_3D/render_3D/detail/Render_Domain.hpp b/render_3D/render_3D/detail/Render_Domain.hpp index 1b3ed2e..de764ad 100644 --- a/render_3D/render_3D/detail/Render_Domain.hpp +++ b/render_3D/render_3D/detail/Render_Domain.hpp @@ -5,13 +5,9 @@ #include #include #include -#include #include -#include #include -#include #include -#include namespace aethera::render_3d::detail { struct Render_Domain final : public std::enable_shared_from_this { struct Task { @@ -30,19 +26,6 @@ public: /* 把闭包所有权无等待转移到所属 GPU 的单消费者线程。 */ [[nodiscard]] Post_Result post(std::function function, Exception_Handler on_exception); - template - struct Invoke_Result { - using Value = std::conditional_t, std::monostate, Result>; - std::optional value; /* 成功执行后的返回值;停止时为空。 */ - Post_Result result{Post_Result::queued}; /* 本次同步调用是否进入 Render Domain。 */ - [[nodiscard]] explicit operator bool() const noexcept; - }; - /* - * 从 Kernel Taskflow Worker 投递域任务;等待期间调用 Worker 通过 - * Task_Graph::corun_until 继续执行其他可运行节点。禁止从普通线程调用。 - */ - template - [[nodiscard]] auto cooperative_invoke(Function&& function) -> Invoke_Result>; private: Render_Domain(); static void destroy(Render_Domain* domain) noexcept; @@ -57,4 +40,3 @@ private: std::thread thread_; /* 唯一允许访问 Datoviz/Vulkan 对象的线程。 */ }; } -#include "Render_Domain.ipp" diff --git a/render_3D/render_3D/detail/Render_Domain.ipp b/render_3D/render_3D/detail/Render_Domain.ipp deleted file mode 100644 index 5606965..0000000 --- a/render_3D/render_3D/detail/Render_Domain.ipp +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -#include -namespace aethera::render_3d::detail { -template -Render_Domain::Invoke_Result::operator bool() const noexcept { - return result == Post_Result::queued; -} -template -auto Render_Domain::cooperative_invoke(Function&& function) -> Invoke_Result> { - using Result = std::invoke_result_t; - using Value = typename Invoke_Result::Value; - Invoke_Result output; - if (current_domain_ == this) { - if constexpr (std::is_void_v) { - std::invoke(std::forward(function)); - output.value.emplace(); - } - else output.value.emplace(std::invoke(std::forward(function))); - return output; - } - auto promise = std::make_shared>(); - auto future = promise->get_future(); - auto callable = std::make_shared>( - std::forward(function)); - output.result = post( - [promise, callable] { - if constexpr (std::is_void_v) { - std::invoke(*callable); - promise->set_value(std::monostate{}); - } - else promise->set_value(std::invoke(*callable)); - }, - [promise](std::exception_ptr exception) { - promise->set_exception(std::move(exception)); - }); - if (output.result != Post_Result::queued) return output; - /* - * corun_until 不占住当前 Worker:Datoviz 域串行执行 function 时, - * Worker 留在同一 Executor 的 work-stealing 循环。谓词 acquire 读取 - * future 的 ready 状态,完成后的 get() 只取值或搬运 Unknown Failure。 - */ - Task_Graph::corun_until([&future] { - return future.wait_for(std::chrono::seconds(0)) == - std::future_status::ready; - }); - output.value.emplace(future.get()); - return output; -} -} diff --git a/render_3D/render_3D/scene/Render_Scene_3D.cpp b/render_3D/render_3D/scene/Render_Scene_3D.cpp index d4f1f82..5980023 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.cpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.cpp @@ -8,6 +8,11 @@ Render_Scene_3D::Render_Result Render_Scene_3D::render(Frame_3D* frame) { void Render_Scene_3D::set_frame_callback(Frame_Callback callback) { double_buffer::detail::Internal_Access::get(this).set_frame_callback(this, std::move(callback)); } +void Render_Scene_3D::set_submitted_frame_callback( + Submitted_Frame_Callback callback) { + double_buffer::detail::Internal_Access::get(this) + .set_submitted_frame_callback(this, std::move(callback)); +} void Render_Scene_3D::activate_view() { double_buffer::detail::Internal_Access::get(this).set_view_active(this, true); } diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp index 3c8b197..c0b886d 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -50,14 +50,17 @@ struct Render_Scene_3D : Def { std::uint32_t gpu_index{}; /* Datoviz 使用的物理 GPU 下标。 */ bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */ }; - enum struct Render_Result { submitted, frame_in_flight, view_inactive, empty_viewport, backend_unavailable }; + enum struct Render_Result { submitted, frame_pipeline_busy, view_inactive, empty_viewport, backend_unavailable }; using Frame_Callback = std::function; + using Submitted_Frame_Callback = std::function; using Render_Result_Type = Render_Result; using Render_Frame_Type = Frame_3D; [[nodiscard]] Render_Result render(Frame_3D* frame); void set_frame_callback(Frame_Callback callback); - /* 无等待提交到调用方拥有的帧;必须先安装回调,回调返回前只允许一帧在途。 */ - /* 安装完成帧回调;回调传回同一外部帧,返回时 Scene 才释放下一帧准入。 */ + void set_submitted_frame_callback(Submitted_Frame_Callback callback); + /* 无等待提交到调用方拥有的帧;最多三个帧槽,资源能力决定提交或完成释放。 */ + /* GPU 实际提交并释放下一次 Prepare 时调用;bool 表示是否与 GPU 重叠。 */ + /* 安装完成帧回调;completion graph 收口后传回同一外部帧。 */ /* * 返回 GPU 完成并回读像素后、帧回调前执行的直接 Taskflow。 * 只能在 Scene 没有运行时修改该图;禁止在执行期间 emplace/erase/clear。 diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index 836b85b..67243c1 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -1,7 +1,9 @@ #pragma once #include "../detail/Async_Render_Backend.hpp" #include -#include +#include +#include +#include #include #include #include @@ -12,26 +14,59 @@ template struct Scene_Paint_Context { std::shared_ptr backend{}; /* Scene 拥有、异步命令延长生命周期的后端。 */ Object* scene{}; /* 仅在 Scene 拥有本上下文期间读取当前 Prop。 */ - Frame_3D* frame{}; /* 当前 Submit 图对应的外部帧;process 返回后清空。 */ - Scene_3D_Parameters parameters{}; /* 本帧从 Scene 组件读取的一致快照。 */ - std::shared_ptr visuals{ /* Plot 准备域拥有的 Visual 快照;提交只共享不可变版本。 */ + Scene_3D_Parameters parameters{}; /* 当前 Prepare 读取的 Scene 参数。 */ + std::shared_ptr visuals{ /* 当前 Prepare 发布的数据句柄集合。 */ std::make_shared()}; }; } struct Render_Scene_3D::Private : Prev_Private { - std::mutex render_mutex{}; /* 只保护完成回调和单帧准入。 */ - bool frame_in_flight{}; /* render 准入到异步完成回调返回的唯一状态源。 */ - Frame_Callback frame_callback{}; /* Scene 的唯一对外完成出口。 */ + enum struct Frame_Phase : std::uint8_t { + available, + preparing, + submitted, + completing + }; + struct Frame_Context { + std::atomic phase{Frame_Phase::available}; + Frame_3D* frame{}; + Event_Batch events{}; + bool trace_started{}; + bool overlaps_gpu{}; + std::atomic_bool cpu_finished{}; + std::atomic_bool gpu_submitted{}; + std::atomic_bool gpu_completed{}; + std::exception_ptr failure{}; + }; + /* + * 3D 核心线程模型: + * + * 1. Scene::advance、Visual Prepare 和共享 paint_context 始终只有一个 + * 写者;prepare_active 是这段 CPU 发布域的唯一准入状态。 + * 2. 三个 Frame_Context 分别绑定调用方的三个物理 Frame。CPU 图完成且 + * Vulkan 已实际提交后,拥有逐目标独立资源的 Visual 立即释放下一次 + * Prepare,使 CPU(N+1)、GPU(N) 和读回(N-1) 能够重叠。 + * 3. Image/Text/Volume 等仍共享 Datoviz Scene 资源的 Visual 不在提交点 + * 提前释放,而在 GPU 退休后释放;这是一条明确能力边界,不是 fallback。 + * 4. GPU fence 回调只发布 gpu_completed 事实。completion_busy 选择一个 + * 已完成帧执行 completion_graph;该图结束后才归还槽。因此 Scene 和 + * Renderable State、Plot 发布及用户 callback 始终保持唯一完成写者。 + * 5. 全路径不等待 GPU、不复制组件状态、不创建帧外统计副本。帧身份、 + * 像素、Trace 和完成事实都留在其 Frame_Context/Frame 本身。 + */ + static constexpr std::size_t frame_capacity{3}; + std::array frame_contexts{}; + std::atomic_bool prepare_active{}; + std::atomic_bool completion_busy{}; + std::atomic> frame_callback{}; + std::atomic> submitted_callback{}; std::shared_ptr backend{}; /* Scene 拥有的异步后端;已入队命令自行延长实现寿命。 */ std::shared_ptr paint_context{}; /* Paint 节点读取 Scene 当前 Prop 的生命周期门闩。 */ Root* camera_component{}; /* Builder 绑定的 Camera 组件。 */ Root* axes_component{}; /* Builder 绑定的三轴组件。 */ Camera_Descriptor (*read_camera)(const Root*){}; /* 读取 Camera 当前配置。 */ std::array (*read_axes)(const Root*){}; /* 读取三轴当前配置。 */ - Frame_3D* active_frame{}; /* 当前同步 process 借用的外部帧;提交完成后清空。 */ + Frame_Context* active_context{}; /* 只由唯一 CPU Prepare 图访问。 */ std::unique_ptr frame_taskflow{}; - Event_Batch active_events{}; - bool active_trace{}; /* Builder 内部初始化后端;必须在绑定 Visual Paint 目标之前调用一次。 */ template void initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, @@ -39,12 +74,17 @@ struct Render_Scene_3D::Private : Prev_Private { Camera_Descriptor (*camera_reader)(const Root*), std::array (*axes_reader)(const Root*)); template [[nodiscard]] detail::Scene_3D_Parameters parameters(Object* object) const; - template void complete_frame(Object* object, Frame_3D* frame); + [[nodiscard]] Frame_Context* context_for(Frame_3D* frame) noexcept; + template void try_release_prepare(Object* object, Frame_Context* context); + template void arm_completion(Object* object); + template void consume_completion(Object* object); + template void complete_frame(Object* object, Frame_Context* context); /* CRTP 覆盖:绑定 Scene 机制和 Render_Scene_3D 公开薄壳。 */ template void bind_private_crtp(Object* object); template void ensure_frame_taskflow(Object* object); template [[nodiscard]] Render_Result render(Object* object, Frame_3D* frame); template void set_frame_callback(Object* object, Frame_Callback callback); + template void set_submitted_frame_callback(Object* object, Submitted_Frame_Callback callback); template void set_view_active(Object* object, bool active); template void reset_diagnostics(Object* object); }; @@ -155,30 +195,115 @@ detail::Scene_3D_Parameters Render_Scene_3D::Private::parameters(Object* object) return {prop.viewport, prop.clear_color, read_camera(camera_component), axis[0], axis[1], axis[2]}; } template -void Render_Scene_3D::Private::complete_frame(Object* object, Frame_3D* frame) { - Frame_Callback callback; - { - std::lock_guard lock(render_mutex); - callback = frame_callback; - } - aethera::detail::finish_taskflow_trace(*frame); - active_trace = false; +void Render_Scene_3D::Private::complete_frame( + Object*, Frame_Context* context) { + auto* frame = context->frame; + const auto callback = frame_callback.load(std::memory_order_acquire); frame->mark(Frame_Trace_Marker::callback_started); - if (callback) callback(frame); + if (callback && *callback) (*callback)(frame); frame->mark(Frame_Trace_Marker::callback_finished); frame->mark(Frame_Trace_Marker::frame_ready); - double_buffer::detail::Internal_Access::publish_state(object, - [this, frame](State_Access states) { - auto& state = states.template get(); - state.event_statistics = event_statistics.state(); - }); - auto context = std::static_pointer_cast< - detail::Scene_Paint_Context>(paint_context); - if (context->frame == frame) context->frame = nullptr; - if (active_frame == frame) active_frame = nullptr; - std::lock_guard lock(render_mutex); - frame_in_flight = false; + if (context->trace_started) + aethera::detail::finish_taskflow_trace(*frame); + context->frame = nullptr; + context->events.clear(); + context->failure = {}; + context->trace_started = false; + context->overlaps_gpu = false; + context->cpu_finished.store(false, std::memory_order_relaxed); + context->gpu_submitted.store(false, std::memory_order_relaxed); + context->gpu_completed.store(false, std::memory_order_relaxed); + context->phase.store(Frame_Phase::available, std::memory_order_release); +} + +inline Render_Scene_3D::Private::Frame_Context* +Render_Scene_3D::Private::context_for(Frame_3D* frame) noexcept { + const auto found = std::ranges::find(frame_contexts, frame, + &Frame_Context::frame); + return found == frame_contexts.end() ? nullptr : &*found; +} + +template +void Render_Scene_3D::Private::try_release_prepare( + Object* object, Frame_Context* context) { + if (!context->cpu_finished.load(std::memory_order_acquire) || + !context->gpu_submitted.load(std::memory_order_acquire) || + (!context->overlaps_gpu && + !context->gpu_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 (const auto callback = + submitted_callback.load(std::memory_order_acquire); + callback && *callback) + (*callback)(context->frame, context->overlaps_gpu); + if (context->gpu_completed.load(std::memory_order_acquire)) + arm_completion(object); +} + +template +void Render_Scene_3D::Private::arm_completion(Object* object) { + bool expected{}; + if (!completion_busy.compare_exchange_strong( + expected, true, std::memory_order_acq_rel, + std::memory_order_acquire)) + return; + aethera::schedule_task("render_3d.frame.retire", [this, object] { + consume_completion(object); + }); +} + +template +void Render_Scene_3D::Private::consume_completion(Object* object) { + Frame_Context* selected{}; + for (auto& context : frame_contexts) { + if (context.phase.load(std::memory_order_acquire) != + Frame_Phase::submitted || + !context.gpu_completed.load(std::memory_order_acquire)) + continue; + if (!selected || context.frame->identity().sequence < + selected->frame->identity().sequence) + selected = &context; + } + if (!selected) { + completion_busy.store(false, std::memory_order_release); + if (std::ranges::any_of(frame_contexts, [](const auto& context) { + return context.phase.load(std::memory_order_acquire) == + Frame_Phase::submitted && + context.gpu_completed.load(std::memory_order_acquire); + })) + arm_completion(object); + 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)) { + completion_busy.store(false, std::memory_order_release); + arm_completion(object); + return; + } + auto* frame = selected->frame; + frame->mark(Frame_Trace_Marker::paint_finished); + frame->mark(Frame_Trace_Marker::scene_render_finished); + auto done = [this, object, selected] { + complete_frame(object, selected); + completion_busy.store(false, std::memory_order_release); + arm_completion(object); + }; + if (completion_graph.empty()) { + done(); + return; + } + if (frame->taskflow_trace_requested()) + aethera::detail::run_taskflow( + completion_graph, *frame, "render_3d.completion", std::move(done)); + else + aethera::detail::run_taskflow(completion_graph, std::move(done)); } template void Render_Scene_3D::Private::initialize_backend( @@ -199,7 +324,7 @@ void Render_Scene_3D::Private::initialize_backend( std::move(visuals), initial); paint_context = std::make_shared>( detail::Scene_Paint_Context{ - backend, object, nullptr, initial, + backend, object, initial, std::move(prepared_visuals)}); } template @@ -213,12 +338,13 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { graph.describe("owner_kind", "scene") .describe("owner_component", "scene"); auto begin = graph.add("scene.begin", [this, object] { - if (!active_frame) + if (!active_context || !active_context->frame) throw std::logic_error("3D frame DAG lost its active frame"); - active_frame->mark(Frame_Trace_Marker::scene_render_started); - active_frame->mark(Frame_Trace_Marker::event_dispatch_started); - active_events = take_events(object); - active_frame->mark(Frame_Trace_Marker::event_dispatch_finished); + auto* frame = active_context->frame; + frame->mark(Frame_Trace_Marker::scene_render_started); + frame->mark(Frame_Trace_Marker::event_dispatch_started); + active_context->events = take_events(object); + frame->mark(Frame_Trace_Marker::event_dispatch_finished); auto context = std::static_pointer_cast< detail::Scene_Paint_Context>(paint_context); @@ -234,12 +360,11 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { if (context->visuals.use_count() != 1) context->visuals = std::make_shared(*context->visuals); - context->frame = active_frame; camera_component->advance_object(); axes_component->advance_object(); - active_frame->mark(Frame_Trace_Marker::prepare_started); - active_frame->mark(Frame_Trace_Marker::prepare_finished); - active_frame->mark(Frame_Trace_Marker::paint_started); + frame->mark(Frame_Trace_Marker::prepare_started); + frame->mark(Frame_Trace_Marker::prepare_finished); + frame->mark(Frame_Trace_Marker::paint_started); }); begin.describe("dimension", "3D").describe("stage", "frame setup"); @@ -249,6 +374,9 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { .describe("execution_domain", "Taskflow workers"); auto submit = graph.add("scene.backend.submit", [this, object] { + auto* execution = active_context; + if (!execution || !execution->frame) + throw std::logic_error("3D submit lost its frame context"); auto context = std::static_pointer_cast< detail::Scene_Paint_Context>(paint_context); if (!detail::prepared_visual_batch_complete(*context->visuals)) @@ -258,42 +386,87 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { double_buffer::detail::Internal_Access::pending_state(object)); state.event_statistics = event_statistics.state(); context->backend->render( - context->visuals, context->parameters, active_frame, - std::move(active_events), [this](Frame_3D* completed) { - if (completed != active_frame) + context->visuals, context->parameters, execution->frame, + std::move(execution->events), + [this, object](Frame_3D* submitted, bool overlaps_gpu) { + auto* frame_context = context_for(submitted); + if (!frame_context) throw std::logic_error( - "3D backend completed a different Scene frame"); - completed->mark(Frame_Trace_Marker::paint_finished); - completed->mark(Frame_Trace_Marker::scene_render_finished); + "3D backend submitted an unowned Scene frame"); + frame_context->overlaps_gpu = overlaps_gpu; + frame_context->gpu_submitted.store( + true, std::memory_order_release); + try_release_prepare(object, frame_context); + }, + [this, object](Frame_3D* completed, + std::exception_ptr failure) { + auto* frame_context = context_for(completed); + if (!frame_context) + throw std::logic_error( + "3D backend completed an unowned Scene frame"); + frame_context->failure = std::move(failure); + if (frame_context->failure && + !frame_context->gpu_submitted.load( + std::memory_order_acquire)) { + frame_context->overlaps_gpu = false; + frame_context->gpu_submitted.store( + true, std::memory_order_release); + } + frame_context->gpu_completed.store( + true, std::memory_order_release); + try_release_prepare(object, frame_context); + if (frame_context->phase.load(std::memory_order_acquire) == + Frame_Phase::submitted) + arm_completion(object); }); }); submit.describe("dimension", "3D") .describe("backend", "Datoviz") .describe("stage", "backend submission"); - auto completion = graph.compose("scene.completion", completion_graph); - completion.describe("owner", "scene") - .describe("stage", "frame consumers"); - begin.precede(renderables); renderables.precede(submit); - submit.precede(completion); } template Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, Frame_3D* frame) { if (!frame) throw std::invalid_argument("Render_Scene_3D requires a non-null external frame"); frame->mark(Frame_Trace_Marker::scene_render_entered); - { - std::lock_guard lock(render_mutex); - if (!frame_callback) - throw std::logic_error("Render_Scene_3D requires a frame callback before render"); - if (frame_in_flight) return Render_Result::frame_in_flight; - frame_in_flight = true; - active_frame = frame; + const auto callback = frame_callback.load(std::memory_order_acquire); + if (!callback || !*callback) + throw std::logic_error( + "Render_Scene_3D requires a frame callback before render"); + 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* execution{}; + for (auto& context : frame_contexts) { + auto expected = Frame_Phase::available; + if (context.phase.compare_exchange_strong( + expected, Frame_Phase::preparing, + std::memory_order_acq_rel, std::memory_order_acquire)) { + execution = &context; + break; + } } - const auto reject_frame = [this](Render_Result result) { - active_frame = nullptr; - std::lock_guard lock(render_mutex); - frame_in_flight = false; + if (!execution) { + prepare_active.store(false, std::memory_order_release); + return Render_Result::frame_pipeline_busy; + } + execution->frame = frame; + execution->events.clear(); + execution->failure = {}; + execution->trace_started = false; + execution->overlaps_gpu = false; + execution->cpu_finished.store(false, std::memory_order_relaxed); + execution->gpu_submitted.store(false, std::memory_order_relaxed); + execution->gpu_completed.store(false, std::memory_order_relaxed); + active_context = execution; + const auto reject_frame = [this, execution](Render_Result result) { + active_context = nullptr; + execution->frame = nullptr; + execution->phase.store(Frame_Phase::available, std::memory_order_release); + prepare_active.store(false, std::memory_order_release); return result; }; try { @@ -318,16 +491,18 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, throw; } frame->mark(Frame_Trace_Marker::scene_render_requested); - active_trace = aethera::detail::begin_taskflow_trace(*frame); + execution->trace_started = aethera::detail::begin_taskflow_trace(*frame); try { - auto completion = [this, object, frame] { + auto completion = [this, object, execution] { double_buffer::detail::Internal_Access:: current_dependency_graph(object).for_each_bound( [](Renderable* renderable, Renderable::Private& data) { data.publish_renderable_state(renderable); }); double_buffer::detail::Internal_Access::publish_state(object); - complete_frame(object, frame); + active_context = nullptr; + execution->cpu_finished.store(true, std::memory_order_release); + try_release_prepare(object, execution); }; if (frame->taskflow_trace_requested()) aethera::detail::run_taskflow( @@ -336,13 +511,11 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, aethera::detail::run_taskflow(*frame_taskflow, std::move(completion)); } catch (...) { - if (active_trace) { + if (execution->trace_started) { aethera::detail::finish_taskflow_trace(*frame); - active_trace = false; + execution->trace_started = false; } - active_frame = nullptr; - std::lock_guard lock(render_mutex); - frame_in_flight = false; + reject_frame(Render_Result::backend_unavailable); throw; } return Render_Result::submitted; @@ -358,8 +531,20 @@ void Render_Scene_3D::Private::reset_diagnostics(Object* object) { } template void Render_Scene_3D::Private::set_frame_callback(Object*, Frame_Callback callback) { - std::lock_guard lock(render_mutex); - frame_callback = std::move(callback); + frame_callback.store( + callback ? std::make_shared(std::move(callback)) + : std::shared_ptr{}, + std::memory_order_release); +} +template +void Render_Scene_3D::Private::set_submitted_frame_callback( + Object*, Submitted_Frame_Callback callback) { + submitted_callback.store( + callback + ? std::make_shared( + std::move(callback)) + : std::shared_ptr{}, + std::memory_order_release); } template void Render_Scene_3D::Private::set_view_active(Object* object, bool active) { diff --git a/render_3D/render_3D/visual/Basic_Visual.ipp b/render_3D/render_3D/visual/Basic_Visual.ipp index 69f1562..4326d11 100644 --- a/render_3D/render_3D/visual/Basic_Visual.ipp +++ b/render_3D/render_3D/visual/Basic_Visual.ipp @@ -1,6 +1,7 @@ #pragma once #include #include +#include namespace aethera::render_3d { template struct Basic_Visual::Private : Basic_Visual::Prev_Private { @@ -12,6 +13,10 @@ struct Basic_Visual::Private : Basic_Visual::Prev_Private { Paint_Target paint_target{}; /* Builder 绑定的所属 Scene 异步 Paint 目标。 */ Prepared_Visual prepared{}; /* 最近一次 Prepare 生成的唯一提交产物。 */ std::uint64_t next_revision{1}; /* 下一份 Prepared_Visual 的单调版本。 */ + std::uint64_t next_data_revision{1}; /* 下一份实际数据转换结果的版本。 */ + static constexpr std::uint8_t state_change{1U}; + static constexpr std::uint8_t data_change{2U}; + std::uint8_t pending_changes{data_change}; /* 单写者推进域内尚未由 Prepare 消费的变更域。 */ /* Scene Builder 调用:绑定只入队的异步 Paint 目标,目标生命周期必须覆盖 Visual。 */ void bind_paint_target(std::shared_ptr context, Enqueue_Paint enqueue); /* CRTP 数据能力:从当前 Prop 构建后端字段,不访问其他缓冲角色。 */ @@ -46,15 +51,56 @@ void Basic_Visual::Private::prepare_data(Object* object) { using Visual_Tag = typename Basic_Visual::Base_Tag; const auto& prop = this->template read_current_prop(object); if (!valid_items(prop.items) || !detail::finite(prop.transform)) throw std::invalid_argument("3D visual property contains invalid data"); - auto data = std::make_shared(); - Spec::prepare(prop.items, *data); - if constexpr (requires { data->style = prop.style; }) data->style = prop.style; + const auto changes = std::exchange(pending_changes, 0); + auto data = prepared.data; + auto data_revision = prepared.data_revision; + if ((changes & data_change) != 0 || !data) { + auto updated = std::make_shared(); + Spec::prepare(prop.items, *updated); + if constexpr (requires { updated->style = prop.style; }) updated->style = prop.style; + if constexpr (requires { updated->field_width = prop.field_width; }) { + updated->field_width = prop.field_width; + updated->field_height = prop.field_height; + } + if constexpr (requires { updated->field_pixels; prop.field_pixels; }) { + updated->field_pixels.reserve(prop.field_pixels.size()); + for (const auto pixel : prop.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 = prop.field_labels; }) { + updated->field_labels = prop.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 = prop.field_depth; }) + updated->field_depth = prop.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++; + } auto& output = prepared; output = {}; output.transform = prop.transform; output.visible = prop.visible; output.depth_test = prop.depth_test; output.revision = next_revision++; + output.data_revision = data_revision; output.data = std::move(data); double_buffer::detail::Internal_Access::update_state<&State::item_count, &State::prepared_item_count, &State::prepared_revision>(object, [&](State_Access states) { auto& state = states.template get(); @@ -84,8 +130,15 @@ bool Basic_Visual::Private::should_render(Object*, const State&, bool) { } template template -void Basic_Visual::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { - if constexpr (std::same_as) +void Basic_Visual::Private::after_prop_set(Object* object, Member Owner::* member, Prop_Access) { + if constexpr (std::same_as) { + bool state_only{}; + if constexpr (std::same_as) + state_only = member == &Prop::transform; + else if constexpr (std::same_as) + state_only = member == &Prop::visible || member == &Prop::depth_test; + pending_changes |= state_only ? state_change : data_change; double_buffer::detail::Internal_Access::mark_dirty(object); + } } } diff --git a/render_3D/render_3D/visual/Prepared_Visual.hpp b/render_3D/render_3D/visual/Prepared_Visual.hpp index a118ad6..af4a2ff 100644 --- a/render_3D/render_3D/visual/Prepared_Visual.hpp +++ b/render_3D/render_3D/visual/Prepared_Visual.hpp @@ -12,6 +12,7 @@ struct Prepared_Visual_For { bool visible{true}; bool depth_test{true}; std::uint64_t revision{}; + std::uint64_t data_revision{}; std::shared_ptr data{}; }; } diff --git a/render_3D/render_3D/visual/Visuals.hpp b/render_3D/render_3D/visual/Visuals.hpp index a99e60c..30a847a 100644 --- a/render_3D/render_3D/visual/Visuals.hpp +++ b/render_3D/render_3D/visual/Visuals.hpp @@ -72,14 +72,12 @@ struct Image { Vec3 position{}; /* 图像中心 Scene 坐标。 */ Vec2 extent{1.0F, 1.0F}; /* Scene 坐标尺寸;两个分量必须为正数。 */ Vec4 texture_rectangle{0.0F, 0.0F, 1.0F, 1.0F}; /* 纹理采样矩形。 */ - Color tint{Color::white()}; /* 图像颜色调制。 */ bool operator==(const Image&) const = default; }; struct Label { Vec3 position{}; /* 标签中心 Scene 坐标。 */ - std::uint32_t image_index{}; /* 纹理数组图像下标。 */ Vec2 extent{32.0F, 16.0F}; /* 标签尺寸,单位为像素;两个分量必须为正数。 */ - Color tint{Color::white()}; /* 标签颜色调制。 */ + Vec4 texture_rectangle{0.0F, 0.0F, 1.0F, 1.0F}; /* 分类字段采样矩形。 */ bool operator==(const Label&) const = default; }; struct Glyph { @@ -157,6 +155,7 @@ struct Mesh_Prepared_Data { std::vector positions{}; std::vector colors{}; std::vector normals{}; + std::vector> texture_coordinates{}; }; struct Path_Prepared_Data { std::vector positions{}; @@ -166,25 +165,37 @@ struct Path_Prepared_Data { struct Image_Prepared_Data { std::vector positions{}; std::vector> extents{}; - std::vector colors{}; + std::vector> texture_rectangles{}; + std::vector field_pixels{}; + std::uint32_t field_width{}; + std::uint32_t field_height{}; }; struct Labels_Prepared_Data { std::vector positions{}; std::vector> extents{}; - std::vector colors{}; + std::vector> texture_rectangles{}; + std::vector field_labels{}; + std::uint32_t field_width{}; + std::uint32_t field_height{}; }; struct Glyph_Prepared_Data { std::vector positions{}; std::vector colors{}; std::vector angles{}; + std::vector> bounds{}; + std::vector> texture_coordinates{}; }; struct Text_Prepared_Data { std::vector positions{}; std::vector colors{}; std::vector sizes{}; + std::vector strings{}; }; struct Volume_Prepared_Data { std::vector values{}; + std::uint32_t field_width{}; + std::uint32_t field_height{}; + std::uint32_t field_depth{}; }; template @@ -222,7 +233,7 @@ struct Vector_Spec : Visual_Spec&, Prepared_Data&); }; -struct Primitive_Spec : Visual_Spec { +struct Primitive_Spec : Visual_Spec { [[nodiscard]] static bool valid(const Item&) noexcept; static void prepare(const std::vector&, Prepared_Data&); }; @@ -234,15 +245,15 @@ struct Path_Spec : Visual_Spec&, Prepared_Data&); }; -struct Image_Spec : Visual_Spec { +struct Image_Spec : Visual_Spec { [[nodiscard]] static bool valid(const Item&) noexcept; static void prepare(const std::vector&, Prepared_Data&); }; -struct Labels_Spec : Visual_Spec { +struct Labels_Spec : Visual_Spec { [[nodiscard]] static bool valid(const Item&) noexcept; static void prepare(const std::vector&, Prepared_Data&); }; -struct Glyph_Spec : Visual_Spec { +struct Glyph_Spec : Visual_Spec { [[nodiscard]] static bool valid(const Item&) noexcept; static void prepare(const std::vector&, Prepared_Data&); }; diff --git a/render_3D/render_3D/visual/Visuals.ipp b/render_3D/render_3D/visual/Visuals.ipp index 643a921..944ddd7 100644 --- a/render_3D/render_3D/visual/Visuals.ipp +++ b/render_3D/render_3D/visual/Visuals.ipp @@ -25,17 +25,17 @@ inline void Vector_Spec::prepare(const std::vector& items, Prepared_Data& inline bool Primitive_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.normal); } inline void Primitive_Spec::prepare(const std::vector& items, Prepared_Data& output) { reserve_position_color(output, items.size()); output.normals.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.normals.push_back({item.normal.x, item.normal.y, item.normal.z}); } } inline bool Mesh_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.normal) && finite(item.texture_coordinate); } -inline void Mesh_Spec::prepare(const std::vector& items, Prepared_Data& output) { reserve_position_color(output, items.size()); output.normals.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.normals.push_back({item.normal.x, item.normal.y, item.normal.z}); } } +inline void Mesh_Spec::prepare(const std::vector& items, Prepared_Data& output) { reserve_position_color(output, items.size()); output.normals.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.normals.push_back({item.normal.x, item.normal.y, item.normal.z}); output.texture_coordinates.push_back({item.texture_coordinate.x, item.texture_coordinate.y}); } } inline bool Path_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.width_px) && item.width_px > 0.0F; } inline void Path_Spec::prepare(const std::vector& items, Prepared_Data& output) { reserve_position_color(output, items.size()); output.widths.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.widths.push_back(item.width_px); } } 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); } -inline void Image_Spec::prepare(const std::vector& items, Prepared_Data& output) { reserve_position_color(output, items.size()); output.extents.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.colors.push_back(channels(item.tint)); } } -inline bool Labels_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.extent) && item.extent.x > 0.0F && item.extent.y > 0.0F; } -inline void Labels_Spec::prepare(const std::vector& items, Prepared_Data& output) { reserve_position_color(output, items.size()); output.extents.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.colors.push_back(channels(item.tint)); } } +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}); } } +inline bool Labels_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); } +inline void Labels_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}); } } inline bool Glyph_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.bounds) && finite(item.texture_coordinates) && finite(item.angle); } -inline void Glyph_Spec::prepare(const std::vector& items, Prepared_Data& output) { reserve_position_color(output, items.size()); output.angles.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); } } +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}); } } inline bool Text_Spec::valid(const Item& item) noexcept { return finite(item.position) && !item.text.empty() && finite(item.size_px) && item.size_px > 0.0F; } -inline void Text_Spec::prepare(const std::vector& items, Prepared_Data& output) { reserve_position_color(output, items.size()); output.sizes.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.sizes.push_back(item.size_px); } } +inline void Text_Spec::prepare(const std::vector& items, Prepared_Data& output) { reserve_position_color(output, items.size()); output.sizes.reserve(items.size()); output.strings.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.sizes.push_back(item.size_px); output.strings.push_back(item.text); } } inline bool Volume_Spec::valid(const Item& item) noexcept { return finite(item.value); } inline void Volume_Spec::prepare(const std::vector& items, Prepared_Data& output) { output.values.reserve(items.size()); for (const auto& item : items) output.values.push_back(item.value); } } diff --git a/render_3D/tests/Visual_Tests.cpp b/render_3D/tests/Visual_Tests.cpp index 9c2fd18..30aacd4 100644 --- a/render_3D/tests/Visual_Tests.cpp +++ b/render_3D/tests/Visual_Tests.cpp @@ -1,9 +1,6 @@ #include #include #include -#include -#include -#include namespace aethera::render_3d { namespace { template @@ -46,59 +43,37 @@ TEST(Render_3D_Scene, Builder_Instantiates_Async_Paint_Contract) { scene_builder.add_renderable(visual.get()).add_camera(camera.get()).add_axes(axes.get()); static_assert(std::same_as, Dependency_Graph_Error>>); } -TEST(Render_3D_Scene, Paint_Submits_Without_Waiting_For_Gpu) { - using Visual_Object = Point_Visual; - using Scene_Object = Render_Scene_3D; - auto visual_result = typename Visual_Object::template Builder{}.build(); - ASSERT_TRUE(visual_result.has_value()); - auto visual = std::move(visual_result).value(); - auto camera = *Camera_3D::Builder{}.build(); - auto axes = *Axes_3D::Builder{}.build(); - visual->set<&Point_Visual::Prop::items>(std::vector{Point{.position = {0.0F, 0.0F, 0.0F}, .color = Color::red_color(), .diameter_px = 8.0F}}); - visual->advance(); - try { - typename Scene_Object::template Builder scene_builder; - scene_builder.add_renderable(visual.get()).add_camera(camera.get()).add_axes(axes.get()); - auto scene_result = scene_builder.build(); - ASSERT_TRUE(scene_result.has_value()); - auto scene = std::move(scene_result).value(); - std::atomic_bool frame_ready{}; - Frame_3D frame{Frame_Identity{1, 0}}; - Frame_3D unchanged_frame{Frame_Identity{2, 0}}; - Frame_3D exchanged_unchanged_frame{Frame_Identity{3, 0}}; - Frame_3D* completed_frame{}; - std::atomic_size_t completion_calls{}; - scene->completion_taskflow().add("test.scene_3d.completion", [&] { - completion_calls.fetch_add(1, std::memory_order_release); - }); - scene->set_frame_callback([&](Frame_3D* value) { - EXPECT_GT(completion_calls.load(std::memory_order_acquire), 0u); - completed_frame = value; - frame_ready.store(true, std::memory_order_release); - }); - const auto started = std::chrono::steady_clock::now(); - EXPECT_EQ(scene->render(&frame), Render_Scene_3D::Render_Result::submitted); - const auto submit_duration = std::chrono::steady_clock::now() - started; - EXPECT_LT(submit_duration, std::chrono::seconds(1)); - for (std::size_t attempt = 0; attempt != 500 && !frame_ready.load(std::memory_order_acquire); ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(10)); - EXPECT_EQ(completed_frame, &frame); - EXPECT_FALSE(frame.pixels().empty()); - frame_ready.store(false, std::memory_order_release); - completed_frame = nullptr; - EXPECT_EQ(scene->render(&unchanged_frame), Render_Scene_3D::Render_Result::submitted); - for (std::size_t attempt = 0; attempt != 500 && !frame_ready.load(std::memory_order_acquire); ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(10)); - EXPECT_EQ(completed_frame, &unchanged_frame); - EXPECT_FALSE(unchanged_frame.pixels().empty()); - frame_ready.store(false, std::memory_order_release); - completed_frame = nullptr; - EXPECT_EQ(scene->render(&exchanged_unchanged_frame), Render_Scene_3D::Render_Result::submitted); - for (std::size_t attempt = 0; attempt != 500 && !frame_ready.load(std::memory_order_acquire); ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(10)); - EXPECT_EQ(completed_frame, &exchanged_unchanged_frame); - EXPECT_EQ(completion_calls.load(std::memory_order_acquire), 3u); - EXPECT_TRUE(std::ranges::equal(unchanged_frame.pixels(), exchanged_unchanged_frame.pixels())); - } - catch (const std::exception& error) { - GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); - } +TEST(Render_3D_Visual, Prepared_Data_Preserves_Datoviz_Attributes) { + detail::Mesh_Prepared_Data mesh; + detail::Mesh_Spec::prepare( + std::vector{{.texture_coordinate = {0.25F, 0.75F}}}, + mesh); + ASSERT_EQ(mesh.texture_coordinates.size(), 1U); + EXPECT_EQ(mesh.texture_coordinates.front(), + (std::array{0.25F, 0.75F})); + + detail::Image_Prepared_Data image; + detail::Image_Spec::prepare( + std::vector{{.texture_rectangle = {0.1F, 0.2F, 0.8F, 0.9F}}}, + image); + EXPECT_EQ(image.texture_rectangles.front(), + (std::array{0.1F, 0.2F, 0.8F, 0.9F})); + + detail::Glyph_Prepared_Data glyph; + detail::Glyph_Spec::prepare(std::vector{{ + .bounds = {-1.0F, -2.0F, 3.0F, 4.0F}, + .texture_coordinates = {0.0F, 0.1F, 0.9F, 1.0F}, + .angle = 0.5F}}, glyph); + EXPECT_EQ(glyph.bounds.front(), + (std::array{-1.0F, -2.0F, 3.0F, 4.0F})); + EXPECT_EQ(glyph.texture_coordinates.front(), + (std::array{0.0F, 0.1F, 0.9F, 1.0F})); + + detail::Text_Prepared_Data text; + detail::Text_Spec::prepare( + std::vector{{.text = "dynamic", .size_px = 23.0F}}, text); + ASSERT_EQ(text.strings.size(), 1U); + EXPECT_EQ(text.strings.front(), "dynamic"); + EXPECT_FLOAT_EQ(text.sizes.front(), 23.0F); } } diff --git a/web_server/src/Gallery_Plots.cpp b/web_server/src/Gallery_Plots.cpp new file mode 100644 index 0000000..d6e1200 --- /dev/null +++ b/web_server/src/Gallery_Plots.cpp @@ -0,0 +1,49 @@ +#include "Gallery_Plots.hpp" +#include +#include + +namespace aethera::web { +namespace { +constexpr std::array definitions{ + Plot_Definition{"axes", "坐标轴", "基础组件", "二维数值轴与时间轴布局。", Plot_Dimension::two_d, make_axes_plot}, + Plot_Definition{"spectrum", "频谱图", "频域分析", "实时频谱、保持曲线和标记。", Plot_Dimension::two_d, make_spectrum_plot}, + Plot_Definition{"frequency_trace", "频率轨迹", "频域分析", "按时间推进的频率轨迹。", Plot_Dimension::two_d, make_frequency_trace_plot}, + Plot_Definition{"sweep_spectrum", "扫频图", "频域分析", "分块到达的扫频数据。", Plot_Dimension::two_d, make_sweep_spectrum_plot}, + Plot_Definition{"afterglow", "余辉图", "密度分析", "带衰减历史的频率功率密度。", Plot_Dimension::two_d, make_afterglow_plot}, + Plot_Definition{"waterfall", "瀑布图", "密度分析", "分区并行绘制的频谱历史。", Plot_Dimension::two_d, make_waterfall_plot}, + Plot_Definition{"constellation", "星座图", "信号分析", "调制星座点、参考锚点与选区。", Plot_Dimension::two_d, make_constellation_plot}, + Plot_Definition{"selection_overlay", "矩形选区", "交互组件", "独立的二维矩形选择覆盖层。", Plot_Dimension::two_d, make_selection_overlay_plot}, + Plot_Definition{"datoviz_point", "三维点图", "Datoviz 图元", "Datoviz point visual。", Plot_Dimension::three_d, make_datoviz_point_plot}, + Plot_Definition{"datoviz_splat", "三维高斯 Splat", "Datoviz 图元", "Datoviz Gaussian splat visual。", Plot_Dimension::three_d, make_datoviz_splat_plot}, + Plot_Definition{"datoviz_pixel", "三维像素", "Datoviz 图元", "Datoviz pixel visual。", Plot_Dimension::three_d, make_datoviz_pixel_plot}, + Plot_Definition{"datoviz_marker", "三维标记", "Datoviz 图元", "可拾取并显示坐标的 marker visual。", Plot_Dimension::three_d, make_datoviz_marker_plot}, + Plot_Definition{"datoviz_sphere", "三维球体", "Datoviz 图元", "Datoviz sphere visual。", Plot_Dimension::three_d, make_datoviz_sphere_plot}, + Plot_Definition{"datoviz_segment", "三维线段", "Datoviz 图元", "Datoviz segment visual。", Plot_Dimension::three_d, make_datoviz_segment_plot}, + Plot_Definition{"datoviz_vector", "三维向量", "Datoviz 图元", "Datoviz vector visual。", Plot_Dimension::three_d, make_datoviz_vector_plot}, + Plot_Definition{"datoviz_primitive", "三维图元", "Datoviz 图元", "基础拓扑顶点图元。", Plot_Dimension::three_d, make_datoviz_primitive_plot}, + Plot_Definition{"datoviz_mesh", "三维网格", "Datoviz 表面", "带法线和纹理坐标的 mesh visual。", Plot_Dimension::three_d, make_datoviz_mesh_plot}, + Plot_Definition{"datoviz_spectrogram", "三维频谱瀑布", "Datoviz 表面", "动态频谱网格与峰值标记。", Plot_Dimension::three_d, make_datoviz_spectrogram_plot}, + Plot_Definition{"datoviz_path", "三维路径", "Datoviz 图元", "Datoviz path visual。", Plot_Dimension::three_d, make_datoviz_path_plot}, + Plot_Definition{"datoviz_image", "三维图像", "Datoviz 纹理", "空间中的采样图像。", Plot_Dimension::three_d, make_datoviz_image_plot}, + Plot_Definition{"datoviz_labels", "三维标签", "Datoviz 纹理", "分类字段标签 visual。", Plot_Dimension::three_d, make_datoviz_labels_plot}, + Plot_Definition{"datoviz_glyph", "三维字形", "Datoviz 文本", "可控制几何和 atlas 坐标的 glyph visual。", Plot_Dimension::three_d, make_datoviz_glyph_plot}, + Plot_Definition{"datoviz_text", "三维文本", "Datoviz 文本", "动态 UTF-8 文本标签。", Plot_Dimension::three_d, make_datoviz_text_plot}, + Plot_Definition{"datoviz_volume", "三维体数据", "Datoviz 体渲染", "动态标量体字段的最大强度投影。", Plot_Dimension::three_d, make_datoviz_volume_plot}, +}; +} + +std::span gallery_plot_definitions() noexcept { + return definitions; +} + +const Plot_Definition* find_gallery_plot_definition( + std::string_view id) noexcept { + const auto found = std::ranges::find(definitions, id, + &Plot_Definition::id); + return found == definitions.end() ? nullptr : &*found; +} + +std::string_view plot_dimension_name(Plot_Dimension dimension) noexcept { + return dimension == Plot_Dimension::three_d ? "3D" : "2D"; +} +} diff --git a/web_server/src/Gallery_Plots.hpp b/web_server/src/Gallery_Plots.hpp index 896ed83..ae0049c 100644 --- a/web_server/src/Gallery_Plots.hpp +++ b/web_server/src/Gallery_Plots.hpp @@ -1,3 +1,25 @@ #pragma once #include "Gallery_Plots_2D.hpp" #include "Gallery_Plots_3D.hpp" +#include +#include + +namespace aethera::web { +enum struct Plot_Dimension : std::uint8_t { two_d, three_d }; + +struct Plot_Definition { + std::string_view id; /* 协议和路由使用的稳定标识。 */ + std::string_view title; /* 前端直接展示的业务名称。 */ + std::string_view category; /* Catalog 分组名称。 */ + std::string_view description; /* Plot 能力说明。 */ + Plot_Dimension dimension{Plot_Dimension::two_d}; /* 渲染后端维度。 */ + std::shared_ptr (*create)(); /* 创建该 Plot 唯一实例的工厂。 */ +}; + +[[nodiscard]] std::span +gallery_plot_definitions() noexcept; +[[nodiscard]] const Plot_Definition* find_gallery_plot_definition( + std::string_view id) noexcept; +[[nodiscard]] std::string_view plot_dimension_name( + Plot_Dimension dimension) noexcept; +} diff --git a/web_server/src/Gallery_Plots_3D.cpp b/web_server/src/Gallery_Plots_3D.cpp index d5e6f65..fa624a9 100644 --- a/web_server/src/Gallery_Plots_3D.cpp +++ b/web_server/src/Gallery_Plots_3D.cpp @@ -207,7 +207,68 @@ private: std::uniform_real_distribution first{}; std::uniform_real_distribution second{}; }; -struct Random_Data_Generator {}; +struct Random_Data_Generator { + static std::uint64_t mix(std::uint64_t value) noexcept { + value += 0x9E3779B97F4A7C15ULL; + value = (value ^ (value >> 30U)) * 0xBF58476D1CE4E5B9ULL; + value = (value ^ (value >> 27U)) * 0x94D049BB133111EBULL; + return value ^ (value >> 31U); + } + static float signed_unit(std::uint64_t sequence, std::size_t index, + std::uint64_t lane) noexcept { + const auto bits = mix(sequence ^ + (static_cast(index) << 8U) ^ lane); + return static_cast(bits >> 40U) / + static_cast(1U << 24U) * 2.0F - 1.0F; + } + static void push(Vec3& value, std::uint64_t sequence, + std::size_t index, std::uint64_t lane, + float amount = 0.018F) noexcept { + value.x = std::clamp(value.x + amount * signed_unit(sequence, index, lane), + -0.96F, 0.96F); + value.y = std::clamp(value.y + amount * signed_unit(sequence, index, lane + 1U), + -0.96F, 0.96F); + value.z = std::clamp(value.z + amount * signed_unit(sequence, index, lane + 2U), + -0.96F, 0.96F); + } + template + void update(Visual_Object& visual, Axes_3D&, Marker_Visual*, + const Plot_Render_Tick& request) { + using Definition = typename Visual_Object::Attached_Object; + using Prop = typename Definition::Prop; + const auto sequence = request.sequence != 0 + ? request.sequence + : static_cast(request.time_milliseconds * 10.0); + auto items = visual.template read_prop().items; + for (std::size_t index = 0; index < items.size(); ++index) { + auto& item = items[index]; + if constexpr (requires { item.position; }) + push(item.position, sequence, index, 0U); + else if constexpr (requires { item.center; }) + push(item.center, sequence, index, 0U); + else if constexpr (requires { item.origin; }) { + push(item.origin, sequence, index, 0U); + if constexpr (requires { item.direction; }) + push(item.direction, sequence, index, 3U, 0.008F); + } + else if constexpr (requires { item.start; item.end; }) { + push(item.start, sequence, index, 0U); + push(item.end, sequence, index, 3U); + } + if constexpr (requires { item.value; }) + item.value = std::clamp( + item.value + 0.07F * signed_unit(sequence, index, 7U), + 0.0F, 1.0F); + if constexpr (std::same_as) { + if (index == 0) + item.text = "Aethera frame " + + std::to_string(sequence % 10'000U); + } + } + visual.template set<&Prop::items>(std::move(items)); + + } +}; template struct Visual_Scene_View final : public Plot::Scene_View { public: @@ -738,7 +799,6 @@ std::shared_ptr make_datoviz_vector_plot() { } std::shared_ptr make_datoviz_primitive_plot() { return make_visual_plot("Primitive Visual", Primitive_Visual::Builder{} - .set(&Primitive_Visual::Prop::topology, Primitive_Topology::triangle_list) .set(&Primitive_Visual::Prop::items, std::vector{{{-0.72F, -0.55F, 0.0F}, color(255, 86, 110), {0, 0, 1}}, {{0.72F, -0.55F, 0.0F}, color(75, 145, 255), {0, 0, 1}}, {{0.0F, 0.72F, 0.25F}, color(82, 226, 190), {0, 0, 1}}}).build()); } std::shared_ptr make_datoviz_mesh_plot() { @@ -811,18 +871,29 @@ std::shared_ptr make_datoviz_path_plot() { return make_visual_plot("Path Visual", Path_Visual::Builder{}.set(&Path_Visual::Prop::items, std::move(path)).build()); } std::shared_ptr make_datoviz_image_plot() { + std::vector pixels(32U * 32U); + for (std::uint32_t y = 0; y < 32U; ++y) + for (std::uint32_t x = 0; x < 32U; ++x) + pixels[y * 32U + x] = (x / 8U + y / 8U) % 2U == 0U + ? color(40, 235, 205) : color(18, 42, 72); return make_visual_plot("Image Visual", Image_Visual::Builder{} .set(&Image_Visual::Prop::field_width, 32U).set(&Image_Visual::Prop::field_height, 32U) - .set(&Image_Visual::Prop::items, std::vector{{{0.0F, 0.0F, 0.0F}, {1.45F, 1.0F}, {0, 0, 1, 1}, color(255, 255, 255)}}).build()); + .set(&Image_Visual::Prop::field_pixels, std::move(pixels)) + .set(&Image_Visual::Prop::items, std::vector{{{0.0F, 0.0F, 0.0F}, {1.45F, 1.0F}, {0, 0, 1, 1}}}).build()); } std::shared_ptr make_datoviz_labels_plot() { + std::vector labels(8U * 8U); + for (std::uint32_t y = 0; y < 8U; ++y) + for (std::uint32_t x = 0; x < 8U; ++x) + labels[y * 8U + x] = static_cast( + x / 4U + 2U * (y / 4U)); return make_visual_plot("Labels Visual", Labels_Visual::Builder{} .set(&Labels_Visual::Prop::field_width, 8U).set(&Labels_Visual::Prop::field_height, 8U) - .set(&Labels_Visual::Prop::items, std::vector