diff --git a/kernel/src/kernel/plot/Axis.cpp b/kernel/src/kernel/plot/Axis.cpp index ace06c7..64f127f 100644 --- a/kernel/src/kernel/plot/Axis.cpp +++ b/kernel/src/kernel/plot/Axis.cpp @@ -1,6 +1,7 @@ #include "Axis.hpp" #include +#include #include #include #include @@ -39,6 +40,71 @@ std::string numeric_label(double value, int precision) { return result; } +bool ascii_iequals(std::string_view left, std::string_view right) { + return left.size() == right.size() && + std::ranges::equal(left, right, [](char first, char second) { + return std::tolower(static_cast(first)) == + std::tolower(static_cast(second)); + }); +} + +std::string time_label(double seconds, int precision, + std::string_view unit) { + if (!unit.empty() && !ascii_iequals(unit, "s") && + !ascii_iequals(unit, "sec") && !ascii_iequals(unit, "second") && + !ascii_iequals(unit, "seconds")) + return numeric_label(seconds, precision) + " " + std::string(unit); + const auto sign = seconds < 0.0 ? "-" : ""; + const auto absolute = std::abs(seconds); + if (absolute == 0.0) return "0 s"; + if (absolute < 1.0) + return std::string(sign) + + numeric_label(absolute * 1'000.0, precision) + " ms"; + if (absolute < 60.0) + return std::string(sign) + numeric_label(absolute, precision) + " s"; + + const auto hours = static_cast(absolute / 3'600.0); + const auto minutes = static_cast(absolute / 60.0) % 60U; + const auto remaining = absolute - + static_cast(hours * 3'600U + minutes * 60U); + std::ostringstream stream; + stream << sign; + if (hours != 0U) stream << hours << ':' << std::setfill('0') << std::setw(2); + stream << minutes << ':' << std::setfill('0') + << std::setw(precision > 0 ? precision + 3 : 2) + << std::fixed << std::setprecision(std::clamp(precision, 0, 6)) + << remaining; + return stream.str(); +} + +std::string frequency_label(double frequency, int precision) { + const auto magnitude = std::abs(frequency); + double divisor{1.0}; + std::string_view unit{"Hz"}; + if (magnitude >= 1.0e9) { + divisor = 1.0e9; + unit = "GHz"; + } + else if (magnitude >= 1.0e6) { + divisor = 1.0e6; + unit = "MHz"; + } + else if (magnitude >= 1.0e3) { + divisor = 1.0e3; + unit = "kHz"; + } + return numeric_label(frequency / divisor, precision) + " " + + std::string(unit); +} + +std::string axis_tick_label(const Axis_Descriptor& axis, double coordinate) { + if (axis.scale == Axis_Scale::time) + return time_label(coordinate, axis.precision, axis.unit); + if (ascii_iequals(axis.unit, "hz")) + return frequency_label(coordinate, axis.precision); + return numeric_label(coordinate, axis.precision); +} + std::vector linear_ticks(const Axis_Descriptor& axis) { const auto step = nice_tick_step(axis.range, axis.target_tick_count); if (!(step > 0.0) || !std::isfinite(step)) return {}; @@ -49,7 +115,7 @@ std::vector linear_ticks(const Axis_Descriptor& axis) { for (std::size_t index = 0; index < maximum_count; ++index) { const auto coordinate = first + static_cast(index) * step; if (coordinate > maximum + step * 1.0e-9) break; - ticks.push_back({coordinate, numeric_label(coordinate, axis.precision)}); + ticks.push_back({coordinate, axis_tick_label(axis, coordinate)}); } if (axis.range.length() < 0.0) std::ranges::reverse(ticks); return ticks; @@ -73,7 +139,7 @@ std::vector logarithmic_ticks(const Axis_Descriptor& axis) { const auto coordinate = multiple * decade; if (coordinate >= minimum * (1.0 - 1.0e-12) && coordinate <= maximum * (1.0 + 1.0e-12)) - ticks.push_back({coordinate, numeric_label(coordinate, axis.precision)}); + ticks.push_back({coordinate, axis_tick_label(axis, coordinate)}); } } const auto add_boundary = [&](double coordinate) { @@ -95,7 +161,7 @@ std::vector logarithmic_ticks(const Axis_Descriptor& axis) { logarithmic_span < minimum_spacing) ticks.erase(nearest); } - ticks.push_back({coordinate, numeric_label(coordinate, axis.precision)}); + ticks.push_back({coordinate, axis_tick_label(axis, coordinate)}); } }; add_boundary(minimum); diff --git a/kernel/src/test/Axis_Formatting_Test.cpp b/kernel/src/test/Axis_Formatting_Test.cpp new file mode 100644 index 0000000..3845be6 --- /dev/null +++ b/kernel/src/test/Axis_Formatting_Test.cpp @@ -0,0 +1,52 @@ +#include +#include + +#include +#include +#include + +namespace aethera::plot { +namespace { + +TEST(Axis_Formatting, Time_Scale_Uses_Elapsed_Time_Labels) { + const Axis_Descriptor axis{ + {0.0, 4.0}, Axis_Scale::time, "Time", "s", + 5, 1, true, true, true}; + + const auto ticks = axis_ticks(axis); + ASSERT_EQ(ticks.size(), 5U); + EXPECT_EQ(ticks.front().label, "0 s"); + EXPECT_EQ(ticks[1].label, "1 s"); + EXPECT_EQ(ticks.back().label, "4 s"); +} + +TEST(Axis_Formatting, Time_Scale_Formats_Long_Durations) { + const Axis_Descriptor axis{ + {0.0, 4'000.0}, Axis_Scale::time, "Elapsed", "s", + 5, 0, true, true, true}; + + const auto ticks = axis_ticks(axis); + ASSERT_FALSE(ticks.empty()); + EXPECT_EQ(ticks.front().label, "0 s"); + EXPECT_EQ(ticks.back().label, "1:06:40"); +} + +TEST(Axis_Formatting, Hertz_Unit_Uses_Frequency_Prefixes) { + const Axis_Descriptor axis{ + {10.0, 20'000.0}, Axis_Scale::logarithmic, "Frequency", "Hz", + 5, 0, true, true, true}; + + const auto ticks = axis_ticks(axis); + const std::vector labels = [&] { + std::vector result; + result.reserve(ticks.size()); + for (const auto& tick : ticks) result.push_back(tick.label); + return result; + }(); + EXPECT_NE(std::ranges::find(labels, "10 Hz"), labels.end()); + EXPECT_NE(std::ranges::find(labels, "1 kHz"), labels.end()); + EXPECT_NE(std::ranges::find(labels, "20 kHz"), labels.end()); +} + +} // namespace +} // namespace aethera::plot diff --git a/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp b/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp new file mode 100644 index 0000000..42f335d --- /dev/null +++ b/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +namespace aethera::render_3d { + +enum struct Datoviz_Frame_Path : std::uint8_t { + recorded, + reused +}; + +struct Datoviz_Gpu_Timing { + std::uint64_t render_ns{}; /* GPU 绘制阶段耗时,单位为纳秒。 */ + std::uint64_t transition_ns{}; /* GPU 布局转换耗时,单位为纳秒。 */ + std::uint64_t copy_ns{}; /* GPU 读回复制耗时,单位为纳秒。 */ + std::uint64_t total_ns{}; /* GPU 整帧总耗时,单位为纳秒。 */ +}; + +/* + * Datoviz 的逐帧诊断直接属于产生它的 Frame_3D。它随物理帧槽 begin() 清空, + * 后端完成后一次写入,调用方只在完成边界读取;这里没有旁路历史或状态快照。 + */ +struct Datoviz_Frame_Observation { + std::uint64_t render_sequence{}; /* Scene 分配的帧序号。 */ + Datoviz_Frame_Path path{Datoviz_Frame_Path::recorded}; /* 本帧录制命令或复用既有命令。 */ + bool gpu_timing_requested{}; /* 本帧是否读取已录制的 GPU 时间戳。 */ + bool readback_requested{}; /* 本帧是否请求最终 RGBA 读回。 */ + std::uint64_t render_domain_queue_wait_ns{}; /* 唯一 GPU 提交域排队耗时。 */ + std::uint64_t apply_ns{}; /* 应用本帧 Visual 数据耗时。 */ + std::uint64_t emit_ns{}; /* 生成 Datoviz 帧计划耗时。 */ + std::uint64_t execute_ns{}; /* 执行 Datoviz 帧计划耗时。 */ + std::uint64_t submit_ns{}; /* Vulkan 队列提交耗时。 */ + std::uint64_t gpu_fence_wait_ns{}; /* 从 watch 到 fence 完成的墙钟耗时。 */ + std::uint64_t readback_ns{}; /* 已完成 GPU 目标的 CPU 读回耗时。 */ + std::uint64_t uploaded_bytes{}; /* 本帧写入映射属性存储的有效字节数。 */ + std::uint64_t readback_bytes{}; /* 本帧产出的连续像素字节数。 */ + std::optional gpu{}; /* 支持时间戳时的 GPU 分段数据。 */ + std::uint64_t artifact_resource_version{}; /* Datoviz 帧计划资源版本。 */ + std::uint64_t artifact_frame_index{}; /* Datoviz 帧计划内部帧下标。 */ + std::uint32_t artifact_status{}; /* Datoviz 帧计划状态码。 */ + std::uint32_t validation_code{}; /* 帧计划验证结果码。 */ + std::uint64_t validation_command_index{}; /* 首个无效命令下标。 */ + bool validation_performed{}; /* 本帧是否执行了帧计划验证。 */ + bool validation_ok{}; /* 已执行验证时,帧计划是否有效。 */ + std::string artifact_json{}; /* 验证失败时的可选 Datoviz 帧计划 JSON。 */ +}; + +} // namespace aethera::render_3d diff --git a/render_3D/render_3D/base/Frame_3D.cpp b/render_3D/render_3D/base/Frame_3D.cpp index b8ecd3b..7bfc7b4 100644 --- a/render_3D/render_3D/base/Frame_3D.cpp +++ b/render_3D/render_3D/base/Frame_3D.cpp @@ -1,5 +1,6 @@ #include "Frame_3D.hpp" #include +#include namespace aethera::render_3d { struct Frame_3D::Private { Pixel_Format output_format{Frame_3D::native_pixel_format}; @@ -7,6 +8,7 @@ struct Frame_3D::Private { std::shared_ptr> pixels{}; /* 合并提交共享的不可变 RGBA8 像素。 */ Frame_3D_Output output{Frame_3D_Output::pixels}; /* 调用方要求的最终帧输出。 */ Frame_Identity rendered_identity{}; /* 产生本像素的真实 GPU 提交帧身份。 */ + std::optional datoviz_observation{}; /* 与本逻辑帧同寿命的 Datoviz 逐帧诊断。 */ }; Frame_3D::Frame_3D(Frame_Identity identity, Frame_3D_Output output, Pixel_Format output_format) @@ -24,6 +26,7 @@ void Frame_3D::begin(Frame_Identity identity, Frame_3D_Output output, d->extent = {}; d->pixels.reset(); d->rendered_identity = {}; + d->datoviz_observation.reset(); } Frame_3D::~Frame_3D() = default; Extent Frame_3D::extent() const noexcept { return d->extent; } @@ -39,6 +42,10 @@ Frame_Identity Frame_3D::rendered_identity() const noexcept { } Frame_3D_Output Frame_3D::output() const noexcept { return d->output; } Pixel_Format Frame_3D::output_format() const noexcept { return d->output_format; } +std::optional +Frame_3D::take_datoviz_observation() { + return std::exchange(d->datoviz_observation, {}); +} void detail::Frame_3D_Access::assign_pixels( std::span frames, Extent extent, std::vector pixels, Frame_Identity rendered_identity) { @@ -62,4 +69,11 @@ void detail::Frame_3D_Access::share_pixels(Frame_3D* source, Frame_3D* target) { ? source->d->pixels : nullptr; } +void detail::Frame_3D_Access::assign_datoviz_observation( + Frame_3D* frame, Datoviz_Frame_Observation observation) { + if (!frame) + throw std::invalid_argument( + "Render_Scene_3D requires a non-null observation frame"); + frame->d->datoviz_observation.emplace(std::move(observation)); +} } diff --git a/render_3D/render_3D/base/Frame_3D.hpp b/render_3D/render_3D/base/Frame_3D.hpp index 4eadfcd..ef7aea0 100644 --- a/render_3D/render_3D/base/Frame_3D.hpp +++ b/render_3D/render_3D/base/Frame_3D.hpp @@ -1,8 +1,10 @@ #pragma once +#include "Datoviz_Frame_Observation.hpp" #include "Types.hpp" #include #include #include +#include #include #include namespace aethera::render_3d { @@ -17,6 +19,8 @@ struct Frame_3D_Access { std::vector pixels, Frame_Identity rendered_identity); static void share_pixels(Frame_3D* source, Frame_3D* target); + static void assign_datoviz_observation( + Frame_3D* frame, Datoviz_Frame_Observation observation); }; } struct Frame_3D final : public Render_Frame { @@ -39,6 +43,8 @@ public: [[nodiscard]] Frame_Identity rendered_identity() const noexcept; [[nodiscard]] Frame_3D_Output output() const noexcept; [[nodiscard]] Pixel_Format output_format() const noexcept; + [[nodiscard]] std::optional + take_datoviz_observation(); private: struct Private; std::unique_ptr d; /* 本帧最终三维读回结果的唯一所有权。 */ diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp index d333f0a..627884f 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -16,8 +17,6 @@ #include namespace aethera::render_3d::detail { namespace { -constexpr std::uint64_t backend_observation_period{100}; - float wheel_step(double pixel, double angle) { if (angle != 0.0) return static_cast(angle / 120.0); return static_cast(pixel / 100.0); @@ -28,23 +27,38 @@ Mouse_Button held_button(Mouse_Button_Mask buttons) { if ((buttons & 4U) != 0) return Mouse_Button::middle; return Mouse_Button::none; } -void record_datoviz_trace(Frame_3D* frame, const Datoviz_Frame_Trace& trace) { - if (trace.apply_ns) - frame->record(Frame_Trace_Measurement::backend_apply_ns, trace.apply_ns); - if (trace.emit_ns) - frame->record(Frame_Trace_Measurement::backend_plan_ns, trace.emit_ns); - if (trace.execute_ns) - frame->record(Frame_Trace_Measurement::backend_execute_ns, trace.execute_ns); - if (trace.submit_ns) - frame->record(Frame_Trace_Measurement::backend_submit_ns, trace.submit_ns); - if (trace.readback_ns) - frame->record(Frame_Trace_Measurement::readback_ns, trace.readback_ns); - if (!trace.gpu) return; - frame->record(Frame_Trace_Measurement::gpu_render_ns, trace.gpu->render_ns); - frame->record(Frame_Trace_Measurement::gpu_transition_ns, - trace.gpu->transition_ns); - frame->record(Frame_Trace_Measurement::gpu_copy_ns, trace.gpu->copy_ns); - frame->record(Frame_Trace_Measurement::gpu_total_ns, trace.gpu->total_ns); +void publish_datoviz_observation( + Frame_3D* frame, Datoviz_Frame_Observation observation) { + if (observation.apply_ns) + frame->record(Frame_Trace_Measurement::backend_apply_ns, + observation.apply_ns); + if (observation.emit_ns) + frame->record(Frame_Trace_Measurement::backend_plan_ns, + observation.emit_ns); + if (observation.execute_ns) + frame->record(Frame_Trace_Measurement::backend_execute_ns, + observation.execute_ns); + if (observation.submit_ns) + frame->record(Frame_Trace_Measurement::backend_submit_ns, + observation.submit_ns); + if (observation.readback_ns) + frame->record(Frame_Trace_Measurement::readback_ns, + observation.readback_ns); + if (observation.gpu_fence_wait_ns) + frame->record(Frame_Trace_Measurement::gpu_fence_wait_ns, + observation.gpu_fence_wait_ns); + if (observation.gpu) { + frame->record(Frame_Trace_Measurement::gpu_render_ns, + observation.gpu->render_ns); + frame->record(Frame_Trace_Measurement::gpu_transition_ns, + observation.gpu->transition_ns); + frame->record(Frame_Trace_Measurement::gpu_copy_ns, + observation.gpu->copy_ns); + frame->record(Frame_Trace_Measurement::gpu_total_ns, + observation.gpu->total_ns); + } + Frame_3D_Access::assign_datoviz_observation( + frame, std::move(observation)); } void append_exception_description(const std::exception& error, std::string& result) { @@ -91,6 +105,8 @@ struct Async_Render_Backend::Implementation Async_Render_Backend::Submitted submitted_callback{}; Async_Render_Backend::Completed completed_callback{}; bool prepared{}; + std::chrono::steady_clock::time_point domain_queued_at{}; /* 进入 GPU 单写者队列的时刻。 */ + std::uint64_t domain_queue_wait_ns{}; /* Render Domain 消费入口一次写入。 */ std::atomic_bool submitted{}; /* 区分提交前取消与 GPU 已拥有资源后的终止。 */ std::atomic_bool completion_enqueued{}; /* reservation 与提交失败只允许一个退休事实。 */ }; @@ -108,9 +124,7 @@ struct Async_Render_Backend::Implementation bool validation_enabled_value, std::vector visuals, 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)) { + : render_domain(Render_Domain::acquire(gpu_index_value)) { overlaps_gpu = std::ranges::all_of(visuals, [](const auto& visual) { switch (visual.family) { case Visual_Family::point: @@ -133,6 +147,14 @@ struct Async_Render_Backend::Implementation } return false; }); + auto built = Datoviz_Visual_Backend::Builder< + Datoviz_Visual_Backend>{ + gpu_index_value, validation_enabled_value, + std::move(visuals), initial_scene}.build(); + if (!built) + throw std::logic_error( + "Datoviz backend dependency graph is invalid"); + backend = std::move(built).value(); } ~Implementation(); @@ -144,8 +166,12 @@ struct Async_Render_Backend::Implementation Async_Render_Backend::Submitted submitted_callback, Async_Render_Backend::Completed completed_callback); void queue_prepare(Submission submission, std::shared_ptr pending); + void queue_submit(std::shared_ptr pending); void prepare_and_submit(Submission submission, std::shared_ptr pending); + void submit_prepared(std::shared_ptr pending); + void mark_domain_queued(const std::shared_ptr& pending); + void mark_domain_entered(const std::shared_ptr& pending); void queue_finish(std::shared_ptr pending, std::optional result, std::exception_ptr failure); @@ -207,8 +233,7 @@ void Async_Render_Backend::Implementation::render( }, [self, pending](std::exception_ptr value) { self->queue_finish(pending, {}, std::move(value)); - }, frame->identity().sequence == 1 || - frame->identity().sequence % backend_observation_period == 0); + }, frame->taskflow_trace_requested()); if (!reservation) { if (reservation.result == Gpu_Completion_Service::Admission_Result::capacity_exhausted) @@ -216,16 +241,58 @@ void Async_Render_Backend::Implementation::render( throw std::runtime_error("GPU completion service is unavailable"); } pending->completion_reservation.emplace(std::move(reservation.reservation)); - queue_prepare(Submission{std::move(visuals), std::move(parameters), - std::move(events)}, std::move(pending)); + Submission submission{std::move(visuals), std::move(parameters), + std::move(events)}; + const auto sequence = frame->identity().sequence; + const bool observe = frame->taskflow_trace_requested(); + const bool readback = frame->output() == Frame_3D_Output::pixels; + if (submission.events.empty()) { + frame->mark(Frame_Trace_Marker::backend_prepare_started); + try { + if (auto prepared = backend->try_prepare_reused( + submission.parameters, *submission.visuals, sequence, + observe, readback)) { + pending->backend_frame = std::move(*prepared); + pending->prepared = true; + frame->mark(Frame_Trace_Marker::backend_prepare_finished); + queue_submit(std::move(pending)); + return; + } + } + catch (...) { + auto failure_value = std::current_exception(); + fail(failure_value); + pending->completion_reservation.reset(); + pending->completed_callback(pending->output, + std::move(failure_value)); + return; + } + } + queue_prepare(std::move(submission), std::move(pending)); +} +void Async_Render_Backend::Implementation::mark_domain_queued( + const std::shared_ptr& pending) { + pending->domain_queued_at = std::chrono::steady_clock::now(); + pending->output->mark(Frame_Trace_Marker::backend_queue_entered); +} +void Async_Render_Backend::Implementation::mark_domain_entered( + const std::shared_ptr& pending) { + const auto entered = std::chrono::steady_clock::now(); + pending->domain_queue_wait_ns = static_cast( + std::max(0, std::chrono::duration_cast< + std::chrono::nanoseconds>( + entered - pending->domain_queued_at).count())); + pending->output->mark(Frame_Trace_Marker::backend_queue_left); } void Async_Render_Backend::Implementation::queue_prepare( Submission submission, std::shared_ptr pending) { auto self = shared_from_this(); Render_Domain::Post_Result queued{}; try { + mark_domain_queued(pending); queued = render_domain->post( [self, submission = std::move(submission), pending]() mutable { + self->mark_domain_entered(pending); self->prepare_and_submit(std::move(submission), pending); }, [self, pending](std::exception_ptr value) { @@ -244,6 +311,29 @@ void Async_Render_Backend::Implementation::queue_prepare( pending->completion_reservation.reset(); pending->completed_callback(pending->output, std::move(failure_value)); } +void Async_Render_Backend::Implementation::queue_submit( + std::shared_ptr pending) { + auto self = shared_from_this(); + Render_Domain::Post_Result queued{}; + try { + mark_domain_queued(pending); + queued = render_domain->post( + [self, pending] { + self->mark_domain_entered(pending); + self->submit_prepared(pending); + }, + [self, pending](std::exception_ptr value) { + self->queue_finish(pending, {}, std::move(value)); + }); + } + catch (...) { + queue_finish(pending, {}, std::current_exception()); + return; + } + if (queued == Render_Domain::Post_Result::queued) return; + queue_finish(pending, {}, std::make_exception_ptr(std::runtime_error( + "render domain stopped before 3D frame submission"))); +} void Async_Render_Backend::Implementation::prepare_and_submit( Submission submission, std::shared_ptr pending) { std::optional prepared; @@ -255,12 +345,11 @@ void Async_Render_Backend::Implementation::prepare_and_submit( 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; + const bool observe = pending->output->taskflow_trace_requested(); /* - * Datoviz 对同一 GPU context 的 apply/emit/submit/collect 只有 Render Domain - * 这一位写者。本任务已经位于该域,直接完成 Prepare 后提交,不把中间结果 - * 往返给 Scene worker,也不为资源串行化再增加第二把锁。 + * 只有结构 Prepare 才到达这里。Datoviz 对同一 GPU context 的 Scene/runtime + * 修改与 VkQueue submit 保持 Render Domain 单写者;稳定命令的映射上传已经在 + * 调用 worker 完成,fence 后 collect 也不会回到本域阻塞后续提交。 */ prepared = backend->prepare(submission.parameters, *submission.visuals, sequence, observe, readback); @@ -268,13 +357,18 @@ void Async_Render_Backend::Implementation::prepare_and_submit( throw std::logic_error( "Datoviz did not provide a frame target after Scene admission"); } + prepared->observation.render_domain_queue_wait_ns = + pending->domain_queue_wait_ns; pending->output->mark(Frame_Trace_Marker::backend_prepare_finished); - record_datoviz_trace(pending->output, prepared->trace); - pending->output->mark(Frame_Trace_Marker::backend_queue_entered); - pending->output->mark(Frame_Trace_Marker::backend_submit_queued); pending->backend_frame = std::move(*prepared); pending->prepared = true; - pending->output->mark(Frame_Trace_Marker::backend_queue_left); + submit_prepared(std::move(pending)); +} +void Async_Render_Backend::Implementation::submit_prepared( + std::shared_ptr pending) { + pending->backend_frame.observation.render_domain_queue_wait_ns = + pending->domain_queue_wait_ns; + pending->output->mark(Frame_Trace_Marker::backend_submit_queued); backend->submit(pending->backend_frame); pending->submitted.store(true, std::memory_order_release); pending->output->mark(Frame_Trace_Marker::gpu_submitted); @@ -290,9 +384,11 @@ void Async_Render_Backend::Implementation::queue_finish( if (pending->completion_enqueued.exchange(true, std::memory_order_acq_rel)) return; auto self = shared_from_this(); - const auto queued = render_domain->post( - [self, pending, result = std::move(result), - failure_value = std::move(failure_value)]() mutable { + try { + schedule_task( + "render_3d.backend.collect", + [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{ @@ -304,15 +400,15 @@ void Async_Render_Backend::Implementation::queue_finish( } pending->completed_callback(pending->output, std::move(completion_failure)); - }, - [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) { - auto value = std::make_exception_ptr(std::runtime_error( - "render domain stopped before 3D frame retirement")); + } + catch (...) { + auto value = std::current_exception(); fail(value); + if (pending->submitted.load(std::memory_order_acquire)) + backend->quarantine(std::move(pending->backend_frame)); + else if (pending->prepared) + backend->discard(std::move(pending->backend_frame)); pending->completed_callback(pending->output, std::move(value)); } } @@ -320,7 +416,8 @@ void Async_Render_Backend::Implementation::resolve( std::shared_ptr pending, Datoviz_Visual_Backend::Completed_Frame completed) { pending->output->mark(Frame_Trace_Marker::readback_finished); - record_datoviz_trace(pending->output, completed.trace); + publish_datoviz_observation( + pending->output, std::move(completed.observation)); std::array outputs{pending->output}; Frame_3D_Access::assign_pixels(outputs, completed.extent, std::move(completed.pixels), @@ -329,9 +426,8 @@ void Async_Render_Backend::Implementation::resolve( void Async_Render_Backend::Implementation::finish(Gpu_Completion completion) { completion.pending->output->mark(Frame_Trace_Marker::gpu_completed); if (completion.result) - completion.pending->output->record( - Frame_Trace_Measurement::gpu_fence_wait_ns, - completion.result->wait_duration_ns); + completion.pending->backend_frame.observation.gpu_fence_wait_ns = + completion.result->wait_duration_ns; completion.pending->output->mark(Frame_Trace_Marker::readback_started); if (completion.failure) { if (completion.pending->submitted.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 bce1a58..23ce36d 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.hpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.hpp @@ -17,10 +17,14 @@ public: using Submitted = std::function; using Completed = std::function; /* - * render() 只把本帧不可变输入句柄移交给 Render Domain,不等待 Prepare、 - * vkQueueSubmit 或 GPU。Datoviz Prepare、实际提交和 fence 注册由同一个 GPU - * 单写者顺序完成;submitted 在真实提交后发布资源重叠能力,completed 在该域 - * collect 完成后发布。调用方线程与 Taskflow worker 全程没有同步等待节点。 + * 3D 核心线程模型: + * 1. Scene Taskflow worker 准备不可变业务数据;已录制命令的热路径还可并行 + * 写本 Scene 独占的三缓冲映射属性区,不访问共享 Datoviz 结构或 VkQueue。 + * 2. 同一 GPU 只有 Render Domain 能修改 Datoviz Scene/runtime、录制结构命令和 + * 调用 vkQueueSubmit;这是跨 Scene 的确定单写者序列。 + * 3. GPU completion 只登记 fence;fence 已完成后的目标读回由普通 Taskflow + * worker 并行完成,因为每个目标槽和映射读回区只属于对应 Scene。 + * 4. 所有跨边界动作都是投递和回调,不引入 join、future.get 或条件变量等待。 */ void render( Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, diff --git a/render_3D/render_3D/detail/Datoviz_Frame_Trace.hpp b/render_3D/render_3D/detail/Datoviz_Frame_Trace.hpp deleted file mode 100644 index 91c9250..0000000 --- a/render_3D/render_3D/detail/Datoviz_Frame_Trace.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#include -#include -#include -namespace aethera::render_3d::detail { -struct Datoviz_Gpu_Timing { - std::uint64_t render_ns{}; /* GPU 绘制阶段耗时,单位为纳秒。 */ - std::uint64_t transition_ns{}; /* GPU 布局转换耗时,单位为纳秒。 */ - std::uint64_t copy_ns{}; /* GPU 回读复制耗时,单位为纳秒。 */ - std::uint64_t total_ns{}; /* GPU 整帧总耗时,单位为纳秒。 */ -}; -struct Datoviz_Frame_Trace { - std::uint64_t render_sequence{}; /* Scene 分配的帧序号。 */ - bool observed{}; /* 本帧是否采集诊断数据。 */ - std::uint64_t render_domain_queue_wait_ns{}; /* Render Domain 排队耗时,单位为纳秒。 */ - std::uint64_t apply_ns{}; /* 应用 Visual 数据耗时,单位为纳秒。 */ - std::uint64_t emit_ns{}; /* 生成帧计划耗时,单位为纳秒。 */ - std::uint64_t execute_ns{}; /* 执行后端命令耗时,单位为纳秒。 */ - std::uint64_t submit_ns{}; /* Vulkan 提交耗时,单位为纳秒。 */ - std::uint64_t gpu_fence_wait_ns{}; /* GPU fence 等待耗时,单位为纳秒。 */ - std::uint64_t readback_ns{}; /* 像素读回耗时,单位为纳秒。 */ - std::optional gpu; /* 后端支持时间戳时的 GPU 分段数据。 */ - std::uint64_t artifact_resource_version{}; /* Datoviz 帧计划资源版本。 */ - std::uint64_t artifact_frame_index{}; /* Datoviz 帧计划内部帧下标。 */ - std::uint32_t artifact_status{}; /* Datoviz 帧计划状态码。 */ - std::uint32_t validation_code{}; /* 帧计划验证结果码。 */ - std::uint64_t validation_command_index{}; /* 首个无效命令下标。 */ - bool validation_ok{}; /* 帧计划验证是否成功。 */ - std::string artifact_json; /* 可选帧计划诊断 JSON。 */ -}; -} // namespace aethera::render_3d::detail - - diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp index 522a3e8..3da2caf 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp @@ -438,7 +438,7 @@ void retain_quarantined_context( static_cast(new(std::nothrow) std::shared_ptr(std::move(context))); } -struct Datoviz_Visual_Backend::Frame_Target final { +struct Datoviz_Visual_Backend::Private::Frame_Target final { public: struct Collection { std::vector pixels; @@ -490,6 +490,7 @@ public: dvz_buffer_flags(readback_, DVZ_ALLOC_HOST_ACCESS_RANDOM | DVZ_ALLOC_MAPPED); dvz_buffer_usage(readback_, VK_BUFFER_USAGE_TRANSFER_DST_BIT); if (dvz_buffer_create(readback_) != 0) throw std::runtime_error("failed to create Datoviz readback buffer"); + initialize_timestamps(device, queue); } catch (...) { destroy(); @@ -506,15 +507,13 @@ public: } } [[nodiscard]] bool can_reuse( - std::uint64_t command_revision, bool observe, - bool readback) const noexcept { + std::uint64_t command_revision, bool readback) const noexcept { return available() && recorded_ && recorded_command_revision_ == command_revision && - recorded_observing_ == observe && recorded_readback_ == readback; } void reuse(std::uint64_t command_revision, bool observe, bool readback) { - if (!can_reuse(command_revision, observe, readback)) + if (!can_reuse(command_revision, readback)) throw std::logic_error( "Datoviz frame target command recording cannot be reused"); observing_ = observe; @@ -531,11 +530,6 @@ public: * finish_recording() may publish the new cache identity. */ recorded_ = false; try { - if (observing_ && !timestamps_initialized_) { - initialize_timestamps( - dvz_gpu_ctx_device(gpu_context_), - dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN)); - } dvz_cmd_reset(commands_); if (dvz_cmd_begin_result(commands_) != 0) throw std::runtime_error( @@ -584,7 +578,7 @@ public: dvz_barrier_image_mip(image_barrier, 0, 1); dvz_barrier_image_layers(image_barrier, 0, 1); dvz_cmd_barriers(commands_, &barriers); - if (observing_ && timestamps_supported_) { + if (timestamps_supported_) { const VkCommandBuffer command_buffer = dvz_commands_handle(commands_); vkCmdResetQueryPool(command_buffer, query_pool_, 0, 4); @@ -622,7 +616,7 @@ public: void finish_recording() { if (!recording_) throw std::logic_error("Datoviz frame target is not recording"); const VkCommandBuffer command_buffer = dvz_commands_handle(commands_); - if (observing_ && timestamps_supported_) { + if (timestamps_supported_) { vkCmdWriteTimestamp( command_buffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, @@ -646,7 +640,7 @@ public: dvz_barrier_image_mip(image_barrier, 0, 1); dvz_barrier_image_layers(image_barrier, 0, 1); dvz_cmd_barriers(commands_, &image_barriers); - if (observing_ && timestamps_supported_) { + if (timestamps_supported_) { vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, query_pool_, 2); } @@ -667,11 +661,11 @@ public: VK_ACCESS_2_HOST_READ_BIT); dvz_cmd_barriers(commands_, &buffer_barriers); } - else if (observing_ && timestamps_supported_) { + else if (timestamps_supported_) { vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, query_pool_, 2); } - if (observing_ && timestamps_supported_) { + if (timestamps_supported_) { vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, query_pool_, 3); @@ -682,7 +676,6 @@ public: dvz_submit(submit_); dvz_submit_command(submit_, dvz_commands_handle(commands_)); recorded_command_revision_ = recording_command_revision_; - recorded_observing_ = observing_; recorded_readback_ = readback_requested_; recorded_ = true; prepared_ = true; @@ -918,18 +911,29 @@ private: std::uint64_t recording_command_revision_{}; std::uint64_t recorded_command_revision_{}; bool recorded_{}; - bool recorded_observing_{}; bool recorded_readback_{}; bool quarantined_{}; }; -struct Datoviz_Visual_Backend::Frame_Targets { +struct Datoviz_Visual_Backend::Private::Frame_Targets { static constexpr std::size_t count = 3; std::array, count> values{}; /* 固定三槽,槽地址在后端生命周期内稳定。 */ }; -Datoviz_Visual_Backend::Datoviz_Visual_Backend( +Datoviz_Visual_Backend::Private::Private() + : targets_(std::make_unique()) {} + +Datoviz_Visual_Backend::Private::~Private() { + try { + destroy(); + } + catch (...) { + abandon_resources(); + } +} + +void Datoviz_Visual_Backend::Private::initialize( std::uint32_t gpu_index, bool validation_enabled, const std::vector& visuals, - const Scene_3D_Parameters& initial_scene) : targets_(std::make_unique()) { + const Scene_3D_Parameters& initial_scene) { try { render_context_ = Datoviz_Render_Context::acquire(gpu_index, validation_enabled); std::scoped_lock context_lock(render_context_->api_mutex(), @@ -953,15 +957,8 @@ Datoviz_Visual_Backend::Datoviz_Visual_Backend( raise_context("creating Datoviz backend", failure); } } -Datoviz_Visual_Backend::~Datoviz_Visual_Backend() noexcept { - try { - destroy(); - } - catch (...) { - abandon_resources(); - } -} -void Datoviz_Visual_Backend::create_scene( + +void Datoviz_Visual_Backend::Private::create_scene( const std::vector& registrations, const Scene_3D_Parameters& initial_scene) { if (registrations.empty()) throw std::invalid_argument("Datoviz backend requires at least one Visual registration"); @@ -1197,7 +1194,8 @@ void Datoviz_Visual_Backend::create_scene( }; dvz_input_emit_resize(input_router_, &resize); } -void Datoviz_Visual_Backend::apply_axes(const Scene_3D_Parameters& scene) { +void Datoviz_Visual_Backend::Private::apply_axes( + const Scene_3D_Parameters& scene) { const std::array descriptors{scene.x_axis, scene.y_axis, scene.z_axis}; if (applied_axes_ && *applied_axes_ == descriptors) return; using Position = std::array; @@ -1307,7 +1305,8 @@ void Datoviz_Visual_Backend::apply_axes(const Scene_3D_Parameters& scene) { throw std::runtime_error("failed to upload Datoviz 3D axes labels"); applied_axes_ = descriptors; } -void Datoviz_Visual_Backend::apply_camera(const Camera_Descriptor& source) { +void Datoviz_Visual_Backend::Private::apply_camera( + const Camera_Descriptor& source) { if (applied_camera_ && *applied_camera_ == source) return; if (camera_controller_ != nullptr) { dvz_controller_destroy(camera_controller_); @@ -1404,7 +1403,7 @@ void Datoviz_Visual_Backend::apply_camera(const Camera_Descriptor& source) { throw std::runtime_error("failed to bind Datoviz camera controller"); applied_camera_ = source; } -bool Datoviz_Visual_Backend::matches_command_structure( +bool Datoviz_Visual_Backend::Private::matches_command_structure( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared) const { if (input_changed_ || figure_extent_ != scene.viewport || @@ -1428,7 +1427,7 @@ bool Datoviz_Visual_Backend::matches_command_structure( } return true; } -void Datoviz_Visual_Backend::apply( +std::uint64_t Datoviz_Visual_Backend::Private::apply( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared, std::uint8_t target_index, bool bind_target) { if (figure_extent_ != scene.viewport) { @@ -1445,15 +1444,18 @@ void Datoviz_Visual_Backend::apply( apply_camera(scene.camera); apply_axes(scene); if (prepared.size() != visuals_.size()) throw std::logic_error("3D frame did not publish every registered Visual"); + std::uint64_t uploaded_bytes{}; for (auto& target : visuals_) { const auto found = std::ranges::find_if(prepared, [&](const Prepared_Visual_Instance& value) { return value.identity == target.identity; }); if (found == prepared.end()) throw std::logic_error("3D frame contains an unknown or missing Visual identity"); - apply_visual(target, found->visual, target_index, bind_target); + uploaded_bytes += apply_visual( + target, found->visual, target_index, bind_target); } + return uploaded_bytes; } -void Datoviz_Visual_Backend::ensure_external_attributes( +void Datoviz_Visual_Backend::Private::ensure_external_attributes( Visual_Instance& target, const Prepared_Visual& visual) { const auto item_count = prepared_item_count(visual); if (item_count == 0) return; @@ -1470,10 +1472,6 @@ void Datoviz_Visual_Backend::ensure_external_attributes( break; case Visual_Family::sphere: expected_count = 3; break; - case Visual_Family::segment: expected_count = 4; - break; - case Visual_Family::vector: expected_count = 4; - break; case Visual_Family::primitive: expected_count = 3; break; case Visual_Family::mesh: expected_count = 4; @@ -1548,16 +1546,6 @@ void Datoviz_Visual_Backend::ensure_external_attributes( create("color", sizeof(Prepared_Color)); create("radius", sizeof(float)); break; - case Visual_Family::segment: create("position_start", sizeof(Prepared_Position)); - create("position_end", sizeof(Prepared_Position)); - create("color", sizeof(Prepared_Color)); - create("stroke_width_px", sizeof(float)); - break; - case Visual_Family::vector: create("position", sizeof(Prepared_Position)); - create("vector", sizeof(Prepared_Position)); - create("color", sizeof(Prepared_Color)); - create("stroke_width_px", sizeof(float)); - break; case Visual_Family::primitive: create("position", sizeof(Prepared_Position)); create("color", sizeof(Prepared_Color)); create("normal", sizeof(Prepared_Position)); @@ -1581,11 +1569,12 @@ void Datoviz_Visual_Backend::ensure_external_attributes( target.attributes = std::move(attributes); target.uploaded_data_revisions = {}; } -void Datoviz_Visual_Backend::upload_external_attributes( +std::uint64_t Datoviz_Visual_Backend::Private::upload_external_attributes( Visual_Instance& target, const Prepared_Visual& visual, std::uint8_t target_index) { const auto item_count = prepared_item_count(visual); - if (item_count == 0) return; + if (item_count == 0) return 0; + std::uint64_t uploaded_bytes{}; const auto upload = [&](External_Attribute& attribute, const void* source, std::size_t count) { if (source == nullptr || count != item_count) throw std::logic_error("Datoviz external attribute fields have different item counts"); @@ -1593,6 +1582,7 @@ void Datoviz_Visual_Backend::upload_external_attributes( const auto byte_offset = static_cast(target_index) * attribute.capacity * attribute.stride; dvz_buffer_upload(attribute.gpu_buffer, byte_offset, byte_count, source); + uploaded_bytes += byte_count; }; auto attribute = target.attributes.begin(); switch (target.family) { @@ -1634,22 +1624,6 @@ void Datoviz_Visual_Backend::upload_external_attributes( upload(*attribute++, data.radii.data(), data.radii.size()); break; } - case Visual_Family::segment: { - const auto& data = prepared_data(visual); - upload(*attribute++, data.starts.data(), data.starts.size()); - upload(*attribute++, data.ends.data(), data.ends.size()); - upload(*attribute++, data.colors.data(), data.colors.size()); - upload(*attribute++, data.widths.data(), data.widths.size()); - break; - } - case Visual_Family::vector: { - const auto& data = prepared_data(visual); - upload(*attribute++, data.origins.data(), data.origins.size()); - upload(*attribute++, data.directions.data(), data.directions.size()); - upload(*attribute++, data.colors.data(), data.colors.size()); - upload(*attribute++, data.widths.data(), data.widths.size()); - break; - } case Visual_Family::primitive: { const auto& data = prepared_data(visual); upload(*attribute++, data.positions.data(), data.positions.size()); @@ -1687,8 +1661,9 @@ void Datoviz_Visual_Backend::upload_external_attributes( default: throw std::logic_error( "external attribute upload requested for an unsupported Visual family"); } + return uploaded_bytes; } -void Datoviz_Visual_Backend::bind_external_attributes( +void Datoviz_Visual_Backend::Private::bind_external_attributes( Visual_Instance& target, std::uint8_t target_index, std::uint32_t item_count) { for (auto& attribute : target.attributes) { @@ -1700,7 +1675,7 @@ void Datoviz_Visual_Backend::bind_external_attributes( throw std::runtime_error("failed to bind Datoviz external visual attribute"); } } -void Datoviz_Visual_Backend::register_external_attributes( +void Datoviz_Visual_Backend::Private::register_external_attributes( const DvzDrp2CommandStream* stream, std::uint8_t target_index) { if (stream == nullptr) return; if (target_index >= runtime_slots_.size()) throw std::logic_error("Datoviz external attribute target is invalid"); @@ -1730,13 +1705,13 @@ void Datoviz_Visual_Backend::register_external_attributes( } } } -void Datoviz_Visual_Backend::apply_visual( +std::uint64_t Datoviz_Visual_Backend::Private::apply_visual( Visual_Instance& target, const Prepared_Visual& point, std::uint8_t target_index, bool bind_target) { if (!has_payload(point)) throw std::logic_error("3D prepared visual has no immutable payload"); if (!uses_external_attributes(target.family) && point.revision == target.applied_revision) - return; + return 0; const auto item_count = prepared_item_count(point); auto* visual = target.visual; const bool structure_changed = !target.applied || @@ -1808,20 +1783,23 @@ void Datoviz_Visual_Backend::apply_visual( if (dvz_visual_set_visible(visual, false) != DVZ_OK) throw std::runtime_error("failed to hide empty Datoviz point visual"); target.applied_revision = point.revision; target.applied = point; - return; + return 0; } const auto count = static_cast(item_count); if (uses_external_attributes(target.family)) { /* - * 直接顶点族在第一次录制前即绑定三槽外部属性。Segment/Vector/Image/ - * Labels 仍依赖 Datoviz 的 CPU 派生几何或纹理资源,不进入这条路径; + * 普通顶点族在第一次录制前即绑定三槽外部属性。Segment/Vector 的 + * typed-stroke 元数据、Glyph/Text 的 atlas 几何以及 Image/Labels/Volume + * 的采样场资源继续使用 Datoviz 结构路径; * 把 dense 数据先写入再改绑会违反 Datoviz 的单一属性所有权契约。 */ + std::uint64_t uploaded_bytes{}; ensure_external_attributes(target, point); if (bind_target) bind_external_attributes(target, target_index, count); if (target.uploaded_data_revisions[target_index] != point.data_revision) { - upload_external_attributes(target, point, target_index); + uploaded_bytes = upload_external_attributes( + target, point, target_index); target.uploaded_data_revisions[target_index] = point.data_revision; } @@ -1831,12 +1809,12 @@ void Datoviz_Visual_Backend::apply_visual( "failed to apply Datoviz external visual visibility"); target.applied_revision = point.revision; target.applied = point; - return; + return uploaded_bytes; } if (target.applied && target.applied->data_revision == point.data_revision) { target.applied_revision = point.revision; target.applied = point; - return; + return 0; } DvzResult result = DVZ_OK; switch (target.family) { @@ -2069,8 +2047,9 @@ void Datoviz_Visual_Backend::apply_visual( throw std::runtime_error("failed to upload Datoviz visual payload"); target.applied_revision = point.revision; target.applied = point; + return 0; } -void Datoviz_Visual_Backend::dispatch_pointer( +void Datoviz_Visual_Backend::Private::dispatch_pointer( ::aethera::Event_Type event, float x, float y, ::aethera::Mouse_Button mouse_button, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) { @@ -2098,7 +2077,7 @@ void Datoviz_Visual_Backend::dispatch_pointer( modifiers(keyboard_modifiers), 1.0F, dvz_input_timestamp_ns(), nullptr); } -void Datoviz_Visual_Backend::dispatch_wheel( +void Datoviz_Visual_Backend::Private::dispatch_wheel( float x, float y, float delta_x, float delta_y, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) { std::lock_guard api_lock(render_context_->api_mutex()); @@ -2112,7 +2091,7 @@ void Datoviz_Visual_Backend::dispatch_wheel( static_cast(viewport.height), delta_x, delta_y, modifiers(keyboard_modifiers), 1.0F, dvz_input_timestamp_ns(), nullptr); } -void Datoviz_Visual_Backend::dispatch_key( +void Datoviz_Visual_Backend::Private::dispatch_key( const ::aethera::Key_Event& event) { std::lock_guard api_lock(render_context_->api_mutex()); input_changed_ = true; @@ -2142,7 +2121,7 @@ void Datoviz_Visual_Backend::dispatch_key( key_code(event.key, event.native_key), modifiers(event.modifiers), nullptr); } -DvzSceneFrameArtifact* Datoviz_Visual_Backend::emit( +DvzSceneFrameArtifact* Datoviz_Visual_Backend::Private::emit( const Scene_3D_Parameters& scene, std::uint8_t target_index) { if (target_index >= runtime_slots_.size() || runtime_slots_[target_index].emitter == nullptr) @@ -2179,7 +2158,8 @@ DvzSceneFrameArtifact* Datoviz_Visual_Backend::emit( } return artifact; } -std::optional Datoviz_Visual_Backend::acquire_target(Extent extent) { +std::optional +Datoviz_Visual_Backend::Private::acquire_target(Extent extent) { for (std::uint8_t index = 0; index < targets_->values.size(); ++index) { auto& candidate = targets_->values[index]; if (candidate && candidate->available() && candidate->extent() == extent) return index; @@ -2193,27 +2173,31 @@ std::optional Datoviz_Visual_Backend::acquire_target(Extent extent } return std::nullopt; } -Datoviz_Visual_Backend::Frame_Target& Datoviz_Visual_Backend::target( +Datoviz_Visual_Backend::Private::Frame_Target& +Datoviz_Visual_Backend::Private::target( const Pending_Frame& pending) { if (pending.target_index >= targets_->values.size()) throw std::logic_error("Datoviz pending frame target index is invalid"); auto& result = targets_->values[pending.target_index]; if (!result || result->generation() != pending.target_generation) throw std::logic_error("Datoviz pending frame target no longer exists"); return *result; } -std::optional Datoviz_Visual_Backend::reuse_recorded_target( +std::optional +Datoviz_Visual_Backend::Private::reuse_recorded_target( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared, std::uint64_t frame_sequence, bool observe, bool readback) { - Datoviz_Frame_Trace trace; - trace.render_sequence = frame_sequence; - trace.observed = observe; - const std::uint64_t started = observe ? trace_now_ns() : 0; + Datoviz_Frame_Observation observation; + observation.render_sequence = frame_sequence; + observation.path = Datoviz_Frame_Path::reused; + observation.gpu_timing_requested = observe; + observation.readback_requested = readback; + const std::uint64_t started = trace_now_ns(); std::lock_guard target_lock(target_mutex_); if (!matches_command_structure(scene, prepared)) return std::nullopt; for (std::uint8_t target_index = 0; target_index < targets_->values.size(); ++target_index) { auto& candidate = targets_->values[target_index]; if (!candidate || candidate->extent() != scene.viewport || - !candidate->can_reuse(command_revision_, observe, readback)) + !candidate->can_reuse(command_revision_, readback)) continue; /* The command buffer already binds this target's independent region. * Only immutable frame payload bytes may change on this path. No @@ -2236,7 +2220,7 @@ std::optional Datoviz_Visual_Backend::reu if (prepared_item_count(source.visual) != 0 && visual->uploaded_data_revisions[target_index] != source.visual.data_revision) { - upload_external_attributes( + observation.uploaded_bytes += upload_external_attributes( *visual, source.visual, target_index); visual->uploaded_data_revisions[target_index] = source.visual.data_revision; @@ -2245,16 +2229,25 @@ std::optional Datoviz_Visual_Backend::reu visual->applied = source.visual; } candidate->reuse(command_revision_, observe, readback); - if (observe) trace.apply_ns = trace_now_ns() - started; + observation.apply_ns = trace_now_ns() - started; return Pending_Frame{ candidate->device(), candidate->fence(), scene.viewport, frame_sequence, target_index, candidate->generation(), - std::move(trace) + std::move(observation) }; } return std::nullopt; } -std::optional Datoviz_Visual_Backend::prepare( +std::optional +Datoviz_Visual_Backend::Private::try_prepare_reused( + const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, + std::uint64_t frame_sequence, bool observe, bool readback) { + if (scene.viewport.empty()) return std::nullopt; + return reuse_recorded_target( + scene, visuals, frame_sequence, observe, readback); +} +std::optional +Datoviz_Visual_Backend::Private::prepare( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback) { if (scene.viewport.empty()) return std::nullopt; @@ -2271,16 +2264,19 @@ std::optional Datoviz_Visual_Backend::pre std::lock_guard target_lock(target_mutex_); const auto target_index = acquire_target(scene.viewport); if (!target_index) return std::nullopt; - Datoviz_Frame_Trace trace; - trace.render_sequence = frame_sequence; - trace.observed = observe; - std::uint64_t phase_started = observe ? trace_now_ns() : 0; + Datoviz_Frame_Observation observation; + observation.render_sequence = frame_sequence; + observation.path = Datoviz_Frame_Path::recorded; + observation.gpu_timing_requested = observe; + observation.readback_requested = readback; + std::uint64_t phase_started = trace_now_ns(); const bool content_changed = !matches_command_structure(scene, visuals); if (content_changed) ++command_revision_; auto& frame_target = *targets_->values[*target_index]; const bool bind_target = - !frame_target.can_reuse(command_revision_, observe, readback); - apply(scene, visuals, *target_index, bind_target); + !frame_target.can_reuse(command_revision_, readback); + observation.uploaded_bytes = apply( + scene, visuals, *target_index, bind_target); if (item_interaction_ != nullptr) { static_cast(dvz_figure_process_queries( figure_, runtime_slots_[*target_index].runtime, nullptr)); @@ -2314,20 +2310,21 @@ std::optional Datoviz_Visual_Backend::pre } } } - if (observe) trace.apply_ns = trace_now_ns() - phase_started; + observation.apply_ns = trace_now_ns() - phase_started; /* A stable Scene does not need another Figure artifact or another DRP2 * execution. Each of the three targets owns its recorded command buffer; * once that target has seen the current content revision, the render-domain * handoff is reduced to fence reset plus vkQueueSubmit. Data, camera, * viewport and input revisions remain authoritative in the backend and * invalidate all older target recordings through command_revision_. */ - if (frame_target.can_reuse(command_revision_, observe, readback)) { + if (frame_target.can_reuse(command_revision_, readback)) { frame_target.reuse(command_revision_, observe, readback); input_changed_ = false; + observation.path = Datoviz_Frame_Path::reused; return Pending_Frame{ frame_target.device(), frame_target.fence(), scene.viewport, frame_sequence, *target_index, frame_target.generation(), - std::move(trace) + std::move(observation) }; } frame_target.begin(observe, readback, command_revision_); @@ -2342,27 +2339,27 @@ std::optional Datoviz_Visual_Backend::pre decltype(&dvz_scene_frame_artifact_destroy)> artifact{nullptr, &dvz_scene_frame_artifact_destroy}; try { - if (observe) phase_started = trace_now_ns(); + phase_started = trace_now_ns(); artifact.reset(emit(scene, *target_index)); - if (observe) trace.emit_ns = trace_now_ns() - phase_started; + observation.emit_ns = trace_now_ns() - phase_started; } catch (...) { frame_target.abort(); raise_context("preparing Datoviz frame", std::current_exception()); } if (observe) { - trace.artifact_status = static_cast( + observation.artifact_status = static_cast( dvz_scene_frame_artifact_status(artifact.get())); - trace.artifact_resource_version = + observation.artifact_resource_version = dvz_scene_frame_artifact_resource_version(artifact.get()); - trace.artifact_frame_index = + observation.artifact_frame_index = dvz_scene_frame_artifact_frame_index(artifact.get()); } const DvzDrp2CommandStream* stream = dvz_scene_frame_artifact_stream(artifact.get()); register_external_attributes(stream, *target_index); const DvzStreamFrame target_frame = frame_target.stream_frame(); - if (observe) phase_started = trace_now_ns(); + phase_started = trace_now_ns(); const bool attached = stream != nullptr && dvz_drp2_runtime_attach_frame_target( runtime_slots_[*target_index].runtime, color_target_id, &target_frame); @@ -2370,14 +2367,15 @@ std::optional Datoviz_Visual_Backend::pre attached ? dvz_drp2_runtime_execute(runtime_slots_[*target_index].runtime, stream) : DvzDrp2ValidationResult{}; - if (observe) trace.execute_ns = trace_now_ns() - phase_started; - trace.validation_ok = attached && result.ok; - trace.validation_code = static_cast(result.code); - trace.validation_command_index = result.command_index; - if (!trace.validation_ok) { + observation.execute_ns = trace_now_ns() - phase_started; + observation.validation_performed = true; + observation.validation_ok = attached && result.ok; + observation.validation_code = static_cast(result.code); + observation.validation_command_index = result.command_index; + if (!observation.validation_ok) { if (char* json = dvz_scene_frame_artifact_json( artifact.get(), "renderive_frame")) { - trace.artifact_json = json; + observation.artifact_json = json; dvz_drp2_stream_json_destroy(json); } } @@ -2392,9 +2390,9 @@ std::optional Datoviz_Visual_Backend::pre "failed to execute Datoviz point frame: validation code " + std::to_string(static_cast(result.code)) + ", command " + std::to_string(result.command_index); - if (!trace.artifact_json.empty()) + if (!observation.artifact_json.empty()) message += ", failing command " + artifact_command_excerpt( - trace.artifact_json, result.command_index); + observation.artifact_json, result.command_index); throw std::runtime_error(std::move(message)); } frame_target.finish_recording(); @@ -2403,39 +2401,41 @@ std::optional Datoviz_Visual_Backend::pre return Pending_Frame{ frame_target.device(), frame_target.fence(), scene.viewport, frame_sequence, *target_index, frame_target.generation(), - std::move(trace) + std::move(observation) }; } -void Datoviz_Visual_Backend::submit(Pending_Frame& pending) { +void Datoviz_Visual_Backend::Private::submit(Pending_Frame& pending) { /* * 所有显式 GPU 提交仍只由 Render Domain 执行;queue_mutex 只满足 Vulkan * 对同一 VkQueue 的外部同步要求,不把 CPU apply、读回或回调串行化。 */ std::lock_guard queue_lock(render_context_->queue_mutex()); std::lock_guard target_lock(target_mutex_); - const std::uint64_t started = pending.trace.observed ? trace_now_ns() : 0; + const std::uint64_t started = trace_now_ns(); target(pending).submit(); - if (pending.trace.observed) pending.trace.submit_ns = trace_now_ns() - started; + pending.observation.submit_ns = trace_now_ns() - started; } -Datoviz_Visual_Backend::Completed_Frame Datoviz_Visual_Backend::collect( +Datoviz_Visual_Backend::Completed_Frame +Datoviz_Visual_Backend::Private::collect( Pending_Frame pending) { /* Fence 已完成,读回缓冲与查询池只属于本目标槽;不同 Scene 的映射内存 * 下载可以并行,且不能反向阻塞 Render_Domain 的下一批提交。 */ std::lock_guard target_lock(target_mutex_); - const std::uint64_t readback_started = pending.trace.observed - ? trace_now_ns() - : 0; + const std::uint64_t readback_started = trace_now_ns(); auto collection = target(pending).collect(); - if (pending.trace.observed) pending.trace.readback_ns = trace_now_ns() - readback_started; - pending.trace.gpu = std::move(collection.gpu_timing); - return {pending.extent, std::move(collection.pixels), std::move(pending.trace)}; + pending.observation.readback_ns = trace_now_ns() - readback_started; + pending.observation.readback_bytes = collection.pixels.size(); + pending.observation.gpu = std::move(collection.gpu_timing); + return {pending.extent, std::move(collection.pixels), + std::move(pending.observation)}; } -void Datoviz_Visual_Backend::discard(Pending_Frame pending) { +void Datoviz_Visual_Backend::Private::discard(Pending_Frame pending) { std::lock_guard target_lock(target_mutex_); target(pending).discard_after_completion(); } -void Datoviz_Visual_Backend::quarantine(Pending_Frame pending) noexcept { - quarantined_ = true; +void Datoviz_Visual_Backend::Private::quarantine( + Pending_Frame pending) noexcept { + quarantined_.store(true, std::memory_order_release); try { std::lock_guard target_lock(target_mutex_); if (pending.target_index < targets_->values.size()) { @@ -2447,37 +2447,13 @@ void Datoviz_Visual_Backend::quarantine(Pending_Frame pending) noexcept { } catch (...) {} } -bool Datoviz_Visual_Backend::can_prepare( - const Scene_3D_Parameters& scene, - const Prepared_Visual_Batch& prepared) const { - if (!render_context_ || quarantined_) return false; - std::lock_guard target_lock(target_mutex_); - const bool target_available = std::ranges::any_of( - targets_->values, [](const auto& target) { - return !target || target->available(); - }); - if (!target_available) return false; - const bool all_targets_available = std::ranges::all_of( - targets_->values, [](const auto& target) { - return !target || target->available(); - }); - if (all_targets_available) return true; - return matches_command_structure(scene, prepared); -} -bool Datoviz_Visual_Backend::idle() const { - if (!render_context_) return true; - std::lock_guard target_lock(target_mutex_); - return std::ranges::all_of(targets_->values, [](const auto& target) { - return !target || target->quiescent(); - }); -} -void Datoviz_Visual_Backend::abandon_resources() noexcept { - quarantined_ = true; +void Datoviz_Visual_Backend::Private::abandon_resources() noexcept { + quarantined_.store(true, std::memory_order_release); static_cast(targets_.release()); retain_quarantined_context(render_context_); } -void Datoviz_Visual_Backend::destroy() { - if (quarantined_) { +void Datoviz_Visual_Backend::Private::destroy() { + if (quarantined_.load(std::memory_order_acquire)) { abandon_resources(); return; } @@ -2546,4 +2522,64 @@ void Datoviz_Visual_Backend::destroy() { if (queue_lock.owns_lock()) queue_lock.unlock(); context.reset(); } + +bool Datoviz_Visual_Backend::Prop::operator==(const Prop&) const = default; +bool Datoviz_Visual_Backend::State::operator==(const State&) const = default; + +Datoviz_Visual_Backend::Datoviz_Visual_Backend() = default; +Datoviz_Visual_Backend::~Datoviz_Visual_Backend() noexcept = default; + +std::optional +Datoviz_Visual_Backend::prepare( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, + std::uint64_t frame_sequence, bool observe, bool readback) { + return static_cast(*d).prepare( + scene, visuals, frame_sequence, observe, readback); +} + +std::optional +Datoviz_Visual_Backend::try_prepare_reused( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, + std::uint64_t frame_sequence, bool observe, bool readback) { + return static_cast(*d).try_prepare_reused( + scene, visuals, frame_sequence, observe, readback); +} + +void Datoviz_Visual_Backend::submit(Pending_Frame& pending) { + static_cast(*d).submit(pending); +} + +Datoviz_Visual_Backend::Completed_Frame +Datoviz_Visual_Backend::collect(Pending_Frame pending) { + return static_cast(*d).collect(std::move(pending)); +} + +void Datoviz_Visual_Backend::discard(Pending_Frame pending) { + static_cast(*d).discard(std::move(pending)); +} + +void Datoviz_Visual_Backend::quarantine(Pending_Frame pending) noexcept { + static_cast(*d).quarantine(std::move(pending)); +} + +void Datoviz_Visual_Backend::dispatch_pointer( + Event_Type type, float x, float y, Mouse_Button button, + Keyboard_Modifier modifiers, Extent viewport) { + static_cast(*d).dispatch_pointer( + type, x, y, button, modifiers, viewport); +} + +void Datoviz_Visual_Backend::dispatch_wheel( + float x, float y, float delta_x, float delta_y, + Keyboard_Modifier modifiers, Extent viewport) { + static_cast(*d).dispatch_wheel( + x, y, delta_x, delta_y, modifiers, viewport); +} + +void Datoviz_Visual_Backend::dispatch_key(const Key_Event& event) { + static_cast(*d).dispatch_key(event); +} + } // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp index ed7aaf1..4acb5e8 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp @@ -1,56 +1,87 @@ -#pragma once -#include "Datoviz_Frame_Trace.hpp" +#pragma once + +#include "../base/Datoviz_Frame_Observation.hpp" #include "Backend_Types.hpp" -#include -#include -#include -#include -#include -#include -#include +#include +#include #include #include +#include #include -#include #include -#include -#include #include + namespace aethera::render_3d::detail { + struct Datoviz_Render_Context; -struct Datoviz_Visual_Backend final { -public: + +struct Datoviz_Visual_Backend final + : Def { + struct Prop : Prev_Prop { + bool operator==(const Prop&) const; + }; + + struct State : Prev_State { + bool operator==(const State&) const; + }; + + struct Private; + + template + struct Builder : Prev_Builder { + using Base = Prev_Builder; + + Builder(std::uint32_t gpu_index, bool validation_enabled, + std::vector visuals, + Scene_3D_Parameters initial_scene); + + [[nodiscard]] std::expected, + Dependency_Graph_Error> + build(); + + private: + std::uint32_t gpu_index{}; /* Datoviz 使用的物理 GPU 下标。 */ + bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */ + std::vector visuals; /* 构造期稳定 Visual 注册表。 */ + Scene_3D_Parameters initial_scene{}; /* 首次创建 Figure 所需场景参数。 */ + }; + struct Pending_Frame { - VkDevice device{VK_NULL_HANDLE}; /* 提交所属 Vulkan Device。 */ - VkFence fence{VK_NULL_HANDLE}; /* 标记本帧 GPU 完成的 fence。 */ - Extent extent{}; /* 本帧离屏目标尺寸。 */ - std::uint64_t sequence{}; /* Scene 分配的帧序号。 */ - std::uint8_t target_index{}; /* 本帧独占的三缓冲目标槽位。 */ - std::uint64_t target_generation{}; /* 防止复用过期目标的资源代次。 */ - Datoviz_Frame_Trace trace; /* 本帧可选诊断数据。 */ + VkDevice device{VK_NULL_HANDLE}; /* 提交所属 Vulkan Device。 */ + VkFence fence{VK_NULL_HANDLE}; /* 标记本帧 GPU 完成的 fence。 */ + Extent extent{}; /* 本帧离屏目标尺寸。 */ + std::uint64_t sequence{}; /* Scene 分配的帧序号。 */ + std::uint8_t target_index{}; /* 本帧独占的三缓冲目标槽位。 */ + std::uint64_t target_generation{}; /* 防止复用过期目标的资源代次。 */ + Datoviz_Frame_Observation observation{}; /* 与外部 Frame_3D 同帧的原始诊断。 */ }; + struct Completed_Frame { - Extent extent{}; /* 已完成 GPU 读回的像素尺寸。 */ - std::vector pixels{}; /* 已完成 GPU 读回的连续 RGBA8 像素。 */ - Datoviz_Frame_Trace trace; /* 已补全 GPU 和读回阶段的原始诊断数据。 */ + Extent extent{}; /* 已完成 GPU 读回的像素尺寸。 */ + std::vector pixels{}; /* 已完成 GPU 读回的连续 RGBA8 像素。 */ + Datoviz_Frame_Observation observation{}; /* 已补全 GPU 和读回阶段的原始诊断。 */ }; - Datoviz_Visual_Backend(std::uint32_t gpu_index, bool validation_enabled, - const std::vector& visuals, - const Scene_3D_Parameters& initial_scene); + + Datoviz_Visual_Backend(); ~Datoviz_Visual_Backend() noexcept; Datoviz_Visual_Backend(const Datoviz_Visual_Backend&) = delete; Datoviz_Visual_Backend& operator=(const Datoviz_Visual_Backend&) = delete; + [[nodiscard]] std::optional prepare( - const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback); + + [[nodiscard]] std::optional try_prepare_reused( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, + std::uint64_t frame_sequence, bool observe, bool readback); + void submit(Pending_Frame& pending); [[nodiscard]] Completed_Frame collect(Pending_Frame pending); void discard(Pending_Frame pending); void quarantine(Pending_Frame pending) noexcept; - [[nodiscard]] bool can_prepare( - const Scene_3D_Parameters& scene, - const Prepared_Visual_Batch& visuals) const; - [[nodiscard]] bool idle() const; + void dispatch_pointer(Event_Type type, float x, float y, Mouse_Button button, Keyboard_Modifier modifiers, @@ -59,88 +90,8 @@ public: Keyboard_Modifier modifiers, Extent viewport); void dispatch_key(const Key_Event& event); -private: - struct Frame_Target; - struct Frame_Targets; - struct External_Attribute { - std::string name{}; /* Datoviz attribute semantic owned by this binding. */ - std::uint32_t stride{}; /* One planar attribute item in bytes. */ - std::uint32_t capacity{}; /* Items per independently writable frame region. */ - DvzSceneBuffer* scene_buffer{}; /* Scene-side stable resource label; Scene owns it. */ - DvzBuffer* gpu_buffer{}; /* Three-region runtime buffer owned by this backend. */ - std::uint8_t registered_targets{}; /* Runtime slots that have borrowed gpu_buffer. */ - }; - struct Visual_Instance { - 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. */ - }; - void create_scene(const std::vector& visuals, - const Scene_3D_Parameters& initial_scene); - [[nodiscard]] DvzVisual* create_visual(Visual_Family family); - void apply_camera(const Camera_Descriptor& camera); - void apply_axes(const Scene_3D_Parameters& scene); - [[nodiscard]] bool matches_command_structure( - const Scene_3D_Parameters& scene, - const Prepared_Visual_Batch& visuals) const; - void apply(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, - std::uint8_t target_index, bool bind_target); - void apply_visual(Visual_Instance& target, const Prepared_Visual& visual, - std::uint8_t target_index, bool bind_target); - void ensure_external_attributes(Visual_Instance& target, - const Prepared_Visual& visual); - void upload_external_attributes(Visual_Instance& target, - const Prepared_Visual& visual, - std::uint8_t target_index); - void bind_external_attributes(Visual_Instance& target, - std::uint8_t target_index, - std::uint32_t item_count); - void register_external_attributes(const DvzDrp2CommandStream* stream, - std::uint8_t target_index); - [[nodiscard]] std::optional reuse_recorded_target( - const Scene_3D_Parameters& scene, - const Prepared_Visual_Batch& visuals, - std::uint64_t frame_sequence, bool observe, bool readback); - [[nodiscard]] DvzSceneFrameArtifact* emit( - const Scene_3D_Parameters& scene, std::uint8_t target_index); - [[nodiscard]] std::optional acquire_target(Extent extent); - [[nodiscard]] Frame_Target& target(const Pending_Frame& pending); - void abandon_resources() noexcept; - void destroy(); - std::shared_ptr render_context_; /* 同一 GPU 上所有后端共享的 Device 与分配器。 */ - mutable std::mutex target_mutex_{}; /* 当前 Scene 三个目标槽的唯一生命周期同步源。 */ - struct Runtime_Slot { - DvzDrp2Runtime* runtime{}; /* Executes only the matching frame target's stream. */ - DvzFramePlanEmitter* emitter{}; /* Retained semantic state paired one-to-one with runtime. */ - }; - std::array runtime_slots_{}; /* Three independent runtime/emitter state machines. */ - DvzScene* scene_{}; /* Datoviz Scene;由本类拥有。 */ - DvzFigure* figure_{}; /* 当前离屏 Figure。 */ - DvzPanel* panel_{}; /* 承载全部业务 Visual 的全屏 Panel。 */ - std::vector visuals_{}; /* 按 Scene 稳定身份管理的原生 Visual。 */ - std::vector external_buffers_{}; /* Runtime-borrowed buffers retained through runtime destruction. */ - DvzVisual* axes_visual_{}; /* 三维主轴、刻度和网格 Segment Visual。 */ - DvzText* axes_text_{}; /* 随相机变换的三维刻度与轴标题。 */ - DvzItemInteraction* item_interaction_{}; /* Datoviz 原生图元悬停与选择控制器。 */ - DvzPinnedReadout* hover_readout_{}; /* 最近一次命中的 Datoviz 数据提示卡。 */ - DvzController* camera_controller_{}; /* 当前绑定到 Panel 的 Datoviz 原生相机控制器。 */ - DvzInputRouter* input_router_{}; /* Scene 输入事件路由器。 */ - DvzPointerGestureHandler* gesture_handler_{}; /* 指针手势解析器。 */ - std::unique_ptr targets_; /* 三个可并行处于准备、GPU 和读回阶段的目标。 */ - Extent figure_extent_{}; /* Datoviz Figure 当前应用的像素尺寸。 */ - std::uint64_t target_generation_{}; /* 每次重建 target_ 时递增的资源代次。 */ - std::uint64_t command_revision_{1}; /* Scene/Visual/Input 任一变化时递增,决定录制命令是否可复用。 */ - std::optional applied_camera_{}; /* 已应用到 Panel 的 Camera 配置。 */ - std::optional> applied_axes_{}; /* 已生成 Visual 的轴描述快照。 */ - bool input_changed_{}; /* 输入控制器是否产生尚未录入完成帧的资源更新。 */ - bool quarantined_{}; /* GPU 已提交后失效时,整套资源停止销毁与复用。 */ }; + } // namespace aethera::render_3d::detail + +#include "Datoviz_Visual_Backend.ipp" diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.ipp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.ipp new file mode 100644 index 0000000..5e42213 --- /dev/null +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.ipp @@ -0,0 +1,163 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aethera::render_3d::detail { + +struct Datoviz_Visual_Backend::Private : Prev_Private { + struct Frame_Target; + struct Frame_Targets; + + struct External_Attribute { + std::string name{}; /* Datoviz attribute semantic owned by this binding. */ + std::uint32_t stride{}; /* One planar attribute item in bytes. */ + std::uint32_t capacity{}; /* Items per independently writable frame region. */ + DvzSceneBuffer* scene_buffer{}; /* Scene-side stable resource label; Scene owns it. */ + DvzBuffer* gpu_buffer{}; /* Three-region runtime buffer owned by this backend. */ + std::uint8_t registered_targets{}; /* Runtime slots that have borrowed gpu_buffer. */ + }; + + struct Visual_Instance { + 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{}; /* 最近应用的元数据与不可变载荷所有权。 */ + std::vector attributes{}; /* 每字段各自拥有三段动态载荷区域。 */ + }; + + struct Runtime_Slot { + DvzDrp2Runtime* runtime{}; /* 只执行对应帧目标的命令流。 */ + DvzFramePlanEmitter* emitter{}; /* 与 runtime 一一对应的保留语义状态。 */ + }; + + Private(); + ~Private() override; + + void initialize(std::uint32_t gpu_index, bool validation_enabled, + const std::vector& visuals, + const Scene_3D_Parameters& initial_scene); + void create_scene(const std::vector& visuals, + const Scene_3D_Parameters& initial_scene); + void apply_camera(const Camera_Descriptor& camera); + void apply_axes(const Scene_3D_Parameters& scene); + [[nodiscard]] bool matches_command_structure( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals) const; + [[nodiscard]] std::uint64_t apply( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, + std::uint8_t target_index, bool bind_target); + [[nodiscard]] std::uint64_t apply_visual( + Visual_Instance& target, const Prepared_Visual& visual, + std::uint8_t target_index, bool bind_target); + void ensure_external_attributes(Visual_Instance& target, + const Prepared_Visual& visual); + [[nodiscard]] std::uint64_t upload_external_attributes( + Visual_Instance& target, const Prepared_Visual& visual, + std::uint8_t target_index); + void bind_external_attributes(Visual_Instance& target, + std::uint8_t target_index, + std::uint32_t item_count); + void register_external_attributes(const DvzDrp2CommandStream* stream, + std::uint8_t target_index); + [[nodiscard]] std::optional reuse_recorded_target( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, + std::uint64_t frame_sequence, bool observe, bool readback); + [[nodiscard]] std::optional prepare( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, + std::uint64_t frame_sequence, bool observe, bool readback); + [[nodiscard]] std::optional try_prepare_reused( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, + std::uint64_t frame_sequence, bool observe, bool readback); + [[nodiscard]] DvzSceneFrameArtifact* emit( + const Scene_3D_Parameters& scene, std::uint8_t target_index); + [[nodiscard]] std::optional acquire_target(Extent extent); + [[nodiscard]] Frame_Target& target(const Pending_Frame& pending); + void submit(Pending_Frame& pending); + [[nodiscard]] Completed_Frame collect(Pending_Frame pending); + void discard(Pending_Frame pending); + void quarantine(Pending_Frame pending) noexcept; + void dispatch_pointer(Event_Type type, float x, float y, + Mouse_Button button, + Keyboard_Modifier modifiers, + Extent viewport); + void dispatch_wheel(float x, float y, float delta_x, float delta_y, + Keyboard_Modifier modifiers, + Extent viewport); + void dispatch_key(const Key_Event& event); + void abandon_resources() noexcept; + void destroy(); + + std::shared_ptr render_context_{}; /* 同一 GPU 上所有后端共享的 Device 与分配器。 */ + mutable std::mutex target_mutex_{}; /* 当前 Scene 三个目标槽的唯一生命周期同步源。 */ + std::array runtime_slots_{}; /* 三套独立 runtime/emitter 状态机。 */ + DvzScene* scene_{}; /* 本后端唯一拥有的 Datoviz Scene。 */ + DvzFigure* figure_{}; /* 当前离屏 Figure。 */ + DvzPanel* panel_{}; /* 承载全部业务 Visual 的全屏 Panel。 */ + std::vector visuals_{}; /* 按 Scene 稳定身份管理的原生 Visual。 */ + std::vector external_buffers_{}; /* runtime 销毁前保持存活的借用缓冲。 */ + DvzVisual* axes_visual_{}; /* 三维主轴、刻度和网格 Segment Visual。 */ + DvzText* axes_text_{}; /* 随相机变换的三维刻度与轴标题。 */ + DvzItemInteraction* item_interaction_{}; /* Datoviz 原生图元悬停与选择控制器。 */ + DvzPinnedReadout* hover_readout_{}; /* 最近一次命中的 Datoviz 数据提示卡。 */ + DvzController* camera_controller_{}; /* 当前绑定到 Panel 的原生相机控制器。 */ + DvzInputRouter* input_router_{}; /* Scene 输入事件路由器。 */ + DvzPointerGestureHandler* gesture_handler_{}; /* 指针手势解析器。 */ + std::unique_ptr targets_{}; /* 三个可并行处于准备、GPU、读回阶段的目标。 */ + Extent figure_extent_{}; /* Figure 当前应用的像素尺寸。 */ + std::uint64_t target_generation_{}; /* 每次重建目标时递增的资源代次。 */ + std::uint64_t command_revision_{1}; /* 结构变化后递增的命令录制版本。 */ + std::optional applied_camera_{}; /* 已应用到 Panel 的 Camera 配置。 */ + std::optional> applied_axes_{}; /* 已应用到轴 Visual 的描述。 */ + bool input_changed_{}; /* 输入是否产生尚未录入命令的资源变化。 */ + std::atomic_bool quarantined_{}; /* GPU 异常后是否永久放弃销毁与复用。 */ +}; + +template +Datoviz_Visual_Backend::Builder::Builder( + std::uint32_t gpu_index_value, bool validation_enabled_value, + std::vector visuals_value, + Scene_3D_Parameters initial_scene_value) + : Base(), + gpu_index(gpu_index_value), + validation_enabled(validation_enabled_value), + visuals(std::move(visuals_value)), + initial_scene(std::move(initial_scene_value)) {} + +template +std::expected, Dependency_Graph_Error> +Datoviz_Visual_Backend::Builder::build() { + if (visuals.empty()) + throw std::invalid_argument( + "Datoviz backend requires at least one Visual registration"); + auto result = Base::build(); + if (!result) return std::unexpected(result.error()); + auto backend = std::move(result).value(); + auto& private_data = Base::private_access(backend.get()) + .template get(); + private_data.initialize(gpu_index, validation_enabled, visuals, + initial_scene); + return backend; +} + +} // namespace aethera::render_3d::detail diff --git a/web_server/src/Gallery_Plots_3D.cpp b/web_server/src/Gallery_Plots_3D.cpp index fa624a9..b771248 100644 --- a/web_server/src/Gallery_Plots_3D.cpp +++ b/web_server/src/Gallery_Plots_3D.cpp @@ -306,9 +306,9 @@ public: detail::Prop_Field < &Camera_3D::Prop::near_plane, "near_plane", "Nearest visible camera distance." >, detail::Prop_Field<&Camera_3D::Prop::far_plane, "far_plane", "Farthest visible camera distance."> >; using Axes_Adapter = detail::Renderable_Adapter, - detail::Prop_Field < &Axes_3D::Prop::y_axis, "y_axis", "Y axis range, scale, ticks, label and unit." >, - detail::Prop_Field<&Axes_3D::Prop::z_axis, "z_axis", "Z axis range, scale, ticks, label and unit."> >; + detail::Prop_Field < &Axes_3D::Prop::x_axis, "x_axis", "X axis range, scale, ticks, label and unit.", "axis3d">, + detail::Prop_Field < &Axes_3D::Prop::y_axis, "y_axis", "Y axis range, scale, ticks, label and unit.", "axis3d" >, + detail::Prop_Field<&Axes_3D::Prop::z_axis, "z_axis", "Z axis range, scale, ticks, label and unit.", "axis3d"> >; descriptors_.push_back(detail::make_renderable_descriptor("scene", "3D 场景", "scene", Scene_Adapter{scene})); descriptors_.push_back(detail::make_renderable_descriptor("camera", "相机控制", "camera", Camera_Adapter{*camera_})); descriptors_.push_back(detail::make_renderable_descriptor("axes", "三维坐标轴", "axes", Axes_Adapter{*axes_})); diff --git a/web_server/src/Gallery_WebSocket.cpp b/web_server/src/Gallery_WebSocket.cpp index 7a3cc14..ed0f713 100644 --- a/web_server/src/Gallery_WebSocket.cpp +++ b/web_server/src/Gallery_WebSocket.cpp @@ -33,6 +33,14 @@ struct Gallery_WebSocket::Private { Gallery_Transport_Mode transport{Gallery_Transport_Mode::ffmpeg}; /* 连接建立时协商的唯一媒体模型。 */ std::atomic_bool attached{}; /* start 成功后为真,并保证 close 只执行一次。 */ std::atomic_bool media_failed{}; /* 首次 WebRTC 发送 Unknown Failure 后停止重复报告。 */ + std::atomic> pending_pixels{}; /* 浏览器尚未确认时仅保留最新图集帧。 */ + std::atomic_bool pixel_frame_in_flight{}; /* Drogon 写队列中至多存在一个原始像素帧。 */ + std::atomic_uint64_t pixel_in_flight_sequence{}; /* 当前等待浏览器确认的协议序号。 */ + std::atomic_uint64_t pixel_queued_count{}; /* 进入 latest-only 槽的图集帧数。 */ + std::atomic_uint64_t pixel_sent_count{}; /* 已交给 Drogon 的有界帧数。 */ + std::atomic_uint64_t pixel_acknowledged_count{}; /* 浏览器完成上传后确认的帧数。 */ + std::atomic_uint64_t pixel_replaced_count{}; /* 等待槽中被更新图集替换的旧帧数。 */ }; Gallery_WebSocket::Gallery_WebSocket( @@ -90,11 +98,27 @@ void Gallery_WebSocket::start() { [weak] { const auto socket = weak.lock(); if (socket && - socket->d->transport == Gallery_Transport_Mode::websocket_pixels) + socket->d->transport == Gallery_Transport_Mode::websocket_pixels) { + const auto pending = socket->d->pending_pixels.load( + std::memory_order_acquire); return nlohmann::json{ {"kind", "websocket_pixel_transport_state"}, {"available", socket->d->attached.load( - std::memory_order_acquire)}}; + std::memory_order_acquire)}, + {"frame_in_flight", socket->d->pixel_frame_in_flight.load( + std::memory_order_acquire)}, + {"pending_latest_frame", static_cast(pending)}, + {"in_flight_sequence", socket->d->pixel_in_flight_sequence.load( + std::memory_order_relaxed)}, + {"queued_frame_count", socket->d->pixel_queued_count.load( + std::memory_order_relaxed)}, + {"sent_frame_count", socket->d->pixel_sent_count.load( + std::memory_order_relaxed)}, + {"acknowledged_frame_count", socket->d->pixel_acknowledged_count.load( + std::memory_order_relaxed)}, + {"replaced_frame_count", socket->d->pixel_replaced_count.load( + std::memory_order_relaxed)}}; + } return socket && socket->d->video ? socket->d->video->diagnostics() : nlohmann::json{{"kind", "webrtc_transport_state"}, @@ -121,15 +145,8 @@ void Gallery_WebSocket::deliver( } if (frame.pixels && d->transport == Gallery_Transport_Mode::websocket_pixels && - !d->media_failed.load(std::memory_order_acquire)) { - try { - connection->send(frame.pixels->bytes, - drogon::WebSocketMessageType::Binary); - } - catch (...) { - fail_media(std::current_exception()); - } - } + !d->media_failed.load(std::memory_order_acquire)) + queue_pixel_frame(std::move(frame.pixels)); if (!frame.notification.empty()) { try { connection->send(std::move(frame.notification), @@ -139,6 +156,63 @@ void Gallery_WebSocket::deliver( } } +void Gallery_WebSocket::queue_pixel_frame( + std::shared_ptr frame) { + if (!frame || !d->attached.load(std::memory_order_acquire)) return; + d->pixel_queued_count.fetch_add(1, std::memory_order_relaxed); + if (d->pending_pixels.exchange( + std::move(frame), std::memory_order_acq_rel)) + d->pixel_replaced_count.fetch_add(1, std::memory_order_relaxed); + try_send_pixel_frame(); +} + +void Gallery_WebSocket::try_send_pixel_frame() { + bool available{}; + if (!d->pixel_frame_in_flight.compare_exchange_strong( + available, true, std::memory_order_acq_rel, + std::memory_order_acquire)) + return; + auto frame = d->pending_pixels.exchange({}, std::memory_order_acq_rel); + if (!frame) { + d->pixel_frame_in_flight.store(false, std::memory_order_release); + if (d->pending_pixels.load(std::memory_order_acquire)) + try_send_pixel_frame(); + return; + } + const auto connection = d->connection.lock(); + if (!connection || !connection->connected() || + !d->attached.load(std::memory_order_acquire)) { + d->pixel_frame_in_flight.store(false, std::memory_order_release); + return; + } + d->pixel_in_flight_sequence.store( + frame->sequence, std::memory_order_release); + try { + connection->send(frame->bytes, + drogon::WebSocketMessageType::Binary); + d->pixel_sent_count.fetch_add(1, std::memory_order_relaxed); + } + catch (...) { + d->pixel_frame_in_flight.store(false, std::memory_order_release); + fail_media(std::current_exception()); + } +} + +void Gallery_WebSocket::acknowledge_pixel_frame(std::uint64_t sequence) { + if (d->transport != Gallery_Transport_Mode::websocket_pixels || + sequence == 0 || sequence != d->pixel_in_flight_sequence.load( + std::memory_order_acquire)) + return; + bool in_flight{true}; + if (!d->pixel_frame_in_flight.compare_exchange_strong( + in_flight, false, std::memory_order_acq_rel, + std::memory_order_acquire)) + return; + d->pixel_acknowledged_count.fetch_add(1, std::memory_order_relaxed); + try_send_pixel_frame(); +} + void Gallery_WebSocket::fail_media(std::exception_ptr failure) noexcept { if (d->media_failed.exchange(true, std::memory_order_acq_rel)) return; try { @@ -159,7 +233,9 @@ void Gallery_WebSocket::receive(std::string_view message) { if (json.is_discarded() || !json.is_object()) return; const auto kind = json.value("kind", std::string{}); try { - if (kind == "webrtc_answer" && d->video) { + if (kind == "pixel_frame_consumed") { + acknowledge_pixel_frame(json.value("sequence", 0ULL)); + } else if (kind == "webrtc_answer" && d->video) { const auto sdp = json.value("sdp", std::string{}); if (!sdp.empty()) d->video->accept_answer(sdp); } else if (kind == "webrtc_candidate" && d->video) { @@ -182,6 +258,8 @@ void Gallery_WebSocket::close() noexcept { } catch (...) {} d->subscription = 0; + d->pending_pixels.store({}, std::memory_order_release); + d->pixel_frame_in_flight.store(false, std::memory_order_release); if (d->video) d->video->close(); } diff --git a/web_server/src/Gallery_WebSocket.hpp b/web_server/src/Gallery_WebSocket.hpp index 9b37d38..58c2b79 100644 --- a/web_server/src/Gallery_WebSocket.hpp +++ b/web_server/src/Gallery_WebSocket.hpp @@ -21,6 +21,11 @@ public: void close() noexcept; private: void deliver(Gallery_Stream_Frame frame); + void queue_pixel_frame( + std::shared_ptr frame); + void try_send_pixel_frame(); + void acknowledge_pixel_frame(std::uint64_t sequence); void fail_media(std::exception_ptr failure) noexcept; struct Private; std::unique_ptr d; diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index 21d60c4..1f3b29f 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -288,11 +288,56 @@ void append_event_statistics_json(nlohmann::json& output, } } +nlohmann::json datoviz_observation_json( + const Datoviz_Frame_Observation& value) { + const auto milliseconds = [](std::uint64_t nanoseconds) { + return static_cast(nanoseconds) / 1'000'000.0; + }; + nlohmann::json result{ + {"render_sequence", value.render_sequence}, + {"path", magic_enum::enum_name(value.path)}, + {"gpu_timing_requested", value.gpu_timing_requested}, + {"readback_requested", value.readback_requested}, + {"timings_ms", { + {"render_domain_queue_wait", + milliseconds(value.render_domain_queue_wait_ns)}, + {"apply", milliseconds(value.apply_ns)}, + {"emit", milliseconds(value.emit_ns)}, + {"execute", milliseconds(value.execute_ns)}, + {"submit", milliseconds(value.submit_ns)}, + {"gpu_fence_wait", milliseconds(value.gpu_fence_wait_ns)}, + {"readback", milliseconds(value.readback_ns)}}}, + {"traffic", { + {"uploaded_bytes", value.uploaded_bytes}, + {"readback_bytes", value.readback_bytes}}}, + {"frame_plan", { + {"resource_version", value.artifact_resource_version}, + {"frame_index", value.artifact_frame_index}, + {"status", value.artifact_status}}}, + {"validation", { + {"performed", value.validation_performed}, + {"ok", value.validation_ok}, + {"code", value.validation_code}, + {"command_index", value.validation_command_index}}} + }; + if (value.gpu) { + result["gpu_ms"] = { + {"render", milliseconds(value.gpu->render_ns)}, + {"transition", milliseconds(value.gpu->transition_ns)}, + {"copy", milliseconds(value.gpu->copy_ns)}, + {"total", milliseconds(value.gpu->total_ns)}}; + } + if (!value.artifact_json.empty()) + result["frame_plan"]["artifact_json"] = value.artifact_json; + return result; +} + } nlohmann::json taskflow_trace_json( const Taskflow_Frame_Trace& trace, - const nlohmann::json& captured_components) { + const nlohmann::json& captured_components, + const nlohmann::json& captured_backend) { nlohmann::json markers = nlohmann::json::object(); for (const auto& marker : trace.markers) markers[magic_enum::enum_name(marker.marker)] = @@ -359,7 +404,7 @@ nlohmann::json taskflow_trace_json( {"observer_exit_ms", task.observer_exit_ms}, {"queue_wait_ms", task.queue_wait_ms}}); } - return { + nlohmann::json result{ {"sequence", trace.identity.sequence}, {"correlation_id", trace.identity.correlation_id}, {"request_source", magic_enum::enum_name(trace.request_source)}, @@ -370,6 +415,8 @@ nlohmann::json taskflow_trace_json( {"measurements", std::move(measurements)}, {"graphs", std::move(graphs)}, {"executions", std::move(executions)}}; + if (!captured_backend.empty()) result["datoviz"] = captured_backend; + return result; } namespace { @@ -596,7 +643,8 @@ struct Plot::Private { std::array>, maximum_taskflow_trace_frames>& slots, const Taskflow_Frame_Trace& trace, - const nlohmann::json& captured_components = {}); + const nlohmann::json& captured_components = {}, + const nlohmann::json& captured_backend = {}); [[nodiscard]] nlohmann::json trace_response( const std::atomic_uint64_t& control, const std::atomic_size_t& remaining, @@ -848,9 +896,10 @@ void Plot::Private::store_trace( std::array>, maximum_taskflow_trace_frames>& slots, const Taskflow_Frame_Trace& value, - const nlohmann::json& captured_components) { + const nlohmann::json& captured_components, + const nlohmann::json& captured_backend) { auto trace = std::make_shared( - taskflow_trace_json(value, captured_components)); + taskflow_trace_json(value, captured_components, captured_backend)); auto state = control.load(std::memory_order_acquire); for (;;) { const auto requested = static_cast(state >> 32U); @@ -1324,6 +1373,7 @@ void Plot::Private::finalize_retired_frame(Render_Frame* frame) { std::optional captured_trace; nlohmann::json captured_components; + nlohmann::json captured_backend; if (frame->taskflow_trace_requested()) { captured_trace.emplace(frame->take_taskflow_trace()); std::unordered_set executed_nodes; @@ -1343,6 +1393,10 @@ void Plot::Private::finalize_retired_frame(Render_Frame* frame) { } } captured_components = view->capture_components(executed_components); + if (auto* frame_3d = dynamic_cast(frame)) { + if (auto observation = frame_3d->take_datoviz_observation()) + captured_backend = datoviz_observation_json(*observation); + } } auto expected = Frame_State::consuming; @@ -1363,11 +1417,13 @@ void Plot::Private::finalize_retired_frame(Render_Frame* frame) { auto owner = lifetime; schedule_task("plot.taskflow.serialize", [owner, trace = std::move(*captured_trace), - components = std::move(captured_components)]() mutable { + components = std::move(captured_components), + backend = std::move(captured_backend)]() mutable { if (const auto plot = owner.lock()) plot->d->store_trace( plot->d->taskflow_trace_control, - plot->d->taskflow_trace_slots, trace, components); + plot->d->taskflow_trace_slots, trace, components, + backend); }); } } diff --git a/web_server/src/Taskflow_Trace_Json.hpp b/web_server/src/Taskflow_Trace_Json.hpp index 83b6da0..39db0b0 100644 --- a/web_server/src/Taskflow_Trace_Json.hpp +++ b/web_server/src/Taskflow_Trace_Json.hpp @@ -5,5 +5,6 @@ namespace aethera::web { [[nodiscard]] nlohmann::json taskflow_trace_json( const Taskflow_Frame_Trace& trace, - const nlohmann::json& captured_components = {}); + const nlohmann::json& captured_components = {}, + const nlohmann::json& captured_backend = {}); } diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 3c4adc4..c9c14bd 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -17,7 +17,7 @@ import "react-calendar-timeline/style.css"; type Plot = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; media: string; schema: string; diagnostics: string; taskflow: string}; type Option = {value: string; label: string}; -type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "partition-grid" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "marker-list" | "surface-marker-list" | "json"; +type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "partition-grid" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "marker-list" | "surface-marker-list" | "axis3d" | "json"; type Color_Channel_Scale = "normalized" | "byte"; type Field = {key: string; label: string; description: string; technical_description?: string; editor: Editor; editable: boolean; value: unknown; options?: Option[]; color_channel_scale?: Color_Channel_Scale; minimum?: number; maximum?: number; step?: number}; type Component = {id: string; label: string; kind: string; fields: Field[]}; @@ -95,10 +95,23 @@ type Frame_Policy_State = { latest_completion_ms: number; average_completion_ms: number; maximum_completion_ms: number}; last_frame: {sequence: number; request_source: "unspecified" | "periodic" | "immediate" | "maximum_rate"}; }; +type Datoviz_Frame_Observation = { + render_sequence: number; + path: "recorded" | "reused"; + gpu_timing_requested: boolean; + readback_requested: boolean; + timings_ms: {render_domain_queue_wait: number; apply: number; emit: number; execute: number; + submit: number; gpu_fence_wait: number; readback: number}; + traffic: {uploaded_bytes: number; readback_bytes: number}; + gpu_ms?: {render: number; transition: number; copy: number; total: number}; + frame_plan: {resource_version: number; frame_index: number; status: number; artifact_json?: string}; + validation: {performed: boolean; ok: boolean; code: number; command_index: number}; +}; type Taskflow_Frame_Trace = {sequence: number; correlation_id: number; created_time_unix_ns: number; worker_count: number; request_source?: "unspecified" | "periodic" | "immediate" | "maximum_rate"; frame_policy?: Frame_Policy_State; markers?: Record; measurements?: Record; + datoviz?: Datoviz_Frame_Observation; graphs: Taskflow_Graph_Trace[]; executions: Taskflow_Execution_Trace[]}; type Taskflow_Frame_Response = {protocol: "aethera.taskflow.frames"; version: 1; requested: number; remaining: number; captured: number; complete: boolean; frames: Taskflow_Frame_Trace[]; media_requested?: number; @@ -510,6 +523,8 @@ function use_gallery_videos(plots: Plot[], transport_mode: Gallery_Transport_Mod ? await event.data.arrayBuffer() : null; if (!buffer || stopped || !pixel_surface) return; const sequence = pixel_surface.render(buffer); + if (socket.readyState === WebSocket.OPEN) + socket.send(JSON.stringify({kind: "pixel_frame_consumed", sequence})); if (!pixel_live) { pixel_live = true; update(current => ({...current, status: "LIVE", @@ -929,6 +944,87 @@ function Structured_Control({field, on_change}: {field: Field; on_change: (value ; } +type Axis_3D_Value = { + range: {origin: number; target: number}; + scale: "linear" | "logarithmic" | "time"; + label: string; + unit: string; + target_tick_count: number; + precision: number; + visible: boolean; + grid_visible: boolean; + labels_visible: boolean; +}; + +function axis_3d_value(value: unknown): Axis_3D_Value { + const source = value && typeof value === "object" ? value as Partial : {}; + const range = source.range && typeof source.range === "object" ? source.range : {origin: 0, target: 1}; + return { + range: {origin: Number(range.origin ?? 0), target: Number(range.target ?? 1)}, + scale: source.scale === "logarithmic" || source.scale === "time" ? source.scale : "linear", + label: String(source.label ?? ""), unit: String(source.unit ?? ""), + target_tick_count: Number(source.target_tick_count ?? 6), precision: Number(source.precision ?? 2), + visible: source.visible ?? true, grid_visible: source.grid_visible ?? true, + labels_visible: source.labels_visible ?? true + }; +} + +function axis_3d_error(value: unknown) { + const axis = axis_3d_value(value); + if (!Number.isFinite(axis.range.origin) || !Number.isFinite(axis.range.target)) return "轴范围必须是有限数字"; + if (axis.range.origin === axis.range.target) return "轴范围起点和终点不能相同"; + if (axis.scale === "logarithmic" && (axis.range.origin <= 0 || axis.range.target <= 0)) + return "对数频率轴的起点和终点必须大于 0"; + if (!Number.isInteger(axis.target_tick_count) || axis.target_tick_count < 2 || axis.target_tick_count > 64) + return "目标刻度数必须是 2 到 64 的整数"; + if (!Number.isInteger(axis.precision) || axis.precision < 0 || axis.precision > 12) + return "小数精度必须是 0 到 12 的整数"; + return ""; +} + +function Axis_3D_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) { + const axis = axis_3d_value(field.value); + const is_frequency = axis.unit.trim().toLowerCase() === "hz" || axis.label.trim().toLowerCase() === "frequency"; + const semantic = axis.scale === "time" ? "time" : is_frequency ? "frequency" : "numeric"; + const semantic_label = semantic === "time" ? "时间轴" : semantic === "frequency" ? "频率轴" : "数值轴"; + const update = (next: Partial) => on_change({...axis, ...next}); + const update_range = (next: Partial) => update({range: {...axis.range, ...next}}); + const select_semantic = (next: string) => { + if (next === "time") update({scale: "time", label: "Time", unit: "s"}); + else if (next === "frequency") update({scale: "logarithmic", label: "Frequency", unit: "Hz", + range: axis.range.origin > 0 && axis.range.target > 0 ? axis.range : {origin: 10, target: 20_000}}); + else update({scale: axis.scale === "time" ? "linear" : axis.scale}); + }; + const error = axis_3d_error(axis); + return
+ {field_label(field)} · {semantic_label} {field.key} +
+ + + + + + + + +
+
+ + + +
+ {error ? {error} : 范围和单位直接写入当前 Axes_3D 属性;渲染帧使用同一份描述生成 Datoviz 轴线、网格和文字。} +
; +} + type Marker_Value = {position: {x: number; y: number; z: number}; color: {red: number; green: number; blue: number; alpha: number}; diameter_px: number; angle: number; shape: string; coordinate_label_visible: boolean}; const marker_shapes = ["disc", "square", "triangle", "diamond", "cross"]; @@ -954,6 +1050,7 @@ function Field_Control({field, on_change}: {field: Field; on_change: (value: unk if (["color", "point2", "size", "rect", "range", "partition-grid", "pen", "brush", "font", "vector3"].includes(field.editor)) return ; if (field.editor === "marker-list" || field.editor === "surface-marker-list") return ; + if (field.editor === "axis3d") return ; if (["json", "color-map", "matrix4"].includes(field.editor)) return ; if (field.editor === "boolean") return ; @@ -968,6 +1065,7 @@ function Property_Control({field, on_commit}: {field: Field; on_commit: (value: const [dirty, set_dirty] = useState(false); const [saving, set_saving] = useState(false); const encoded_value = JSON.stringify(field.value); + const validation_error = field.editor === "axis3d" ? axis_3d_error(draft) : ""; useEffect(() => { set_draft(field.value); set_dirty(false); }, [encoded_value]); const change = (value: unknown) => { set_draft(value); @@ -984,7 +1082,7 @@ function Property_Control({field, on_commit}: {field: Field; on_commit: (value: return
{field.editor !== "boolean" ?
- +
: saving ? 正在提交… : null}
; @@ -1237,6 +1335,9 @@ function taskflow_node_name(name: string, node?: Taskflow_Node_Trace) { if (name === "scene.paint") return "Scene · 绘制子图"; if (name === "scene.paint.complete") return "Scene · 像素绘制完成"; if (name === "scene.completion") return "Scene · 帧完成处理"; + if (name === "scene.backend.submit") return "3D Scene · 准备并提交 Datoviz 帧"; + if (name === "render_3d.backend.collect") return "Datoviz · GPU 完成后读回"; + if (name === "render_3d.frame.retire") return "3D Scene · 完成帧退役"; if (name === "plot.frame.publish") return "Plot · 发布完成帧"; if (name === "gallery.sample.capture") return "Gallery · 30 FPS 最近帧采样"; if (name === "FFmpeg.H264.encode" || name === "gallery.h264.encode") return "FFmpeg H.264 · 编码"; @@ -1678,7 +1779,7 @@ function taskflow_render_domain_state(node: Taskflow_Node_Trace, frame?: Taskflo markers[first] === undefined || markers[last] === undefined ? null : Math.max(0, markers[last] - markers[first]); const prepare = interval("backend_prepare_started", "backend_prepare_finished"); - const submit_queue = interval("backend_submit_queued", "backend_queue_left"); + const submit_queue = interval("backend_queue_entered", "backend_queue_left"); const gpu = interval("gpu_submitted", "gpu_completed"); return { invoke_result: markers.backend_prepare_finished === undefined @@ -1687,7 +1788,8 @@ function taskflow_render_domain_state(node: Taskflow_Node_Trace, frame?: Taskflo submit_queue_ms: submit_queue, gpu_completion_ms: gpu, frame_sequence: frame.sequence, - correlation_id: frame.correlation_id + correlation_id: frame.correlation_id, + datoviz: frame.datoviz ?? null }; } @@ -1886,6 +1988,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_ frame_policy: frame?.frame_policy, markers: frame?.markers ?? {}, measurements: frame?.measurements ?? {}, + datoviz: frame?.datoviz, graph, expanded_graph: expanded_taskflow_graph(graph), /* 保留当前选中拓扑,同时把本帧其余真实 Task_Graph 一并复制。 @@ -2062,7 +2165,11 @@ function Taskflow_Timeline({graph, executions, frame, components, gallery_state, `${label} ${milliseconds(end - start)}`, label, `${start_key} → ${end_key}`); }); - add_lifecycle_group("lifecycle-backend", "后端与 GPU", "准备 → 队列 → GPU → 回读"); + const datoviz = frame.datoviz; + const datoviz_detail = datoviz + ? `${datoviz.path} · 上传 ${datoviz.traffic.uploaded_bytes.toLocaleString("zh-CN")} B · 读回 ${datoviz.traffic.readback_bytes.toLocaleString("zh-CN")} B` + : "准备 → 队列 → GPU → 回读"; + add_lifecycle_group("lifecycle-backend", "后端与 GPU", datoviz_detail); const backend_intervals: Array<[string, string, string, string]> = [ ["backend_prepare_started", "backend_prepare_finished", "后端准备", "backend-prepare"], ["backend_queue_entered", "backend_queue_left", "提交队列", "backend-queue"], @@ -2161,6 +2268,7 @@ function Taskflow_Timeline({graph, executions, frame, components, gallery_state, frame_policy: frame?.frame_policy, markers: frame?.markers ?? {}, measurements: frame?.measurements ?? {}, + datoviz: frame?.datoviz, graph, timeline: {origin: "render_frame_created", submitted_ms: graph.submitted_ms, finished_ms: graph.finished_ms, wall_time_ms: Math.max(0, graph.finished_ms - graph.submitted_ms), last_execution_completed_ms: model.last_completed, @@ -2255,6 +2363,38 @@ function Taskflow_Timeline({graph, executions, frame, components, gallery_state, ; } +function frame_bytes(value: number) { + if (!Number.isFinite(value)) return "--"; + if (value >= 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MiB`; + if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`; + return `${value.toLocaleString("zh-CN")} B`; +} + +function Datoviz_Frame_Summary({observation}: {observation: Datoviz_Frame_Observation}) { + const timing = observation.timings_ms; + const gpu = observation.gpu_ms; + const validation = observation.validation; + const structural = observation.path === "recorded"; + return
+
本帧 Datoviz 路径观测数据归属于 Frame_3D #{observation.render_sequence},不读取独立历史状态。
+ {structural ? "重新录制命令" : "复用录制命令"}
+
+
Render Domain 排队
{milliseconds(timing.render_domain_queue_wait)}
+
Datoviz Apply
{milliseconds(timing.apply)}
+
Frame Plan emit
{milliseconds(timing.emit)}
+
DRP2 execute
{milliseconds(timing.execute)}
+
vkQueueSubmit
{milliseconds(timing.submit)}
+
GPU 总耗时
{gpu ? milliseconds(gpu.total) : "未采集"}
+
GPU fence 等待
{milliseconds(timing.gpu_fence_wait)}
+
像素读回
{milliseconds(timing.readback)}
+
动态数据上传
{frame_bytes(observation.traffic.uploaded_bytes)}
+
帧读回流量
{frame_bytes(observation.traffic.readback_bytes)}
+
Frame Plan
v{observation.frame_plan.resource_version} · 槽 {observation.frame_plan.frame_index}
+
验证
{validation.performed ? validation.ok ? "通过" : `失败 ${validation.code}` : "未执行"}
+
+
; +} + function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Component[]}) { const [frame_count, set_frame_count] = useState(8); const [scene_response, set_scene_response] = useState(null); @@ -2365,6 +2505,7 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon Executor {frame.worker_count} workers · 本帧 {frame.executions.length} 次任务执行 + {frame.datoviz ? : null} {displayed_view_mode === "aggregate" && aggregate ? <>
业务阶段
{aggregate.graph.stage}
捕获帧 / 阶段样本
{aggregate.frames} / {aggregate.samples}
diff --git a/webapp_gallery/src/styles.css b/webapp_gallery/src/styles.css index 5c80ff1..6370a44 100644 --- a/webapp_gallery/src/styles.css +++ b/webapp_gallery/src/styles.css @@ -220,6 +220,14 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; } .taskflowGraphSummary > div, .taskflowRuntimeSummary > div { min-width: 0; padding: 10px; border: 1px solid #213653; border-radius: 9px; background: #0a1422; } .taskflowGraphSummary dt, .taskflowRuntimeSummary dt { color: #71839e; font-size: 10px; } .taskflowGraphSummary dd, .taskflowRuntimeSummary dd { margin: 5px 0 0; color: #5ce4c2; font: 700 13px/1.35 ui-monospace, monospace; white-space: normal; overflow-wrap: anywhere; } +.datovizFrameObservation { overflow: hidden; border: 1px solid #28516a; border-radius: 10px; background: #081522; } +.datovizFrameObservation > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-bottom: 1px solid #203c53; background: #0d1d2c; } +.datovizFrameObservation > header > div { display: grid; gap: 3px; } +.datovizFrameObservation > header strong { color: #dce8f8; font-size: 11px; } +.datovizFrameObservation > header span { color: #7189a8; font-size: 9px; } +.datovizFrameObservation > header i { flex: none; padding: 5px 8px; border: 1px solid #337360; border-radius: 99px; color: #72e3c8; background: #0b2822; font: 700 9px/1 ui-monospace, monospace; font-style: normal; } +.datovizFrameObservation > header i.recorded { color: #efc073; border-color: #76572d; background: #291d0d; } +.datovizFrameObservation .taskflowGraphSummary { padding: 10px; } .taskflowDagSection { min-width: 640px; overflow: visible; border: 1px solid #213653; border-radius: 10px; background: #07101c; } .taskflowDagToolbar { display: flex; align-items: center; justify-content: flex-start; flex-wrap: wrap; gap: 12px; padding: 9px 11px; border-bottom: 1px solid #213653; background: #0c1727; } .taskflowDagToolbar button { flex: none; padding: 6px 9px; color: #b9cce3; border: 1px solid #36516f; border-radius: 6px; background: #132238; cursor: pointer; } @@ -337,6 +345,13 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; } .componentCard > .propGrid { padding: 13px; } .structuredControl { min-width: 0; margin: 0; padding: 10px; border: 1px solid #263b59; border-radius: 9px; } .structuredControl legend { padding: 0 5px; color: #c8d5e8; } +.axis3dControl { gap: 10px; background: #0a1524; } +.axis3dGrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } +.axis3dGrid label { display: grid; min-width: 0; gap: 5px; color: #8298b4; font-size: 10px; } +.axis3dToggles { display: flex; flex-wrap: wrap; gap: 8px 13px; color: #9eb1c9; font-size: 10px; } +.axis3dToggles label { display: inline-flex; align-items: center; gap: 5px; } +.axis3dToggles input[type="checkbox"] { width: 17px; height: 17px; } +@media (max-width: 620px) { .axis3dGrid { grid-template-columns: 1fr; } } .inlineFields { display: grid; grid-template-columns: repeat(auto-fit, minmax(82px, 1fr)); gap: 8px; } .inlineFields > label { display: grid; gap: 4px; min-width: 0; color: #8195b1; font-size: 11px; } .inlineFields input[type="color"] { min-height: 40px; padding: 3px; }