From b3af1c1b04cd54ebb2975d27a9c7b691b99ea44b Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sun, 23 Aug 2026 02:08:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/src/kernel/frame.hpp | 3 + .../render_3D/detail/Async_Render_Backend.cpp | 588 +++++++++++------- .../detail/Datoviz_Visual_Backend.cpp | 177 +++++- .../detail/Datoviz_Visual_Backend.hpp | 16 +- web_server/src/Plot.cpp | 53 +- webapp_gallery/src/app.tsx | 59 +- 6 files changed, 571 insertions(+), 325 deletions(-) diff --git a/kernel/src/kernel/frame.hpp b/kernel/src/kernel/frame.hpp index 6c5b87a..bd8901e 100644 --- a/kernel/src/kernel/frame.hpp +++ b/kernel/src/kernel/frame.hpp @@ -15,6 +15,9 @@ enum class Frame_Trace_Marker : std::uint8_t { paint_started, paint_finished, backend_queue_entered, + backend_prepare_started, + backend_prepare_finished, + backend_submit_queued, backend_queue_left, gpu_submitted, gpu_completed, diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp index b3852b7..61067b4 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.cpp @@ -2,6 +2,7 @@ #include "Datoviz_Visual_Backend.hpp" #include "Gpu_Completion_Service.hpp" #include "Render_Domain.hpp" +#include #include #include #include @@ -9,7 +10,9 @@ #include #include #include +#include #include +#include #include namespace aethera::render_3d::detail { namespace { @@ -24,83 +27,127 @@ Mouse_Button held_button(Mouse_Button_Mask buttons) { 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.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_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); } } -struct Async_Render_Backend::Implementation : std::enable_shared_from_this { +struct Async_Render_Backend::Implementation + : std::enable_shared_from_this { struct Completion { - Frame_3D* output{}; /* 调用方持有到本项回调结束。 */ - Frame_Callback callback{}; /* 提交时捕获,避免后来替换出口改变历史语义。 */ - }; - struct Pending { - std::optional backend_frame{}; - std::vector completions{}; /* 一次实际 GPU 图需要结清的全部逻辑提交。 */ - Extent extent{}; + Frame_3D* output{}; /* 调用方持有到本项回调返回。 */ + Frame_Callback callback{}; /* render 入队时捕获的完成出口。 */ }; struct Submission { - Prepared_Visual_Batch visuals{}; /* 最新提交的大批量不可变 Visual 快照。 */ - Scene_3D_Parameters parameters{}; /* 小型 Scene 参数按值复制进闭包。 */ - std::vector completions{}; /* 被最新画面合并的历史完成项。 */ + Prepared_Visual_Batch visuals{}; /* Scene CPU Prepare 发布的大批量不可变快照。 */ + Scene_3D_Parameters parameters{}; /* 本帧 Scene、Camera 与轴参数快照。 */ + std::vector completions{}; /* 合并到同一实际画面的逻辑帧。 */ }; - std::shared_ptr render_domain; - std::unique_ptr backend; - std::uint32_t gpu_index{}; - bool validation_enabled{}; - std::vector visual_registrations{}; - mutable std::mutex render_mutex; - std::optional pending_submission{}; /* 唯一待提交画面;旧大数据由新快照直接替换。 */ - Frame_Callback frame_callback{}; - bool frame_in_flight{}; - mutable std::mutex failure_mutex; - std::exception_ptr failure{}; - std::atomic_bool available{}; - Implementation(std::uint32_t gpu_index_value, bool validation_enabled_value, + struct Pending { + Datoviz_Visual_Backend::Pending_Frame backend_frame{}; /* 三缓冲目标对应的已录制提交。 */ + std::vector completions{}; /* GPU 完成时一起结清的逻辑帧。 */ + Extent extent{}; /* 失败时生成空结果所需的尺寸。 */ + }; + struct Gpu_Completion { + std::shared_ptr pending{}; /* 完成前保持帧目标和回调集合存活。 */ + std::optional result{}; /* fence 正常交付时的结果。 */ + std::exception_ptr failure{}; /* 提交域或完成服务的 Unknown Failure。 */ + }; + struct Event_Command { + std::shared_ptr event{}; /* Scene 转移给准备域的输入事件。 */ + Extent viewport{}; /* 事件产生时的物理像素视口。 */ + }; + struct Stop_Command {}; + using Command = std::variant; + + static constexpr std::size_t command_capacity = 64; + std::shared_ptr render_domain; /* 同 GPU 唯一的轻量 Queue Submit 域。 */ + std::unique_ptr backend{}; /* 仅准备线程构造、准备、收集和销毁。 */ + std::uint32_t gpu_index{}; /* Datoviz 共享 GPU Context 的设备下标。 */ + bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */ + std::vector visual_registrations{}; /* 首帧初始化后释放的稳定 Visual 注册表。 */ + moodycamel::BlockingConcurrentQueue commands{command_capacity}; /* 多生产者到单准备线程的命令入口。 */ + std::thread preparation_thread{}; /* 每 Scene 独立的 Datoviz CPU 准备域。 */ + std::optional deferred_submission{}; /* 三槽占满时仅保留的最新画面和历史回调。 */ + std::mutex callback_mutex{}; /* 保护完成回调替换与 render 捕获。 */ + Frame_Callback frame_callback{}; /* 后续 render 捕获的完成出口。 */ + std::mutex failure_mutex{}; /* 保护跨线程传播的最后 Unknown Failure。 */ + std::exception_ptr failure{}; /* 后端不可用的根因。 */ + std::atomic_bool available{true}; /* 对 Scene 发布的后端可接收状态。 */ + std::atomic_bool stop_requested{}; /* 拒绝新命令并等待在途槽结清。 */ + + Implementation(std::uint32_t gpu_index_value, + bool validation_enabled_value, std::vector visuals) : render_domain(Render_Domain::acquire(gpu_index_value)), - gpu_index(gpu_index_value), validation_enabled(validation_enabled_value), - visual_registrations(std::move(visuals)) { - available.store(true, std::memory_order_release); - } - ~Implementation() { - available.store(false, std::memory_order_release); - if (!backend) return; - const auto destroyed = render_domain->invoke([this] { backend.reset(); }); - if (!destroyed) backend.release(); - } + gpu_index(gpu_index_value), + validation_enabled(validation_enabled_value), + visual_registrations(std::move(visuals)), + preparation_thread([this] { run(); }) {} + ~Implementation() { stop(); } + + void stop() noexcept; + void run() noexcept; void fail(std::exception_ptr value) noexcept; + void enqueue(Command command); void initialize(const Scene_3D_Parameters& parameters); - void render(Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, - Frame_3D* frame); - void submit(Submission submission); - void finish(std::shared_ptr pending, - Gpu_Completion_Service::Result completion) noexcept; + void render(Prepared_Visual_Batch visuals, + Scene_3D_Parameters parameters, Frame_3D* frame); + void accept(Submission submission); + void merge_deferred(Submission submission); + void prepare(Submission submission); + void submit(std::shared_ptr pending); + void finish(Gpu_Completion completion); void resolve(std::shared_ptr pending, std::optional completed); - void complete(std::shared_ptr pending); - static std::vector frames(const Pending& pending); + void complete(std::vector completions); + void dispatch(Event_Command command); }; + Async_Render_Backend::Async_Render_Backend( std::uint32_t gpu_index, bool validation_enabled, std::vector visuals) : implementation_(std::make_shared( gpu_index, validation_enabled, std::move(visuals))) {} Async_Render_Backend::~Async_Render_Backend() = default; -void Async_Render_Backend::Implementation::fail(std::exception_ptr value) noexcept { + +void Async_Render_Backend::Implementation::stop() noexcept { + available.store(false, std::memory_order_release); + if (!stop_requested.exchange(true, std::memory_order_acq_rel)) { + try { + enqueue(Stop_Command{}); + } + catch (...) { + std::terminate(); + } + } + if (preparation_thread.joinable()) preparation_thread.join(); +} +void Async_Render_Backend::Implementation::fail( + std::exception_ptr value) noexcept { { std::lock_guard lock(failure_mutex); failure = std::move(value); } available.store(false, std::memory_order_release); } +void Async_Render_Backend::Implementation::enqueue(Command command) { + if (!commands.enqueue(std::move(command))) throw std::bad_alloc{}; +} void Async_Render_Backend::Implementation::initialize( const Scene_3D_Parameters& parameters) { if (backend) return; @@ -109,149 +156,154 @@ void Async_Render_Backend::Implementation::initialize( visual_registrations.clear(); visual_registrations.shrink_to_fit(); } -std::vector Async_Render_Backend::Implementation::frames( - const Pending& pending) { - std::vector result; - result.reserve(pending.completions.size()); - for (const auto& completion : pending.completions) - result.push_back(completion.output); - return result; -} void Async_Render_Backend::Implementation::render( Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame) { if (!frame) throw std::invalid_argument("3D backend requires a non-null external frame"); if (!available.load(std::memory_order_acquire)) return; - frame->mark(Frame_Trace_Marker::backend_queue_entered); - std::optional submission; + Frame_Callback callback; { - std::lock_guard lock(render_mutex); - Completion completion{frame, frame_callback}; - if (frame_in_flight) { - Submission latest{std::move(visuals), parameters, {}}; - if (pending_submission) - latest.completions = std::move(pending_submission->completions); - latest.completions.push_back(std::move(completion)); - pending_submission = std::move(latest); - return; - } - frame_in_flight = true; - submission.emplace(Submission{std::move(visuals), parameters, - {std::move(completion)}}); + std::lock_guard lock(callback_mutex); + callback = frame_callback; } - submit(std::move(*submission)); + frame->mark(Frame_Trace_Marker::backend_queue_entered); + enqueue(Submission{std::move(visuals), parameters, + {Completion{frame, std::move(callback)}}}); } -void Async_Render_Backend::Implementation::submit(Submission submission) { - auto self = shared_from_this(); - auto pending = std::make_shared(); - pending->completions = std::move(submission.completions); - pending->extent = submission.parameters.viewport; - const auto sequence = pending->completions.back().output->identity().sequence; - struct Submission_Task { - Prepared_Visual_Batch visuals; - Scene_3D_Parameters parameters; - std::uint64_t sequence{}; - std::shared_ptr pending; - }; - auto task = std::make_shared(Submission_Task{ - std::move(submission.visuals), submission.parameters, sequence, pending}); - const auto queued = render_domain->post( - [self, task] { - for (const auto& completion : task->pending->completions) - completion.output->mark(Frame_Trace_Marker::backend_queue_left); - self->initialize(task->parameters); - const bool readback = std::ranges::any_of( - task->pending->completions, - [](const Completion& completion) { - return completion.output->output() == Frame_3D_Output::pixels; - }); - auto reservation = Gpu_Completion_Service::instance().prepare( - [self, pending = task->pending](Gpu_Completion_Service::Result result) { - self->finish(pending, std::move(result)); - }, - [self, pending = task->pending](std::exception_ptr value) { - self->fail(std::move(value)); - self->resolve(pending, std::nullopt); - }, true); - if (!reservation) { - self->resolve(task->pending, std::nullopt); - return; - } - task->pending->backend_frame = self->backend->submit( - task->parameters, task->visuals, task->sequence, true, readback); - if (!task->pending->backend_frame) { - self->resolve(task->pending, std::nullopt); - return; - } - for (const auto& completion : task->pending->completions) { - record_datoviz_trace(completion.output, - task->pending->backend_frame->trace); - completion.output->mark(Frame_Trace_Marker::gpu_submitted); - } - reservation.reservation.watch(task->pending->backend_frame->device, - task->pending->backend_frame->fence); - }, - [self, pending](std::exception_ptr value) { - self->fail(std::move(value)); - self->resolve(pending, std::nullopt); - }); - if (queued != Render_Domain::Post_Result::queued) { - fail(std::make_exception_ptr( - std::runtime_error("render domain stopped before 3D submission"))); +void Async_Render_Backend::Implementation::merge_deferred( + Submission submission) { + if (deferred_submission) { + submission.completions.insert( + submission.completions.begin(), + std::make_move_iterator(deferred_submission->completions.begin()), + std::make_move_iterator(deferred_submission->completions.end())); + } + deferred_submission = std::move(submission); +} +void Async_Render_Backend::Implementation::accept(Submission submission) { + if (stop_requested.load(std::memory_order_acquire) || + !available.load(std::memory_order_acquire)) { + auto pending = std::make_shared(); + pending->extent = submission.parameters.viewport; + pending->completions = std::move(submission.completions); resolve(std::move(pending), std::nullopt); + return; } + if (backend && + !backend->can_prepare(submission.parameters, submission.visuals)) { + merge_deferred(std::move(submission)); + return; + } + prepare(std::move(submission)); } -void Async_Render_Backend::Implementation::finish( - std::shared_ptr pending, - Gpu_Completion_Service::Result completion) noexcept { - for (const auto& item : pending->completions) { - item.output->mark(Frame_Trace_Marker::gpu_completed); - item.output->record(Frame_Trace_Measurement::gpu_fence_wait_ns, - completion.wait_duration_ns); - } - auto self = shared_from_this(); +void Async_Render_Backend::Implementation::prepare(Submission submission) { + std::optional prepared; try { - const auto result = render_domain->post( - [self, pending, completion] { - if (!pending->backend_frame) { - self->resolve(pending, std::nullopt); - return; - } - if (completion.error != - Gpu_Completion_Service::Completion_Error::none) { - self->backend->discard(std::move(*pending->backend_frame)); - pending->backend_frame.reset(); - self->resolve(pending, std::nullopt); - return; - } - for (const auto& item : pending->completions) - item.output->mark(Frame_Trace_Marker::readback_started); - auto completed = self->backend->collect( - std::move(*pending->backend_frame)); - pending->backend_frame.reset(); - self->resolve(pending, std::move(completed)); - }, - [self, pending](std::exception_ptr value) { - self->fail(std::move(value)); - self->resolve(pending, std::nullopt); + initialize(submission.parameters); + for (const auto& completion : submission.completions) + completion.output->mark(Frame_Trace_Marker::backend_prepare_started); + const auto sequence = + submission.completions.back().output->identity().sequence; + const bool readback = std::ranges::any_of( + submission.completions, [](const Completion& completion) { + return completion.output->output() == Frame_3D_Output::pixels; }); - if (result != Render_Domain::Post_Result::queued) { - fail(std::make_exception_ptr(std::runtime_error( - "render domain stopped before GPU completion collection"))); - resolve(std::move(pending), std::nullopt); - } + prepared = backend->prepare( + submission.parameters, submission.visuals, sequence, true, readback); } catch (...) { fail(std::current_exception()); + auto pending = std::make_shared(); + pending->extent = submission.parameters.viewport; + pending->completions = std::move(submission.completions); resolve(std::move(pending), std::nullopt); + return; + } + if (!prepared) { + merge_deferred(std::move(submission)); + return; + } + for (const auto& completion : submission.completions) { + completion.output->mark(Frame_Trace_Marker::backend_prepare_finished); + record_datoviz_trace(completion.output, prepared->trace); + completion.output->mark(Frame_Trace_Marker::backend_submit_queued); + } + auto pending = std::make_shared(); + pending->backend_frame = std::move(*prepared); + pending->extent = submission.parameters.viewport; + pending->completions = std::move(submission.completions); + try { + submit(pending); + } + catch (...) { + backend->discard(std::move(pending->backend_frame)); + fail(std::current_exception()); + resolve(std::move(pending), std::nullopt); + } +} +void Async_Render_Backend::Implementation::submit( + std::shared_ptr pending) { + auto self = shared_from_this(); + const auto queued = render_domain->post( + [self, pending] { + for (const auto& completion : pending->completions) + completion.output->mark(Frame_Trace_Marker::backend_queue_left); + auto reservation = Gpu_Completion_Service::instance().prepare( + [self, pending](Gpu_Completion_Service::Result result) { + try { + self->enqueue(Gpu_Completion{pending, result, {}}); + } + catch (...) { + self->fail(std::current_exception()); + } + }, + [self, pending](std::exception_ptr value) { + try { + self->enqueue(Gpu_Completion{pending, std::nullopt, + std::move(value)}); + } + catch (...) { + self->fail(std::current_exception()); + } + }, true); + if (!reservation) { + self->enqueue(Gpu_Completion{ + pending, std::nullopt, + std::make_exception_ptr(std::runtime_error( + "GPU completion service stopped before submission"))}); + return; + } + self->backend->submit(pending->backend_frame); + for (const auto& completion : pending->completions) + completion.output->mark(Frame_Trace_Marker::gpu_submitted); + reservation.reservation.watch(pending->backend_frame.device, + pending->backend_frame.fence); + }, + [self, pending](std::exception_ptr value) { + try { + self->enqueue(Gpu_Completion{pending, std::nullopt, + std::move(value)}); + } + catch (...) { + self->fail(std::current_exception()); + } + }); + if (queued != Render_Domain::Post_Result::queued) { + enqueue(Gpu_Completion{ + std::move(pending), std::nullopt, + std::make_exception_ptr(std::runtime_error( + "render domain stopped before 3D queue submission"))}); } } void Async_Render_Backend::Implementation::resolve( std::shared_ptr pending, std::optional completed) { + std::vector outputs; + outputs.reserve(pending->completions.size()); + for (const auto& completion : pending->completions) + outputs.push_back(completion.output); if (completed) { - auto outputs = frames(*pending); for (Frame_3D* output : outputs) { output->mark(Frame_Trace_Marker::readback_finished); record_datoviz_trace(output, completed->trace); @@ -262,48 +314,14 @@ void Async_Render_Backend::Implementation::resolve( else { const auto pixel_count = static_cast(pending->extent.width) * pending->extent.height * 4U; - auto outputs = frames(*pending); - Frame_3D_Access::assign_pixels(outputs, pending->extent, - std::vector(pixel_count)); + Frame_3D_Access::assign_pixels( + outputs, pending->extent, std::vector(pixel_count)); } - complete(std::move(pending)); + complete(std::move(pending->completions)); } void Async_Render_Backend::Implementation::complete( - std::shared_ptr pending) { - std::optional next; - const std::size_t rendered_count = pending->completions.size(); - { - std::lock_guard lock(render_mutex); - frame_in_flight = false; - if (pending_submission && available.load(std::memory_order_acquire)) { - next = std::move(pending_submission); - pending_submission.reset(); - frame_in_flight = true; - } - else if (pending_submission) { - auto historical = std::move(pending_submission->completions); - pending_submission.reset(); - pending->completions.insert( - pending->completions.end(), - std::make_move_iterator(historical.begin()), - std::make_move_iterator(historical.end())); - } - } - if (pending->completions.size() > rendered_count && rendered_count != 0) { - Frame_3D* source = pending->completions.front().output; - for (std::size_t index = rendered_count; - index < pending->completions.size(); ++index) { - try { - Frame_3D_Access::share_pixels( - source, pending->completions[index].output); - } - catch (...) { - std::lock_guard lock(failure_mutex); - failure = std::current_exception(); - } - } - } - for (auto& item : pending->completions) { + std::vector completions) { + for (auto& item : completions) { if (!item.output || !item.callback) continue; item.output->mark(Frame_Trace_Marker::callback_started); try { @@ -315,15 +333,127 @@ void Async_Render_Backend::Implementation::complete( } item.output->mark(Frame_Trace_Marker::callback_finished); } - if (next) submit(std::move(*next)); } +void Async_Render_Backend::Implementation::finish(Gpu_Completion completion) { + for (const auto& item : completion.pending->completions) { + item.output->mark(Frame_Trace_Marker::gpu_completed); + if (completion.result) + item.output->record(Frame_Trace_Measurement::gpu_fence_wait_ns, + completion.result->wait_duration_ns); + item.output->mark(Frame_Trace_Marker::readback_started); + } + std::optional completed; + if (completion.failure) { + fail(std::move(completion.failure)); + backend->discard(std::move(completion.pending->backend_frame)); + } + else if (!completion.result || + completion.result->error != + Gpu_Completion_Service::Completion_Error::none) { + backend->discard(std::move(completion.pending->backend_frame)); + } + else { + completed = backend->collect( + std::move(completion.pending->backend_frame)); + } + resolve(std::move(completion.pending), std::move(completed)); + if (deferred_submission && + !stop_requested.load(std::memory_order_acquire) && + available.load(std::memory_order_acquire)) { + auto next = std::move(*deferred_submission); + deferred_submission.reset(); + prepare(std::move(next)); + } + else if (deferred_submission && + !available.load(std::memory_order_acquire)) { + auto pending = std::make_shared(); + pending->extent = deferred_submission->parameters.viewport; + pending->completions = std::move(deferred_submission->completions); + deferred_submission.reset(); + resolve(std::move(pending), std::nullopt); + } +} +void Async_Render_Backend::Implementation::dispatch(Event_Command command) { + if (!backend || !command.event) return; + const auto* pointer = + dynamic_cast(command.event.get()); + const auto* wheel = + dynamic_cast(command.event.get()); + const auto* key = dynamic_cast(command.event.get()); + if (wheel) { + backend->dispatch_wheel( + static_cast(pointer->position_x()), + static_cast(pointer->position_y()), + wheel_step(wheel->pixel_delta_x_value(), + wheel->angle_delta_x_value()), + wheel_step(wheel->pixel_delta_y_value(), + wheel->angle_delta_y_value()), + pointer->keyboard_modifiers(), command.viewport); + } + else if (pointer) { + backend->dispatch_pointer( + command.event->type, + static_cast(pointer->position_x()), + static_cast(pointer->position_y()), + command.event->type == Event_Type::pointer_move + ? held_button(pointer->pointer_buttons()) + : pointer->pointer_button(), + pointer->keyboard_modifiers(), command.viewport); + } + else if (key) backend->dispatch_key(*key); + command.event->accept(); +} +void Async_Render_Backend::Implementation::run() noexcept { + bool stopping{}; + for (;;) { + Command command; + commands.wait_dequeue(command); + try { + if (auto* submission = std::get_if(&command)) + accept(std::move(*submission)); + else if (auto* completion = std::get_if(&command)) + finish(std::move(*completion)); + else if (auto* event = std::get_if(&command)) + dispatch(std::move(*event)); + else stopping = true; + } + catch (...) { + fail(std::current_exception()); + if (auto* submission = std::get_if(&command)) { + auto pending = std::make_shared(); + pending->extent = submission->parameters.viewport; + pending->completions = std::move(submission->completions); + try { resolve(std::move(pending), std::nullopt); } + catch (...) { fail(std::current_exception()); } + } + else if (auto* completion = std::get_if(&command)) { + try { resolve(std::move(completion->pending), std::nullopt); } + catch (...) { fail(std::current_exception()); } + } + } + if (stopping && (!backend || backend->idle())) { + if (deferred_submission) { + auto pending = std::make_shared(); + pending->extent = deferred_submission->parameters.viewport; + pending->completions = + std::move(deferred_submission->completions); + deferred_submission.reset(); + try { resolve(std::move(pending), std::nullopt); } + catch (...) { fail(std::current_exception()); } + } + backend.reset(); + return; + } + } +} + void Async_Render_Backend::render( Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame) { implementation_->render(std::move(visuals), parameters, frame); } void Async_Render_Backend::set_frame_callback(Frame_Callback callback) { - std::lock_guard lock(implementation_->render_mutex); + std::lock_guard lock(implementation_->callback_mutex); implementation_->frame_callback = std::move(callback); } bool Async_Render_Backend::available() const noexcept { @@ -353,39 +483,13 @@ Async_Render_Backend::dispatch_event(std::shared_ptr event, return Dispatch_Event_Result::invalid_event; } else if (!key) return Dispatch_Event_Result::ignored; - const auto queued = self->render_domain->post( - [self, event = std::move(event), viewport] { - if (!self->backend) return; - if (const auto* event_wheel = - dynamic_cast(event.get())) { - const auto* event_pointer = - dynamic_cast(event.get()); - self->backend->dispatch_wheel( - static_cast(event_pointer->position_x()), - static_cast(event_pointer->position_y()), - wheel_step(event_wheel->pixel_delta_x_value(), - event_wheel->angle_delta_x_value()), - wheel_step(event_wheel->pixel_delta_y_value(), - event_wheel->angle_delta_y_value()), - event_pointer->keyboard_modifiers(), viewport); - } - else if (const auto* event_pointer = - dynamic_cast(event.get())) - self->backend->dispatch_pointer( - event->type, - static_cast(event_pointer->position_x()), - static_cast(event_pointer->position_y()), - event->type == Event_Type::pointer_move - ? held_button(event_pointer->pointer_buttons()) - : event_pointer->pointer_button(), - event_pointer->keyboard_modifiers(), viewport); - else if (const auto* event_key = dynamic_cast(event.get())) - self->backend->dispatch_key(*event_key); - event->accept(); - }, - [self](std::exception_ptr value) { self->fail(std::move(value)); }); - return queued == Render_Domain::Post_Result::queued - ? Dispatch_Event_Result::queued - : Dispatch_Event_Result::backend_unavailable; + try { + self->enqueue(Implementation::Event_Command{std::move(event), viewport}); + } + catch (...) { + self->fail(std::current_exception()); + return Dispatch_Event_Result::backend_unavailable; + } + return Dispatch_Event_Result::queued; } } diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp index 3dd0864..f3400b9 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp @@ -201,6 +201,7 @@ public: } [[nodiscard]] DvzGpuCtx* gpu_context() const noexcept { return gpu_context_; } + [[nodiscard]] std::mutex& api_mutex() noexcept { return api_mutex_; } private: Datoviz_Render_Context(std::uint32_t gpu_index, bool validation_enabled) { @@ -214,6 +215,7 @@ private: } DvzGpuCtx* gpu_context_{}; /* 共享 GPU Device 与分配器的唯一所有权。 */ + std::mutex api_mutex_{}; /* 同 GPU Datoviz/Vulkan API 与 Frame Target 状态的唯一串行化域。 */ }; class Datoviz_Visual_Backend::Frame_Target final { @@ -278,7 +280,8 @@ public: destroy(); } void begin(bool observe, bool readback) { - if (in_flight_) throw std::logic_error("Datoviz frame target is still in flight"); + if (recording_ || prepared_ || in_flight_) + throw std::logic_error("Datoviz frame target is not available"); observing_ = observe; readback_requested_ = readback; if (observing_ && !timestamps_initialized_) { @@ -352,7 +355,7 @@ public: frame.wait_semaphore_fd = -1; return frame; } - void submit() { + 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_) { @@ -414,10 +417,16 @@ public: dvz_fence_reset(fence_); dvz_submit(submit_); dvz_submit_command(submit_, dvz_commands_handle(commands_)); + prepared_ = true; + } + void submit() { + if (!prepared_ || in_flight_) + throw std::logic_error("Datoviz frame target has no prepared submission"); DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN); if (dvz_submit_send(submit_, dvz_queue_handle(queue), dvz_fence_handle(fence_)) != VK_SUCCESS) throw std::runtime_error("failed to submit Datoviz point frame"); + prepared_ = false; in_flight_ = true; completed_layout_ = readback_requested_ ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; } @@ -443,7 +452,14 @@ public: return result; } void discard_after_completion() { - if (!in_flight_) throw std::logic_error("Datoviz frame target has no pending frame"); + if (prepared_) { + prepared_ = false; + observing_ = false; + readback_requested_ = false; + return; + } + if (!in_flight_) + throw std::logic_error("Datoviz frame target has no pending frame"); in_flight_ = false; observing_ = false; readback_requested_ = false; @@ -457,9 +473,14 @@ public: [[nodiscard]] std::uint64_t generation() const noexcept { return generation_; } + [[nodiscard]] Extent extent() const noexcept { return extent_; } + [[nodiscard]] bool available() const noexcept { + return !recording_ && !prepared_ && !in_flight_; + } void abort() noexcept { if (recording_ && commands_ != nullptr) dvz_cmd_reset(commands_); recording_ = false; + prepared_ = false; observing_ = false; readback_requested_ = false; } @@ -544,9 +565,9 @@ private: return timing; } void destroy() { - if (in_flight_) + if (recording_ || prepared_ || in_flight_) throw std::logic_error( - "Datoviz frame target is still in flight during destruction"); + "Datoviz frame target is busy during destruction"); if (query_pool_ != VK_NULL_HANDLE && gpu_context_ != nullptr) { vkDestroyQueryPool( dvz_device_handle(dvz_gpu_ctx_device(gpu_context_)), @@ -598,18 +619,26 @@ private: float timestamp_period_ns_{}; std::uint32_t timestamp_valid_bits_{}; bool recording_{}; + bool prepared_{}; bool in_flight_{}; bool observing_{}; bool readback_requested_{}; bool timestamps_initialized_{}; bool timestamps_supported_{}; }; +struct Datoviz_Visual_Backend::Frame_Targets { + static constexpr std::size_t count = 3; + std::array, count> values{}; /* 固定三槽,槽地址在后端生命周期内稳定。 */ +}; Datoviz_Visual_Backend::Datoviz_Visual_Backend( std::uint32_t gpu_index, bool validation_enabled, const std::vector& visuals, - const Scene_3D_Parameters& initial_scene) : domain_thread_(std::this_thread::get_id()) { + const Scene_3D_Parameters& initial_scene) + : domain_thread_(std::this_thread::get_id()), + targets_(std::make_unique()) { try { render_context_ = Datoviz_Render_Context::acquire(gpu_index, validation_enabled); + std::lock_guard api_lock(render_context_->api_mutex()); auto* gpu_context = render_context_->gpu_context(); DvzDrp2RuntimeConfig runtime_configuration = dvz_drp2_runtime_vklite_config( dvz_gpu_ctx_device(gpu_context), dvz_gpu_ctx_alloc(gpu_context)); @@ -1082,7 +1111,7 @@ void Datoviz_Visual_Backend::apply_camera(const plot::Camera_Descriptor& source) void Datoviz_Visual_Backend::apply( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared) { require_domain(); - if (target_extent_ != scene.viewport) { + if (figure_extent_ != scene.viewport) { if (dvz_figure_resize(figure_, scene.viewport.width, scene.viewport.height) != DVZ_OK) throw std::runtime_error("failed to resize Datoviz point figure"); @@ -1091,6 +1120,7 @@ void Datoviz_Visual_Backend::apply( scene.viewport.width, scene.viewport.height, 1.0F, 1.0F }; dvz_input_emit_resize(input_router_, &resize); + figure_extent_ = scene.viewport; } apply_camera(scene.camera); apply_axes(scene); @@ -1306,6 +1336,8 @@ void Datoviz_Visual_Backend::dispatch_pointer( ::aethera::Mouse_Button mouse_button, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) { require_domain(); + std::lock_guard api_lock(render_context_->api_mutex()); + input_changed_ = true; if (applied_camera_ && applied_camera_->controller == plot::Camera_Controller::turntable) { if (mouse_button == ::aethera::Mouse_Button::left && !applied_camera_->turntable_control.rotate_enabled) return; @@ -1330,6 +1362,8 @@ void Datoviz_Visual_Backend::dispatch_wheel( float x, float y, float delta_x, float delta_y, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) { require_domain(); + std::lock_guard api_lock(render_context_->api_mutex()); + input_changed_ = true; if (applied_camera_ && applied_camera_->controller == plot::Camera_Controller::turntable && !applied_camera_->turntable_control.zoom_enabled) return; @@ -1341,6 +1375,8 @@ void Datoviz_Visual_Backend::dispatch_wheel( void Datoviz_Visual_Backend::dispatch_key( const ::aethera::Key_Event& event) { require_domain(); + std::lock_guard api_lock(render_context_->api_mutex()); + input_changed_ = true; if (event.key == ::aethera::Key::home && event.type == ::aethera::Event_Type::key_press) { DvzResult reset = DVZ_ERROR; switch (dvz_controller_type(camera_controller_)) { @@ -1400,11 +1436,38 @@ DvzSceneFrameArtifact* Datoviz_Visual_Backend::emit( } return artifact; } -std::optional Datoviz_Visual_Backend::submit( +std::optional Datoviz_Visual_Backend::acquire_target(Extent extent) { + for (std::uint8_t index = 0; index < targets_->values.size(); ++index) { + auto& candidate = targets_->values[index]; + if (candidate && candidate->available() && candidate->extent() == extent) + return index; + } + for (std::uint8_t index = 0; index < targets_->values.size(); ++index) { + auto& candidate = targets_->values[index]; + if (candidate && !candidate->available()) continue; + candidate = std::make_unique( + render_context_->gpu_context(), extent, ++target_generation_); + return index; + } + return std::nullopt; +} +Datoviz_Visual_Backend::Frame_Target& Datoviz_Visual_Backend::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::prepare( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback) { require_domain(); + std::lock_guard api_lock(render_context_->api_mutex()); if (scene.viewport.empty()) return std::nullopt; + 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; @@ -1442,13 +1505,13 @@ std::optional Datoviz_Visual_Backend::sub } } if (observe) trace.apply_ns = trace_now_ns() - phase_started; - if (target_ == nullptr || target_extent_ != scene.viewport) { - target_.reset(); - target_extent_ = scene.viewport; - target_ = std::make_unique(render_context_->gpu_context(), target_extent_, - ++target_generation_); - } - target_->begin(observe, readback); + auto& frame_target = *targets_->values[*target_index]; + frame_target.begin(observe, readback); + struct Recording_Scope { + Frame_Target& target; /* 异常退出时回收尚未发布的录制槽。 */ + bool released{}; /* finish_recording 成功后禁止回滚。 */ + ~Recording_Scope() { if (!released) target.abort(); } + } recording_scope{frame_target}; DvzSceneFrameArtifact* artifact{}; try { if (observe) phase_started = trace_now_ns(); @@ -1456,7 +1519,7 @@ std::optional Datoviz_Visual_Backend::sub if (observe) trace.emit_ns = trace_now_ns() - phase_started; } catch (...) { - target_->abort(); + frame_target.abort(); raise_context("preparing Datoviz frame", std::current_exception()); } if (observe) { @@ -1468,7 +1531,7 @@ std::optional Datoviz_Visual_Backend::sub dvz_scene_frame_artifact_frame_index(artifact); } const DvzDrp2CommandStream* stream = dvz_scene_frame_artifact_stream(artifact); - const DvzStreamFrame target_frame = target_->stream_frame(); + const DvzStreamFrame target_frame = frame_target.stream_frame(); if (observe) phase_started = trace_now_ns(); const bool attached = stream != nullptr && dvz_drp2_runtime_attach_frame_target( @@ -1481,7 +1544,7 @@ std::optional Datoviz_Visual_Backend::sub trace.validation_ok = attached && result.ok; trace.validation_code = static_cast(result.code); trace.validation_command_index = result.command_index; - if (observe || !trace.validation_ok) { + if (!trace.validation_ok) { if (char* json = dvz_scene_frame_artifact_json( artifact, "renderive_frame")) { trace.artifact_json = json; @@ -1490,11 +1553,11 @@ std::optional Datoviz_Visual_Backend::sub } dvz_scene_frame_artifact_destroy(artifact); if (!attached) { - target_->abort(); + frame_target.abort(); throw std::runtime_error("failed to attach the Datoviz point frame target"); } if (!result.ok) { - target_->abort(); + frame_target.abort(); std::string message = "failed to execute Datoviz point frame: validation code " + std::to_string(static_cast(result.code)) + @@ -1506,45 +1569,89 @@ std::optional Datoviz_Visual_Backend::sub } throw std::runtime_error(std::move(message)); } - if (observe) phase_started = trace_now_ns(); - target_->submit(); - if (observe) trace.submit_ns = trace_now_ns() - phase_started; + frame_target.finish_recording(); + recording_scope.released = true; + input_changed_ = false; return Pending_Frame{ - target_->device(), target_->fence(), scene.viewport, - frame_sequence, target_->generation(), + frame_target.device(), frame_target.fence(), scene.viewport, + frame_sequence, *target_index, frame_target.generation(), std::move(trace) }; } +void Datoviz_Visual_Backend::submit(Pending_Frame& pending) { + std::lock_guard api_lock(render_context_->api_mutex()); + const std::uint64_t started = pending.trace.observed ? trace_now_ns() : 0; + target(pending).submit(); + if (pending.trace.observed) + pending.trace.submit_ns = trace_now_ns() - started; +} Datoviz_Visual_Backend::Completed_Frame Datoviz_Visual_Backend::collect( Pending_Frame pending) { require_domain(); - if (target_ == nullptr || - target_->generation() != pending.target_generation) - throw std::logic_error("Datoviz pending frame target no longer exists"); + std::lock_guard api_lock(render_context_->api_mutex()); const std::uint64_t readback_started = pending.trace.observed ? trace_now_ns() : 0; - auto collection = target_->collect(); + 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)}; } void Datoviz_Visual_Backend::discard(Pending_Frame pending) { require_domain(); - if (target_ == nullptr || - target_->generation() != pending.target_generation) - throw std::logic_error("Datoviz pending frame target no longer exists"); - target_->discard_after_completion(); + std::lock_guard api_lock(render_context_->api_mutex()); + target(pending).discard_after_completion(); +} +bool Datoviz_Visual_Backend::can_prepare( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& prepared) const { + if (!render_context_) return false; + std::lock_guard api_lock(render_context_->api_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; + if (input_changed_ || figure_extent_ != scene.viewport || + !applied_camera_ || *applied_camera_ != scene.camera || + !applied_axes_ || *applied_axes_ != + std::array{scene.x_axis, scene.y_axis, scene.z_axis}) + return false; + for (const auto& source : prepared) { + const auto target = std::ranges::find_if( + visuals_, [&](const Visual_Instance& value) { + return value.identity == source.identity; + }); + if (target == visuals_.end() || + target->applied_revision != source.visual.revision) + return false; + } + return prepared.size() == visuals_.size(); +} +bool Datoviz_Visual_Backend::idle() const { + if (!render_context_) return true; + std::lock_guard api_lock(render_context_->api_mutex()); + return std::ranges::all_of(targets_->values, [](const auto& target) { + return !target || target->available(); + }); } void Datoviz_Visual_Backend::destroy() { if (std::this_thread::get_id() != domain_thread_) throw std::logic_error( "Datoviz backend may only be destroyed on the render domain"); + auto context = render_context_; + std::unique_lock api_lock; + if (context) api_lock = std::unique_lock(context->api_mutex()); if (runtime_ != nullptr) { dvz_drp2_runtime_destroy(runtime_); runtime_ = nullptr; } - target_.reset(); + for (auto& target : targets_->values) target.reset(); if (camera_controller_ != nullptr) { dvz_controller_destroy(camera_controller_); camera_controller_ = nullptr; @@ -1578,6 +1685,8 @@ void Datoviz_Visual_Backend::destroy() { scene_ = nullptr; } render_context_.reset(); + if (api_lock.owns_lock()) api_lock.unlock(); + context.reset(); } } // 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 0016068..cb149d4 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp @@ -23,6 +23,7 @@ public: 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; /* 本帧可选诊断数据。 */ }; @@ -37,11 +38,16 @@ public: ~Datoviz_Visual_Backend() noexcept(false); Datoviz_Visual_Backend(const Datoviz_Visual_Backend&) = delete; Datoviz_Visual_Backend& operator=(const Datoviz_Visual_Backend&) = delete; - [[nodiscard]] std::optional submit( + [[nodiscard]] std::optional prepare( 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); + [[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, @@ -52,6 +58,7 @@ public: void dispatch_key(const Key_Event& event); private: class Frame_Target; + struct Frame_Targets; struct Visual_Instance { Visual_Identity identity{}; /* Scene 注册的稳定身份。 */ Visual_Family family{Visual_Family::point}; /* 原生 Visual 的确定 family。 */ @@ -68,6 +75,8 @@ private: void apply(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals); void apply_visual(Visual_Instance& target, const Prepared_Visual& visual); [[nodiscard]] DvzSceneFrameArtifact* emit(const Scene_3D_Parameters& scene); + [[nodiscard]] std::optional acquire_target(Extent extent); + [[nodiscard]] Frame_Target& target(const Pending_Frame& pending); void destroy(); std::thread::id domain_thread_; /* 唯一允许访问 Datoviz 对象的线程。 */ std::shared_ptr render_context_; /* 同一 GPU 上所有后端共享的 Device 与分配器。 */ @@ -83,11 +92,12 @@ private: DvzController* camera_controller_{}; /* 当前绑定到 Panel 的 Datoviz 原生相机控制器。 */ DvzInputRouter* input_router_{}; /* Scene 输入事件路由器。 */ DvzPointerGestureHandler* gesture_handler_{}; /* 指针手势解析器。 */ - std::unique_ptr target_; /* 当前尺寸对应的离屏提交和读回目标。 */ - Extent target_extent_{}; /* target_ 当前适配的像素尺寸。 */ + std::unique_ptr targets_; /* 三个可并行处于准备、GPU 和读回阶段的目标。 */ + Extent figure_extent_{}; /* Datoviz Figure 当前应用的像素尺寸。 */ std::uint64_t target_generation_{}; /* 每次重建 target_ 时递增的资源代次。 */ std::optional applied_camera_{}; /* 已应用到 Panel 的 Camera 配置。 */ std::optional> applied_axes_{}; /* 已生成 Visual 的轴描述快照。 */ + bool input_changed_{}; /* 输入控制器是否产生尚未录入完成帧的资源更新。 */ }; } // namespace aethera::render_3d::detail diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index aa1b803..5a57551 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -41,7 +41,7 @@ enum class Frame_Pacing_Mode : std::uint32_t { maximum_rate }; struct Frame_Pacing_Properties { - Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; /* 控制后继 render 请求节奏的策略。 */ + Frame_Pacing_Mode mode{Frame_Pacing_Mode::maximum_rate}; /* 控制后继 render 请求节奏的策略。 */ double fixed_rate_fps{30.0}; /* 固定频率策略的目标帧率,单位为 FPS。 */ double minimum_latency_headroom{1.25}; /* 最低延迟策略相对 P95 生成耗时的安全系数。 */ }; @@ -278,10 +278,9 @@ struct Plot::Private { std::unordered_map handlers; /* 以连接身份索引的完成帧订阅。 */ std::uint64_t next_frame_sequence{1}; /* 下一外部帧使用的单调序号;只在 strand 访问。 */ Frame_Policy frame_policy{}; /* 仅保存可编辑刷新策略,不保存衍生统计。 */ - std::optional active_frame{}; /* 当前由 Scene/异步后端借用指针的外部帧。 */ + std::unordered_map active_frames{}; /* Scene 借用中的外部帧,以回调原地址为唯一索引。 */ std::deque pending_order{}; /* 按首次等待顺序保存连接身份,避免连接间饥饿。 */ std::unordered_map pending_frames{}; /* 每个连接只保留最新一个尚未提交 Scene 的外部帧。 */ - bool frame_in_flight{}; /* Scene 是否已有一次尚未完成回调的帧。 */ template Private(asio::any_io_executor executor, std::unique_ptr value_scene, @@ -301,7 +300,8 @@ struct Plot::Private { void request_frame(Frame_Submission submission); void render_frame(Managed_Frame frame); void publish_completed_frame(Render_Frame* frame); - void frame_completed(); + void frame_completed(Render_Frame* frame); + [[nodiscard]] std::size_t frame_capacity() const noexcept; }; nlohmann::json Plot::Private::schema() const { auto result = view->schema(); @@ -329,7 +329,7 @@ Plot::Private::Managed_Frame Plot::Private::make_frame(Frame_Submission submissi } void Plot::Private::request_frame(Frame_Submission submission) { auto frame = make_frame(std::move(submission)); - if (frame_in_flight) { + if (active_frames.size() >= frame_capacity()) { const auto found = pending_frames.find(frame.owner); if (found != pending_frames.end()) found->second = std::move(frame); else { @@ -342,47 +342,54 @@ void Plot::Private::request_frame(Frame_Submission submission) { render_frame(std::move(frame)); } void Plot::Private::render_frame(Managed_Frame frame) { - frame_in_flight = true; - active_frame = std::move(frame); - auto& request = active_frame->request; + Render_Frame* identity = std::visit( + [](const auto& value) -> Render_Frame* { return value.get(); }, frame.frame); + auto [active, inserted] = active_frames.emplace(identity, std::move(frame)); + if (!inserted) throw std::logic_error("Plot received a duplicate frame address"); + auto& managed = active->second; + auto& request = managed.request; request.width = std::clamp(request.width, 160U, 1920U); request.height = std::clamp(request.height, 120U, 1080U); view->update(request); if (auto* scene_2d = std::get_if>(&scene)) { (*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(Size{static_cast(request.width), static_cast(request.height)}); - const auto result = (*scene_2d)->render(std::get>(active_frame->frame).get()); + const auto result = (*scene_2d)->render(std::get>(managed.frame).get()); if (result != Render_Scene_2D::Render_Result::completed) - asio::post(strand, [this] { - frame_completed(); + asio::post(strand, [this, identity] { + frame_completed(identity); }); return; } auto& scene_3d = std::get>(scene); scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{request.width, request.height}); - const auto result = scene_3d->render(std::get>(active_frame->frame).get()); + const auto result = scene_3d->render(std::get>(managed.frame).get()); if (result != Render_Scene_3D::Render_Result::submitted) - asio::post(strand, [this] { - frame_completed(); + asio::post(strand, [this, identity] { + frame_completed(identity); }); } void Plot::Private::publish_completed_frame(Render_Frame* frame) { - if (!active_frame) throw std::logic_error("frame callback has no externally owned active frame"); + const auto active = active_frames.find(frame); + if (active == active_frames.end()) + throw std::logic_error("frame callback has no externally owned active frame"); + auto& managed = active->second; const auto pacing = frame_policy.snapshot(); - if (auto* frame_2d = std::get_if>(&active_frame->frame); frame_2d && frame_2d->get() == frame) publish(active_frame->owner, encode_frame(frame_2d->get(), active_frame->request.delivery, pacing)); - else if (auto* frame_3d = std::get_if>(&active_frame->frame); frame_3d && frame_3d->get() == frame) publish(active_frame->owner, encode_frame(frame_3d->get(), active_frame->request.delivery, pacing)); + if (auto* frame_2d = std::get_if>(&managed.frame); frame_2d && frame_2d->get() == frame) publish(managed.owner, encode_frame(frame_2d->get(), managed.request.delivery, pacing)); + else if (auto* frame_3d = std::get_if>(&managed.frame); frame_3d && frame_3d->get() == frame) publish(managed.owner, encode_frame(frame_3d->get(), managed.request.delivery, pacing)); else throw std::logic_error("frame callback does not match the externally owned active frame"); - frame_completed(); + frame_completed(frame); } -void Plot::Private::frame_completed() { - active_frame.reset(); - frame_in_flight = false; - while (!pending_order.empty()) { +std::size_t Plot::Private::frame_capacity() const noexcept { + return std::holds_alternative>(scene) ? 3U : 1U; +} +void Plot::Private::frame_completed(Render_Frame* frame) { + active_frames.erase(frame); + while (active_frames.size() < frame_capacity() && !pending_order.empty()) { const auto owner = pending_order.front(); pending_order.pop_front(); auto next = pending_frames.extract(owner); if (next.empty()) continue; render_frame(std::move(next.mapped())); - return; } } Plot::Plot(asio::any_io_executor executor, diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 2aeddc0..2e5bbad 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -281,16 +281,24 @@ function pipeline_stage_values(values: Frame_Stage_Values, dimension: Plot["dime stages.pipeline_3d_scene_coordination_ms = take(Math.max(0, scene - event - prepare - paint)); const scene_finished = values.marker_scene_render_finished_ms ?? 0; const queue_entered = values.marker_backend_queue_entered_ms ?? scene_finished; - const queue_left = values.marker_backend_queue_left_ms ?? scene_finished; - stages.pipeline_3d_backend_queue_ms = take(Math.max(0, queue_left - Math.max(scene_finished, queue_entered))); + const prepare_started = values.marker_backend_prepare_started_ms ?? queue_entered; + const prepare_finished = values.marker_backend_prepare_finished_ms ?? prepare_started; + const submit_queued = values.marker_backend_submit_queued_ms ?? prepare_finished; + const queue_left = values.marker_backend_queue_left_ms ?? submit_queued; + stages.pipeline_3d_prepare_queue_ms = take(Math.max(0, prepare_started - Math.max(scene_finished, queue_entered))); + let preparation_window = Math.max(0, prepare_finished - prepare_started); + const take_preparation = (key: string) => { const value = Math.min(preparation_window, Math.max(0, values[key] ?? 0)); preparation_window -= value; return take(value); }; + stages.pipeline_3d_backend_apply_ms = take_preparation("backend_apply_ms"); + stages.pipeline_3d_backend_plan_ms = take_preparation("backend_plan_ms"); + stages.pipeline_3d_backend_execute_ms = take_preparation("backend_execute_ms"); + stages.pipeline_3d_backend_commands_ms = take(preparation_window); + stages.pipeline_3d_backend_queue_ms = take(Math.max(0, queue_left - submit_queued)); const gpu_submitted = values.marker_gpu_submitted_ms ?? queue_left; - let backend_window = Math.max(0, gpu_submitted - queue_left); - const take_backend = (key: string) => { const value = Math.min(backend_window, Math.max(0, values[key] ?? 0)); backend_window -= value; return take(value); }; - stages.pipeline_3d_backend_apply_ms = take_backend("backend_apply_ms"); - stages.pipeline_3d_backend_plan_ms = take_backend("backend_plan_ms"); - stages.pipeline_3d_backend_execute_ms = take_backend("backend_execute_ms"); - stages.pipeline_3d_backend_submit_ms = take_backend("backend_submit_ms"); - stages.pipeline_3d_backend_commands_ms = take(backend_window); + let submit_window = Math.max(0, gpu_submitted - queue_left); + const measured_submit = Math.min(submit_window, Math.max(0, values.backend_submit_ms ?? 0)); + stages.pipeline_3d_backend_submit_ms = take(measured_submit); + submit_window -= measured_submit; + stages.pipeline_3d_submit_handoff_ms = take(submit_window); let gpu_window = interval("gpu_submitted", "gpu_completed"); const take_gpu = (key: string) => { const value = Math.min(gpu_window, Math.max(0, values[key] ?? 0)); gpu_window -= value; return take(value); }; stages.pipeline_3d_gpu_render_ms = take_gpu("gpu_render_ms"); @@ -386,7 +394,8 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject(); let manual_frame_pending = false; let next_request_id = 1; let previous_request_time = 0; @@ -397,7 +406,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject(); const presentation_callbacks = new Set(); - const pacing = {mode: "fixed_rate" as Frame_Pacing_Mode, fixed_rate_fps: 30, minimum_latency_headroom: 1.25}; + const pacing = {mode: "maximum_rate" as Frame_Pacing_Mode, fixed_rate_fps: 30, minimum_latency_headroom: 1.25}; const local_policy_overrides = new Set(); const socket = new ReconnectingWebSocket(socket_url(plot.websocket), [], { minReconnectionDelay: 300, @@ -428,7 +437,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject { clear_timer(); if (stopped) return; - if (frame_pending) { if (manual) manual_frame_pending = true; return; } + if (in_flight_requests.size >= maximum_in_flight) { if (manual) manual_frame_pending = true; return; } const retry = () => { if (manual) manual_frame_pending = true; timer = window.setTimeout(() => request_frame(manual), 250); }; if (socket.readyState !== WebSocket.OPEN) { retry(); return; } const canvas = canvas_ref.current; @@ -446,9 +455,10 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject { const next = metadata.pacing; @@ -511,7 +521,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject { const started_at = request_started_at.get(pair.value.correlation_id); if (started_at === undefined) return; - frame_pending = false; + in_flight_requests.delete(pair.value.correlation_id); request_started_at.delete(pair.value.correlation_id); latest_metadata = pair.value; synchronize_pacing(pair.value); @@ -536,7 +546,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject { const detail = (event as CustomEvent<{plot_id: string}>).detail; - if (detail?.plot_id === plot.id && !frame_pending) schedule_next(true); + if (detail?.plot_id === plot.id && in_flight_requests.size === 0) schedule_next(true); }; window.addEventListener("aethera-frame-policy", on_policy_change); window.addEventListener("aethera-manual-frame", on_manual_frame); window.addEventListener("aethera-reset-camera", on_camera_reset); window.addEventListener("aethera-reset-frame-diagnostics", on_diagnostics_reset); window.addEventListener("aethera-frame-delivery", on_delivery_change); - socket.onopen = () => { frame_pending = false; pending_metadata = null; request_started_at.clear(); set_status("LIVE"); request_frame(true); }; - socket.onclose = () => { clear_timer(); frame_pending = false; pending_metadata = null; request_started_at.clear(); if (!stopped) set_status("CONNECTING"); }; + socket.onopen = () => { in_flight_requests.clear(); pending_metadata = null; request_started_at.clear(); set_status("LIVE"); request_frame(true); }; + socket.onclose = () => { clear_timer(); in_flight_requests.clear(); pending_metadata = null; request_started_at.clear(); if (!stopped) set_status("CONNECTING"); }; socket.onmessage = event => { if (typeof event.data === "string") { try { @@ -894,16 +904,18 @@ const pipeline_2d_definitions: Pipeline_Stage_Definition[] = [ ]; const pipeline_3d_definitions: Pipeline_Stage_Definition[] = [ pipeline_common_start, - ["pipeline_3d_event_ms", "3D 事件入队", "Scene 将输入事件提交到三维 Render Domain 的耗时。"], + ["pipeline_3d_event_ms", "3D 事件入队", "Scene 将输入事件提交到本 Scene 独立 Datoviz 准备域的耗时。"], ["pipeline_3d_prepare_ms", "3D Visual Prepare", "将 Visual items 转换为不可变 Prepared_Visual GPU 字段的耗时;数据变化时执行。"], ["pipeline_3d_submit_graph_ms", "3D Submit 图", "Submit 依赖图把 Prepared_Visual 入队到异步后端的耗时。"], ["pipeline_3d_scene_coordination_ms", "3D Scene 编排", "三维 Scene 同步阶段中除事件、Prepare、Submit 外的依赖图编排耗时。"], - ["pipeline_3d_backend_queue_ms", "Render Domain 排队", "Scene 提交结束后等待单线程 Datoviz Render Domain 接管的耗时。"], + ["pipeline_3d_prepare_queue_ms", "准备域排队", "Scene 发布不可变 Visual 快照后,等待本 Scene 独立 Datoviz CPU 准备线程的耗时。"], ["pipeline_3d_backend_apply_ms", "Datoviz Apply", "把本帧 Visual 与 Scene 参数应用到 Datoviz 对象的 CPU 耗时。"], ["pipeline_3d_backend_plan_ms", "Datoviz Plan", "Datoviz 生成本帧 GPU 命令计划的 CPU 耗时。"], ["pipeline_3d_backend_execute_ms", "Datoviz Execute", "Datoviz 执行命令构建的 CPU 耗时。"], - ["pipeline_3d_backend_submit_ms", "GPU 提交", "将已构建命令提交到 GPU 队列的 CPU 耗时。"], - ["pipeline_3d_backend_commands_ms", "后端命令衔接", "Render Domain 接管到 GPU 提交之间未落在四个 Datoviz trace 字段中的命令衔接时间。"], + ["pipeline_3d_backend_commands_ms", "准备域其余工作", "每 Scene 准备域中除 Apply、Plan、Execute 外的命令录制和交互处理耗时。"], + ["pipeline_3d_backend_queue_ms", "GPU 提交域排队", "命令已录制完成后,等待同 GPU 单线程 Queue Submit 域的耗时。"], + ["pipeline_3d_backend_submit_ms", "GPU Queue Submit", "共享提交域执行已经录制好的 Vulkan Queue Submit 的 CPU 耗时。"], + ["pipeline_3d_submit_handoff_ms", "提交衔接", "共享提交域接管命令到 fence 开始监视之间未落入 Queue Submit 测量的轻量衔接。"], ["pipeline_3d_gpu_render_ms", "GPU Render", "GPU 执行渲染通道的设备时间。"], ["pipeline_3d_gpu_transition_ms", "GPU 资源转换", "GPU 图像布局和资源状态转换的设备时间。"], ["pipeline_3d_gpu_copy_ms", "GPU 回读复制", "GPU 将渲染结果复制到可回读资源的设备时间。"], @@ -959,7 +971,8 @@ function Frame_Timeline_Chart({diagnostics, dimension, paused, on_context_menu}: ] : [ total_series, ["pipeline_3d_prepare_ms", "Visual Prepare", "#62a8ff"], - ["pipeline_3d_backend_queue_ms", "后端排队", "#f4bd63"], + ["pipeline_3d_prepare_queue_ms", "准备域排队", "#f4bd63"], + ["pipeline_3d_backend_queue_ms", "GPU 提交排队", "#ef9f55"], ["pipeline_3d_gpu_render_ms", "GPU Render", "#ff7d9c"], pixel_delivery ? ["pipeline_3d_readback_ms", "CPU 回读", "#b998ff"] : ["pipeline_3d_gpu_sync_ms", "GPU 同步", "#b998ff"] ];