From 647ad6a66cb53b1c36bab81590d0c573975915bc Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 28 Aug 2026 21:11:39 +0800 Subject: [PATCH] =?UTF-8?q?3D=E4=BC=98=E5=8C=96=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kernel/Frame_Policy/Frame_Scheduler.cpp | 95 ++- mcp/core/runtime/Plot.cpp | 41 +- mcp/main.cmake | 4 +- mcp/tests/Control_Path_Benchmarks.cpp | 286 +++++++ .../base/Datoviz_Frame_Observation.hpp | 5 +- .../render_3D/detail/Async_Render_Backend.cpp | 520 ------------ .../render_3D/detail/Async_Render_Backend.hpp | 40 - .../detail/Datoviz_Visual_Backend.hpp | 100 --- .../detail/Datoviz_Visual_Backend.ipp | 165 ---- .../detail/Gpu_Completion_Service.cpp | 21 +- .../detail/Gpu_Completion_Service.hpp | 7 +- .../detail/Gpu_Completion_Service.ipp | 6 +- render_3D/render_3D/detail/Render_Domain.cpp | 114 --- render_3D/render_3D/detail/Render_Domain.hpp | 45 -- render_3D/render_3D/scene/Render_Scene_3D.ipp | 661 ++++++++++----- .../Render_Scene_3D_Datoviz.cpp} | 756 +++++++++++------- .../render_3D/scene/Scene_Datoviz_State.hpp | 191 +++++ .../datoviz/include/datoviz/drp2/runtime.h | 23 + .../datoviz/include/datoviz/scene.h | 49 ++ .../datoviz/include/datoviz/vklite/commands.h | 5 + .../include/datoviz/vklite/descriptors.h | 4 + .../third_party/datoviz/src/drp2/_runtime.h | 4 +- .../third_party/datoviz/src/drp2/backend.c | 9 +- render_3D/third_party/datoviz/src/drp2/pass.c | 6 +- .../third_party/datoviz/src/drp2/pipeline.c | 17 +- .../third_party/datoviz/src/drp2/runtime.c | 69 ++ .../third_party/datoviz/src/drp2/transfer.c | 10 +- .../datoviz/src/scene/core/_scene.h | 1 + .../datoviz/src/scene/core/figure_emit.c | 229 ++++++ .../datoviz/src/scene/core/scene.c | 2 +- .../datoviz/src/scene/frame_plan/emit.h | 2 + .../src/scene/runtime/common_bindings.c | 3 + .../datoviz/src/vklite/_commands.h | 1 + .../third_party/datoviz/src/vklite/commands.c | 13 +- .../datoviz/src/vklite/descriptors.c | 10 +- webapp_gallery/src/app.tsx | 8 +- 36 files changed, 1974 insertions(+), 1548 deletions(-) delete mode 100644 render_3D/render_3D/detail/Async_Render_Backend.cpp delete mode 100644 render_3D/render_3D/detail/Async_Render_Backend.hpp delete mode 100644 render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp delete mode 100644 render_3D/render_3D/detail/Datoviz_Visual_Backend.ipp delete mode 100644 render_3D/render_3D/detail/Render_Domain.cpp delete mode 100644 render_3D/render_3D/detail/Render_Domain.hpp rename render_3D/render_3D/{detail/Datoviz_Visual_Backend.cpp => scene/Render_Scene_3D_Datoviz.cpp} (83%) create mode 100644 render_3D/render_3D/scene/Scene_Datoviz_State.hpp diff --git a/kernel/src/kernel/Frame_Policy/Frame_Scheduler.cpp b/kernel/src/kernel/Frame_Policy/Frame_Scheduler.cpp index 484f099..3be87e7 100644 --- a/kernel/src/kernel/Frame_Policy/Frame_Scheduler.cpp +++ b/kernel/src/kernel/Frame_Policy/Frame_Scheduler.cpp @@ -38,21 +38,6 @@ Nanoseconds fps_period(double fps) { struct Frame_Scheduler::Timer::State { enum struct Mode : std::uint8_t { stopped, once, periodic }; - not_null owner; - std::uint64_t id{}; - Handler handler{}; - std::atomic_bool alive{true}; - Mode mode{Mode::stopped}; /* 仅 Scheduler thread 访问。 */ - Nanoseconds period{}; /* 仅 Scheduler thread 访问。 */ - Scheduler_Clock::time_point next_deadline{}; /* 真实 deadline,不按 tick 累积。 */ - TimerEvent> event; - - State(not_null value_owner, - std::uint64_t value_id, - Handler value_handler); -}; - -struct Frame_Scheduler::Private { enum struct Command_Type : std::uint8_t { periodic, once, @@ -62,14 +47,48 @@ struct Frame_Scheduler::Private { }; struct Command { Command_Type type{}; - std::shared_ptr state{}; + std::shared_ptr state{}; Nanoseconds value{}; }; + struct Command_Ingress { + moodycamel::BlockingConcurrentQueue commands{256}; + std::atomic_bool accepting{true}; + [[nodiscard]] bool enqueue(Command command) { + if (command.type != Command_Type::destroy && + command.type != Command_Type::stop && + !accepting.load(std::memory_order_acquire)) + return false; + return commands.enqueue(std::move(command)); + } + }; + + not_null owner; + std::weak_ptr ingress; + std::uint64_t id{}; + Handler handler{}; + std::atomic_bool alive{true}; + Mode mode{Mode::stopped}; /* 仅 Scheduler thread 访问。 */ + Nanoseconds period{}; /* 仅 Scheduler thread 访问。 */ + Scheduler_Clock::time_point next_deadline{}; /* 真实 deadline,不按 tick 累积。 */ + TimerEvent> event; + + State(not_null value_owner, + const std::shared_ptr& value_ingress, + std::uint64_t value_id, + Handler value_handler); +}; + +struct Frame_Scheduler::Private { + using Command_Type = Timer::State::Command_Type; + using Command = Timer::State::Command; + using Command_Ingress = Timer::State::Command_Ingress; + + std::shared_ptr ingress{ + std::make_shared()}; TimerWheel wheel{}; Scheduler_Clock::time_point origin{Scheduler_Clock::now()}; Scheduler_Clock::time_point last_advance{origin}; - moodycamel::BlockingConcurrentQueue commands{256}; std::atomic_uint64_t next_id{1}; std::atomic_bool stopping{}; std::jthread thread{}; @@ -77,19 +96,21 @@ struct Frame_Scheduler::Private { Private() : thread([this](std::stop_token stop) { run(stop); }) {} ~Private() { + ingress->accepting.store(false, std::memory_order_release); stopping.store(true, std::memory_order_release); thread.request_stop(); enqueue({Command_Type::stop}); } void enqueue(Command command) { - if (!commands.enqueue(std::move(command))) std::terminate(); + if (!ingress->enqueue(std::move(command))) std::terminate(); } std::shared_ptr make_timer(Handler handler) { if (!handler) throw std::invalid_argument("frame scheduler timer handler is empty"); const auto id = next_id.fetch_add(1, std::memory_order_relaxed); - auto state = std::make_shared(this, id, std::move(handler)); + auto state = std::make_shared( + this, ingress, id, std::move(handler)); return state; } @@ -152,7 +173,7 @@ struct Frame_Scheduler::Private { void process_commands(Scheduler_Clock::time_point now) { Command command; - while (commands.try_dequeue(command)) { + while (ingress->commands.try_dequeue(command)) { process_command(std::move(command), now); now = Scheduler_Clock::now(); } @@ -221,7 +242,7 @@ struct Frame_Scheduler::Private { const auto deadline = last_advance + Frame_Scheduler::tick_duration * static_cast(next); Command command; - if (commands.wait_dequeue_timed( + if (ingress->commands.wait_dequeue_timed( command, std::max(deadline - Scheduler_Clock::now(), Scheduler_Clock::duration::zero()))) process_command(std::move(command), Scheduler_Clock::now()); @@ -234,9 +255,12 @@ struct Frame_Scheduler::Private { }; Frame_Scheduler::Timer::State::State( - not_null value_owner, std::uint64_t value_id, + not_null value_owner, + const std::shared_ptr& value_ingress, + std::uint64_t value_id, Handler value_handler) - : owner(value_owner), id(value_id), handler(std::move(value_handler)), + : owner(value_owner), ingress(value_ingress), id(value_id), + handler(std::move(value_handler)), event([this] { owner->fire(*this); }) {} @@ -259,7 +283,10 @@ Frame_Scheduler::Timer::Timer(std::shared_ptr state) noexcept Frame_Scheduler::Timer::~Timer() noexcept { if (!state_) return; state_->alive.store(false, std::memory_order_release); - state_->owner->enqueue({Private::Command_Type::destroy, state_, {}}); + const auto ingress = state_->ingress.lock(); + if (ingress && !ingress->enqueue( + {Private::Command_Type::destroy, state_, {}})) + std::terminate(); } Frame_Scheduler::Timer::Timer(Timer&& other) noexcept @@ -279,19 +306,31 @@ void Frame_Scheduler::Timer::start_periodic(double fps) { return; } state_->alive.store(true, std::memory_order_release); - state_->owner->enqueue({Private::Command_Type::periodic, state_, fps_period(fps)}); + const auto ingress = state_->ingress.lock(); + if (!ingress || !ingress->enqueue( + {Private::Command_Type::periodic, state_, fps_period(fps)})) { + state_->alive.store(false, std::memory_order_release); + throw std::runtime_error("frame scheduler is stopping"); + } } void Frame_Scheduler::Timer::start_once(std::chrono::nanoseconds delay) { if (!state_) throw std::logic_error("frame scheduler timer is empty"); state_->alive.store(true, std::memory_order_release); - state_->owner->enqueue({Private::Command_Type::once, state_, delay}); + const auto ingress = state_->ingress.lock(); + if (!ingress || !ingress->enqueue( + {Private::Command_Type::once, state_, delay})) { + state_->alive.store(false, std::memory_order_release); + throw std::runtime_error("frame scheduler is stopping"); + } } void Frame_Scheduler::Timer::cancel() noexcept { - if (!state_ || !state_->owner) return; + if (!state_) return; state_->alive.store(false, std::memory_order_release); - state_->owner->enqueue({Private::Command_Type::cancel, state_, {}}); + const auto ingress = state_->ingress.lock(); + if (ingress) static_cast(ingress->enqueue( + {Private::Command_Type::cancel, state_, {}})); } bool Frame_Scheduler::Timer::valid() const noexcept { diff --git a/mcp/core/runtime/Plot.cpp b/mcp/core/runtime/Plot.cpp index 292e11d..de5017a 100644 --- a/mcp/core/runtime/Plot.cpp +++ b/mcp/core/runtime/Plot.cpp @@ -302,8 +302,8 @@ nlohmann::json datoviz_observation_json( {"prepare_released_after_submission", value.prepare_released_after_submission}, {"timings_ms", { - {"render_domain_queue_wait", - milliseconds(value.render_domain_queue_wait_ns)}, + {"queue_submit_wait", + milliseconds(value.queue_submit_wait_ns)}, {"apply", milliseconds(value.apply_ns)}, {"emit", milliseconds(value.emit_ns)}, {"execute", milliseconds(value.execute_ns)}, @@ -535,6 +535,7 @@ struct Plot::Private { std::unique_ptr view; std::once_flag start_once; std::weak_ptr lifetime{}; /* 仅用于 completion 后重新投递 Taskflow,避免在 Scene callback 内重入 render。 */ + std::atomic> frame_policy_lifetime{}; /* 任一物理帧被借用期间由策略保活整个 Plot;最后一槽归还后释放。 */ std::atomic> consumers{ std::make_shared()}; /* 低频订阅修改发布不可变版本。 */ std::atomic_uint64_t next_stream_id{1}; @@ -550,7 +551,7 @@ struct Plot::Private { Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */ moodycamel::ConcurrentQueue tick_requests{}; /* 多生产者提交、唯一短任务消费的帧请求流。 */ std::optional deferred_tick{}; /* 仅 tick consumer 任务访问的 latest 延后请求。 */ - std::atomic_uint64_t tick_request_generation{}; /* 请求入队后推进,用于关闭 consumer 尾部唤醒竞争窗口。 */ + std::atomic_uint64_t consumer_work_generation{}; /* tick 或退休帧入队后推进,关闭 consumer 尾部唤醒竞争窗口。 */ std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */ std::atomic render_admission{Render_Admission_State::ready}; /* Plot 渲染准入及物理槽背压的唯一状态源。 */ std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()}; @@ -633,6 +634,8 @@ struct Plot::Private { void keep_latest_tick(const Plot_Render_Tick& tick); void arm_tick_consumer(std::weak_ptr lifetime); void release_render_admission(std::weak_ptr lifetime); + void retain_frame_policy_lifetime(); + void release_frame_policy_lifetime_if_idle(); void consume_tick(std::weak_ptr lifetime); void refresh_schedule(); void clock_tick(const Plot_Render_Tick& tick); @@ -772,7 +775,7 @@ void Plot::Private::submit_tick_request(Plot_Render_Tick tick) { with_frame_policy([&](auto& policy) { policy.record_request(source, issued_at); }); - tick_request_generation.fetch_add(1, std::memory_order_release); + consumer_work_generation.fetch_add(1, std::memory_order_release); } void Plot::Private::keep_latest_tick(const Plot_Render_Tick& tick) { @@ -831,11 +834,31 @@ void Plot::Private::release_render_admission(std::weak_ptr lifetime) { arm_tick_consumer(std::move(lifetime)); } +void Plot::Private::retain_frame_policy_lifetime() { + if (frame_policy_lifetime.load(std::memory_order_acquire)) return; + const auto owner = lifetime.lock(); + if (!owner) + throw std::logic_error( + "Plot frame policy cannot retain an expired Plot"); + std::shared_ptr empty; + static_cast(frame_policy_lifetime.compare_exchange_strong( + empty, owner, std::memory_order_release, std::memory_order_acquire)); +} + +void Plot::Private::release_frame_policy_lifetime_if_idle() { + if (std::ranges::any_of(frame_slots, [](const Managed_Frame& slot) { + return slot.state.load(std::memory_order_acquire) != + Frame_State::available; + })) + return; + frame_policy_lifetime.store({}, std::memory_order_release); +} + void Plot::Private::consume_tick(std::weak_ptr lifetime) { + const auto observed_generation = + consumer_work_generation.load(std::memory_order_acquire); consume_retired_frames(); if (consume_frame_policy_events()) refresh_schedule(); - const auto observed_generation = - tick_request_generation.load(std::memory_order_acquire); Plot_Render_Tick requested; while (tick_requests.try_dequeue(requested)) keep_latest_tick(requested); @@ -847,7 +870,7 @@ void Plot::Private::consume_tick(std::weak_ptr lifetime) { consume_retired_frames(); if (consume_frame_policy_events()) refresh_schedule(); tick_task_scheduled.store(false, std::memory_order_release); - if (tick_request_generation.load(std::memory_order_acquire) != + if (consumer_work_generation.load(std::memory_order_acquire) != observed_generation || retired_frames.load(std::memory_order_acquire) || (render_admission.load(std::memory_order_acquire) == @@ -1019,6 +1042,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { std::memory_order_release); return; } + retain_frame_policy_lifetime(); managed->presentation_time = std::chrono::duration_cast( std::chrono::duration(tick.time_milliseconds)); @@ -1030,6 +1054,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { static_cast(slot.state.compare_exchange_strong( expected, Frame_State::available, std::memory_order_acq_rel, std::memory_order_acquire)); + release_frame_policy_lifetime_if_idle(); }; bool taskflow_trace_claimed{}; const auto restore_taskflow_trace_claim = [this, &taskflow_trace_claimed] { @@ -1356,6 +1381,7 @@ void Plot::Private::retire_completed_frame(not_null frame) { } while (!retired_frames.compare_exchange_weak( head, managed, std::memory_order_release, std::memory_order_relaxed)); + consumer_work_generation.fetch_add(1, std::memory_order_release); arm_tick_consumer(lifetime); } @@ -1442,6 +1468,7 @@ void Plot::Private::finalize_retired_frame(not_null frame) { admission, Render_Admission_State::ready, std::memory_order_acq_rel, std::memory_order_acquire)); arm_tick_consumer(lifetime); + release_frame_policy_lifetime_if_idle(); if (captured_trace) { auto owner = lifetime; diff --git a/mcp/main.cmake b/mcp/main.cmake index 17f41ba..ea44972 100644 --- a/mcp/main.cmake +++ b/mcp/main.cmake @@ -1,7 +1,9 @@ include("${CMAKE_CURRENT_LIST_DIR}/../web_server/cmake/rely.cmake") set(Aethera_MCP_dependencies global::drogon - global::pfr) + global::pfr + # global::libwebsockets +) rcl_add_dependency_action_targets(Aethera_MCP_env ${Aethera_MCP_dependencies}) set_target_properties(Aethera_MCP_env PROPERTIES FOLDER Aethera_MCP) library_get_missing_with_rely(Aethera_MCP_dependencies_missing diff --git a/mcp/tests/Control_Path_Benchmarks.cpp b/mcp/tests/Control_Path_Benchmarks.cpp index 527161d..9b6e5db 100644 --- a/mcp/tests/Control_Path_Benchmarks.cpp +++ b/mcp/tests/Control_Path_Benchmarks.cpp @@ -3,8 +3,16 @@ #include #include #include +#include +#include +#include +#include #include +#include #include +#include +#include +#include namespace aethera::mcp::benchmarks { namespace { @@ -59,6 +67,268 @@ protected: std::shared_ptr service; }; +constexpr std::array plot_3d_ids{ + "datoviz_point", "datoviz_splat", "datoviz_pixel", "datoviz_marker", + "datoviz_sphere", "datoviz_segment", "datoviz_vector", + "datoviz_primitive", "datoviz_mesh", "datoviz_spectrogram", + "datoviz_path", "datoviz_image", "datoviz_labels", "datoviz_glyph", + "datoviz_text", "datoviz_volume"}; +constexpr std::size_t splat_plot_index{1}; + +enum struct Input_Workload : std::uint8_t { + steady, + drag, + wheel, + mixed +}; + +struct Concurrent_Workload_State { + std::array completed{}; /* Per-Plot publish counts for this benchmark interval. */ +}; + +[[nodiscard]] Plot_Input_Request concurrent_input_request( + Input_Workload workload, std::size_t plot_index, + std::uint64_t sequence) { + Plot_Input_Request request; + request.plot = plot_3d_ids[plot_index]; + request.event.time_milliseconds = 1'000.0 + + static_cast(sequence) * (1'000.0 / 120.0); + request.event.position = { + 360.0 + static_cast( + static_cast((sequence + plot_index * 7U) % 121U) - 60), + 210.0 + static_cast( + static_cast((sequence * 3U + plot_index * 11U) % 81U) - 40)}; + request.event.global_position = request.event.position; + if (workload == Input_Workload::wheel || + (workload == Input_Workload::mixed && (plot_index & 1U) != 0U)) { + request.event.type = Event_Type::wheel; + request.event.pixel_delta_y = (sequence & 1U) != 0U ? 120.0 : -120.0; + request.event.angle_delta_y = request.event.pixel_delta_y; + return request; + } + request.event.type = Event_Type::pointer_move; + request.event.button = Mouse_Button::left; + request.event.buttons = 1; + return request; +} + +class Concurrent_3D : public benchmark::Fixture { +public: + void SetUp(const benchmark::State&) override { + if (shared_service) { + service = shared_service; + workload = shared_workload; + plots = shared_plots; + streams = shared_streams; + return; + } + shared_service = Control_Service::create(); + shared_workload = std::make_shared(); + shared_plots.reserve(plot_3d_ids.size()); + shared_streams.reserve(plot_3d_ids.size()); + for (std::size_t index = 0; index < plot_3d_ids.size(); ++index) { + auto plot = shared_service->find_plot(plot_3d_ids[index]); + if (!plot) throw std::logic_error("concurrent 3D benchmark Plot is unavailable"); + const auto stream = plot->subscribe( + [workload = shared_workload, index]( + std::shared_ptr frame) { + if (!frame || !frame->pixels) return; + workload->completed[index].fetch_add( + 1, std::memory_order_relaxed); + }); + plot->configure_stream(stream, 720, 420); + shared_plots.push_back(std::move(plot)); + shared_streams.push_back(stream); + } + service = shared_service; + workload = shared_workload; + plots = shared_plots; + streams = shared_streams; + } + + void TearDown(const benchmark::State&) override {} + + static void shutdown() { + if (!shared_workload) return; + for (std::size_t index = 0; index < shared_plots.size(); ++index) + shared_plots[index]->unsubscribe(shared_streams[index]); + shared_plots.clear(); + shared_streams.clear(); + shared_service.reset(); + shared_workload.reset(); + } + +protected: + [[nodiscard]] bool submit_input_batch( + Input_Workload input, std::uint64_t sequence, + std::optional pointer_type = std::nullopt) { + for (std::size_t plot_index = 0; + plot_index < plot_3d_ids.size(); ++plot_index) { + const bool wheel = input == Input_Workload::wheel || + (input == Input_Workload::mixed && (plot_index & 1U) != 0U); + if (pointer_type && wheel) continue; + auto request = concurrent_input_request( + input, plot_index, sequence); + if (pointer_type) { + request.event.type = *pointer_type; + request.event.buttons = *pointer_type == Event_Type::pointer_release + ? 0 : 1; + } + const auto result = service->call_tool( + "aethera_plot_input", encode_protocol_value(request)); + if (result.result != Tool_Call_Result::ok) return false; + } + return true; + } + + void run(benchmark::State& state, Input_Workload input) { + for (auto& count : workload->completed) + count.store(0, std::memory_order_relaxed); + for (const auto& plot : plots) plot->reset_diagnostics(); + if (input == Input_Workload::drag || input == Input_Workload::mixed) { + if (!submit_input_batch( + input, 0, Event_Type::pointer_press)) { + state.SkipWithError("16-Plot pointer press batch was rejected"); + return; + } + } + const auto runtime_begin = service->call_tool( + "aethera_task_runtime", nlohmann::json::object()); + const auto started = std::chrono::steady_clock::now(); + constexpr auto input_period = std::chrono::nanoseconds{ + 1'000'000'000 / 120}; + auto next_input = started + input_period; + std::uint64_t input_batches{}; + std::uint64_t input_requests{}; + std::uint32_t input_poll{}; + for ([[maybe_unused]] auto iteration : state) { + benchmark::DoNotOptimize( + workload->completed[splat_plot_index].load( + std::memory_order_relaxed)); + benchmark::ClobberMemory(); + if ((++input_poll & 0x3FFU) != 0U) continue; + const auto now = std::chrono::steady_clock::now(); + if (input == Input_Workload::steady) continue; + if (now < next_input) continue; + ++input_batches; + if (!submit_input_batch(input, input_batches)) { + state.SkipWithError("16-Plot interaction batch was rejected"); + break; + } + input_requests += plot_3d_ids.size(); + do next_input += input_period; while (next_input <= now); + } + const auto finished = std::chrono::steady_clock::now(); + if (input == Input_Workload::drag || input == Input_Workload::mixed) { + static_cast(submit_input_batch( + input, input_batches + 1U, Event_Type::pointer_release)); + } + + const double elapsed_seconds = std::chrono::duration( + finished - started).count(); + std::uint64_t total_completed{}; + double minimum_fps = std::numeric_limits::max(); + double maximum_fps{}; + double splat_fps{}; + for (std::size_t index = 0; index < plots.size(); ++index) { + const auto count = workload->completed[index].load( + std::memory_order_relaxed); + total_completed += count; + const double fps = elapsed_seconds > 0.0 + ? static_cast(count) / elapsed_seconds : 0.0; + minimum_fps = std::min(minimum_fps, fps); + maximum_fps = std::max(maximum_fps, fps); + if (index == splat_plot_index) splat_fps = fps; + + const auto diagnostics = plots[index]->diagnostics(); + const auto& policy = diagnostics.at("frame_policy"); + const auto prefix = "p" + std::to_string(index) + "_"; + state.counters[prefix + "fps"] = fps; + state.counters[prefix + "requested"] = + policy.at("observation").at("request_count").get(); + state.counters[prefix + "submitted"] = + policy.at("observation").at("submitted_frame_count").get(); + state.counters[prefix + "completed"] = + policy.at("observation").at("completed_frame_count").get(); + state.counters[prefix + "active"] = + policy.at("observation").at("active_frame_count").get(); + state.counters[prefix + "scene_rejected"] = + policy.at("requests").at("scene_rejected").get(); + state.counters[prefix + "slot_backpressure"] = + policy.at("requests").at("frame_slot_backpressure").get(); + state.counters[prefix + "completion_ms"] = + policy.at("latency").at("average_completion_ms").get(); + const auto& frame_statistics = diagnostics.at("frame_statistics"); + const auto statistic_average = [&](std::string_view name) { + const auto found = frame_statistics.find(name); + return found == frame_statistics.end() + ? 0.0 : found->at("average").get(); + }; + state.counters[prefix + "backend_queue_ms"] = + statistic_average("backend_queue_ms"); + state.counters[prefix + "backend_plan_ms"] = + statistic_average("backend_plan_ms"); + } + const auto runtime_end = service->call_tool( + "aethera_task_runtime", nlohmann::json::object()); + const auto runtime_delta = [&](std::string_view key) { + if (runtime_begin.result != Tool_Call_Result::ok || + runtime_end.result != Tool_Call_Result::ok) return 0.0; + const auto begin = runtime_begin.content.at(key).get(); + const auto end = runtime_end.content.at(key).get(); + return static_cast(end >= begin ? end - begin : 0U); + }; + const double wall_ns = runtime_delta("observed_wall_time_ns"); + const double busy_ns = runtime_delta("worker_busy_time_ns"); + const double cpu_ns = runtime_delta("worker_cpu_time_ns"); + const double worker_count = runtime_end.result == Tool_Call_Result::ok + ? runtime_end.content.at("worker_count").get() : 0.0; + state.counters["aggregate_fps"] = elapsed_seconds > 0.0 + ? static_cast(total_completed) / elapsed_seconds : 0.0; + state.counters["minimum_plot_fps"] = minimum_fps; + state.counters["maximum_plot_fps"] = maximum_fps; + state.counters["splat_fps"] = splat_fps; + state.counters["fairness_pct"] = maximum_fps > 0.0 + ? minimum_fps / maximum_fps * 100.0 : 0.0; + state.counters["worker_busy_pct"] = wall_ns > 0.0 && worker_count > 0.0 + ? busy_ns / (wall_ns * worker_count) * 100.0 : 0.0; + state.counters["worker_cpu_pct"] = wall_ns > 0.0 && worker_count > 0.0 + ? cpu_ns / (wall_ns * worker_count) * 100.0 : 0.0; + state.counters["input_batches"] = static_cast(input_batches); + state.counters["input_requests"] = static_cast(input_requests); + state.counters["input_request_rate"] = elapsed_seconds > 0.0 + ? static_cast(input_requests) / elapsed_seconds : 0.0; + state.SetItemsProcessed(static_cast(total_completed)); + } + +private: + inline static std::shared_ptr shared_service{}; + inline static std::shared_ptr shared_workload{}; + inline static std::vector> shared_plots{}; + inline static std::vector shared_streams{}; + std::shared_ptr service{}; + std::shared_ptr workload{}; + std::vector> plots{}; + std::vector streams{}; +}; + +BENCHMARK_DEFINE_F(Concurrent_3D, Warmup)(benchmark::State& state) { + run(state, Input_Workload::steady); +} + +BENCHMARK_DEFINE_F(Concurrent_3D, Steady)(benchmark::State& state) { + run(state, Input_Workload::steady); +} +BENCHMARK_DEFINE_F(Concurrent_3D, All_Plots_Drag)(benchmark::State& state) { + run(state, Input_Workload::drag); +} +BENCHMARK_DEFINE_F(Concurrent_3D, All_Plots_Wheel)(benchmark::State& state) { + run(state, Input_Workload::wheel); +} +BENCHMARK_DEFINE_F(Concurrent_3D, All_Plots_Mixed)(benchmark::State& state) { + run(state, Input_Workload::mixed); +} + BENCHMARK_DEFINE_F(Control_Path, Timestamped_Drag_Admission)( benchmark::State& state) { auto request = timestamped_drag_request(Event_Type::pointer_press); @@ -116,6 +386,21 @@ BENCHMARK(decode_timestamped_drag); BENCHMARK(decode_timed_render); BENCHMARK_REGISTER_F(Control_Path, Timestamped_Drag_Admission) ->Iterations(512); +/* Fixture 静态持有 16 个 Plot;MinTime 校准不会重建 Datoviz Scene。 */ +BENCHMARK_REGISTER_F(Concurrent_3D, Warmup) + ->MinTime(10.0)->UseRealTime(); +BENCHMARK_REGISTER_F(Concurrent_3D, Steady) + ->MinTime(10.0)->UseRealTime(); +BENCHMARK_REGISTER_F(Concurrent_3D, All_Plots_Drag) + ->MinTime(10.0)->UseRealTime(); +BENCHMARK_REGISTER_F(Concurrent_3D, All_Plots_Wheel) + ->MinTime(10.0)->UseRealTime(); +BENCHMARK_REGISTER_F(Concurrent_3D, All_Plots_Mixed) + ->MinTime(10.0)->UseRealTime(); + +void shutdown_concurrent_3d_benchmark() { + Concurrent_3D::shutdown(); +} } } @@ -125,6 +410,7 @@ int main(int argc, char** argv) { benchmark::Initialize(&argc, argv); if (benchmark::ReportUnrecognizedArguments(argc, argv)) return 2; benchmark::RunSpecifiedBenchmarks(); + aethera::mcp::benchmarks::shutdown_concurrent_3d_benchmark(); benchmark::Shutdown(); return 0; } diff --git a/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp b/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp index 7773d5a..5e7fbf9 100644 --- a/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp +++ b/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp @@ -8,6 +8,7 @@ namespace aethera::render_3d { enum struct Datoviz_Frame_Path : std::uint8_t { recorded, + updated, reused }; @@ -24,12 +25,12 @@ struct Datoviz_Gpu_Timing { */ struct Datoviz_Frame_Observation { std::uint64_t render_sequence{}; /* Scene 分配的帧序号。 */ - Datoviz_Frame_Path path{Datoviz_Frame_Path::recorded}; /* 本帧录制命令或复用既有命令。 */ + Datoviz_Frame_Path path{Datoviz_Frame_Path::recorded}; /* 本帧录制、更新动态绑定或直接复用命令。 */ bool gpu_timing_requested{}; /* 本帧是否读取已录制的 GPU 时间戳。 */ bool readback_requested{}; /* 本帧是否请求最终 RGBA 读回。 */ bool controller_input_applied{}; /* 本帧是否把已提交输入应用到 Datoviz 控制器。 */ bool prepare_released_after_submission{}; /* 本帧实际提交后是否立即允许下一帧 Prepare。 */ - std::uint64_t render_domain_queue_wait_ns{}; /* 唯一 GPU 提交域排队耗时。 */ + std::uint64_t queue_submit_wait_ns{}; /* 获取共享 VkQueue 提交权的墙钟耗时。 */ std::uint64_t apply_ns{}; /* 应用本帧 Visual 数据耗时。 */ std::uint64_t emit_ns{}; /* 生成 Datoviz 帧计划耗时。 */ std::uint64_t execute_ns{}; /* 执行 Datoviz 帧计划耗时。 */ diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp deleted file mode 100644 index 51406e1..0000000 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ /dev/null @@ -1,520 +0,0 @@ -#include "Async_Render_Backend.hpp" -#include "Datoviz_Visual_Backend.hpp" -#include "Gpu_Completion_Service.hpp" -#include "Render_Domain.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -namespace aethera::render_3d::detail { -namespace { -float wheel_step(double pixel, double angle) { - if (angle != 0.0) return static_cast(angle / 120.0); - return static_cast(pixel / 100.0); -} -Mouse_Button held_button(Mouse_Button_Mask buttons) { - if ((buttons & 1U) != 0) return Mouse_Button::left; - if ((buttons & 2U) != 0) return Mouse_Button::right; - if ((buttons & 4U) != 0) return Mouse_Button::middle; - return Mouse_Button::none; -} -void publish_datoviz_observation( - not_null 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) { - if (!result.empty()) result += ": "; - result += error.what(); - const auto* nested = dynamic_cast(&error); - if (!nested) return; - try { - nested->rethrow_nested(); - } - catch (const std::exception& cause) { - append_exception_description(cause, result); - } - catch (...) { - result += ": non-standard nested exception"; - } -} -std::string failure_description(const std::exception_ptr& failure) { - try { - if (failure) std::rethrow_exception(failure); - } - catch (const std::exception& error) { - std::string result; - append_exception_description(error, result); - return result; - } - catch (...) { - return "non-standard exception"; - } - return "empty exception"; -} -} -struct Async_Render_Backend::Implementation - : std::enable_shared_from_this { - struct Submission { - Shared_Prepared_Visual_Batch visuals{}; /* Scene CPU Prepare 发布的本帧数据句柄。 */ - Scene_3D_Parameters parameters{}; /* 本帧 Scene、Camera 与轴参数。 */ - Scene::Event_Batch events{}; /* Prepare 边界已交换的完整事件批次。 */ - }; - struct Pending { - Datoviz_Visual_Backend::Pending_Frame backend_frame{}; /* 三缓冲目标对应的已录制提交。 */ - not_null output; /* 调用方拥有;后端借用到 completed 返回同一地址。 */ - std::optional completion_reservation{}; - 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 与提交失败只允许一个退休事实。 */ - - explicit Pending(not_null frame) : output(frame) {} - }; - struct Gpu_Completion { - std::shared_ptr pending{}; /* 完成前保持后台目标、借用地址和回调集合。 */ - std::optional result{}; /* fence 正常交付时的结果。 */ - std::exception_ptr failure{}; /* 提交域或完成服务的 Unknown Failure。 */ - }; - std::shared_ptr render_domain; /* 同 GPU 唯一的 Datoviz/Vulkan 写入域。 */ - std::unique_ptr backend{}; /* Builder build() 创建;随后由本 Scene 串行使用。 */ - bool overlaps_gpu{}; /* 全部 Visual 是否拥有逐目标独立上传资源。 */ - std::atomic_bool available{true}; /* 是否仍接受新帧;停止或故障后永久为 false。 */ - - Implementation(std::uint32_t gpu_index_value, - bool validation_enabled_value, - std::vector visuals, - const Scene_3D_Parameters& 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: - case Visual_Family::splat: - case Visual_Family::pixel: - case Visual_Family::sphere: - case Visual_Family::primitive: - case Visual_Family::mesh: - case Visual_Family::path: - return true; - case Visual_Family::marker: - case Visual_Family::segment: - case Visual_Family::vector: - case Visual_Family::image: - case Visual_Family::labels: - case Visual_Family::glyph: - case Visual_Family::text: - case Visual_Family::volume: - return false; - } - return false; - }); - 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(); - - void stop() noexcept; - void fail(std::exception_ptr value) noexcept; - void render( - Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, - not_null frame, Scene::Event_Batch events, - 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); - void finish(Gpu_Completion completion); - void resolve(std::shared_ptr pending, - Datoviz_Visual_Backend::Completed_Frame completed); - void dispatch(const std::shared_ptr& event, Extent viewport); -}; - -Async_Render_Backend::Async_Render_Backend( - std::uint32_t gpu_index, bool validation_enabled, - std::vector visuals, - const Scene_3D_Parameters& initial_scene) - : implementation_(std::make_shared( - gpu_index, validation_enabled, std::move(visuals), initial_scene)) {} -Async_Render_Backend::~Async_Render_Backend() { - implementation_->stop(); -} - -Async_Render_Backend::Implementation::~Implementation() { - stop(); -} - -void Async_Render_Backend::Implementation::stop() noexcept { - available.store(false, std::memory_order_release); -} -void Async_Render_Backend::Implementation::fail( - std::exception_ptr value) noexcept { - available.store(false, std::memory_order_release); - try { - const auto description = failure_description(value); - std::fprintf(stderr, "Aethera 3D backend unavailable: %s\n", - description.c_str()); - std::fflush(stderr); - } - catch (...) {} -} -void Async_Render_Backend::Implementation::render( - Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, - not_null frame, Scene::Event_Batch events, - Async_Render_Backend::Submitted submitted_callback, - Async_Render_Backend::Completed completed_callback) { - if (!visuals || visuals->empty()) - throw std::invalid_argument("3D backend requires prepared Visual data"); - if (!submitted_callback || !completed_callback) - throw std::invalid_argument("3D backend requires a completion callback"); - if (!available.load(std::memory_order_acquire)) - throw std::runtime_error("3D backend is unavailable"); - auto pending = std::make_shared(frame); - pending->submitted_callback = std::move(submitted_callback); - pending->completed_callback = std::move(completed_callback); - auto self = shared_from_this(); - auto reservation = Gpu_Completion_Service::instance().prepare( - [self, pending](Gpu_Completion_Service::Result result) { - self->queue_finish(pending, std::move(result), {}); - }, - [self, pending](std::exception_ptr value) { - self->queue_finish(pending, {}, std::move(value)); - }, frame->taskflow_trace_requested()); - if (!reservation) { - if (reservation.result == - Gpu_Completion_Service::Admission_Result::capacity_exhausted) - throw std::runtime_error("GPU completion capacity is exhausted"); - throw std::runtime_error("GPU completion service is unavailable"); - } - pending->completion_reservation.emplace(std::move(reservation.reservation)); - 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) { - self->queue_finish(pending, {}, std::move(value)); - }); - } catch (...) { - auto failure_value = std::current_exception(); - fail(failure_value); - pending->completion_reservation.reset(); - pending->completed_callback(pending->output, std::move(failure_value)); - return; - } - if (queued == Render_Domain::Post_Result::queued) return; - auto failure_value = std::make_exception_ptr(std::runtime_error( - "render domain stopped before 3D frame preparation")); - pending->completion_reservation.reset(); - pending->completed_callback(pending->output, std::move(failure_value)); -} -void Async_Render_Backend::Implementation::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; - const auto sequence = pending->output->identity().sequence; - for (const auto& event : submission.events) { - if (event) event->mark_dispatch_started(sequence); - dispatch(event, submission.parameters.viewport); - } - submission.events.clear(); - pending->output->mark(Frame_Trace_Marker::backend_prepare_started); - const bool readback = pending->output->output() == Frame_3D_Output::pixels; - const bool observe = pending->output->taskflow_trace_requested(); - /* - * 只有结构 Prepare 才到达这里。Datoviz 对同一 GPU context 的 Scene/runtime - * 修改与 VkQueue submit 保持 Render Domain 单写者;稳定命令的映射上传已经在 - * 调用 worker 完成,fence 后 collect 也不会回到本域阻塞后续提交。 - */ - prepared = backend->prepare(submission.parameters, *submission.visuals, - sequence, observe, readback); - if (!prepared) { - 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); - pending->backend_frame = std::move(*prepared); - pending->prepared = true; - 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->backend_frame.observation.prepare_released_after_submission = - overlaps_gpu; - 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); - pending->completion_reservation->watch( - pending->backend_frame.device, pending->backend_frame.fence); - pending->completion_reservation.reset(); - pending->submitted_callback(pending->output, overlaps_gpu); -} -void Async_Render_Backend::Implementation::queue_finish( - std::shared_ptr pending, - std::optional result, - std::exception_ptr failure_value) { - if (pending->completion_enqueued.exchange(true, std::memory_order_acq_rel)) - return; - auto self = shared_from_this(); - 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{ - pending, std::move(result), completion_failure}); - } - catch (...) { - completion_failure = std::current_exception(); - self->fail(completion_failure); - } - pending->completed_callback(pending->output, - std::move(completion_failure)); - }); - } - 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)); - } -} -void Async_Render_Backend::Implementation::resolve( - std::shared_ptr pending, - Datoviz_Visual_Backend::Completed_Frame completed) { - pending->output->mark(Frame_Trace_Marker::readback_finished); - 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), - pending->output->identity()); -} -void Async_Render_Backend::Implementation::finish(Gpu_Completion completion) { - completion.pending->output->mark(Frame_Trace_Marker::gpu_completed); - if (completion.result) - 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)) { - backend->quarantine(std::move(completion.pending->backend_frame)); - } - else if (completion.pending->prepared) { - backend->discard(std::move(completion.pending->backend_frame)); - } - std::rethrow_exception(std::move(completion.failure)); - } - if (!completion.result || completion.result->error != - Gpu_Completion_Service::Completion_Error::none) { - const auto code = completion.result - ? static_cast(completion.result->error) - : std::numeric_limits::max(); - const auto vulkan = completion.result - ? static_cast(completion.result->vulkan_result) - : static_cast(VK_ERROR_UNKNOWN); - auto failure = std::make_exception_ptr(std::runtime_error( - "Datoviz GPU completion failed: error=" + - std::to_string(code) + ", VkResult=" + std::to_string(vulkan))); - if (completion.pending->submitted.load(std::memory_order_acquire)) { - backend->quarantine(std::move(completion.pending->backend_frame)); - } - else if (completion.pending->prepared) { - backend->discard(std::move(completion.pending->backend_frame)); - } - std::rethrow_exception(std::move(failure)); - } - auto completed = backend->collect( - std::move(completion.pending->backend_frame)); - completion.pending->prepared = false; - resolve(std::move(completion.pending), std::move(completed)); -} -void Async_Render_Backend::Implementation::dispatch( - const std::shared_ptr& event, Extent viewport) { - if (!event) return; - const auto* pointer = - dynamic_cast(event.get()); - const auto* wheel = - dynamic_cast(event.get()); - const auto* key = dynamic_cast(event.get()); - const bool pointer_valid = pointer && - std::isfinite(pointer->position_x()) && - std::isfinite(pointer->position_y()) && - pointer->position_x() >= 0.0 && pointer->position_y() >= 0.0 && - pointer->position_x() <= viewport.width && - pointer->position_y() <= viewport.height; - if (wheel && pointer_valid) { - 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(), viewport, - event->occurred_at.nanoseconds); - } - else if (pointer_valid && - (event->type == Event_Type::pointer_move || - event->type == Event_Type::pointer_press || - event->type == Event_Type::pointer_release)) { - backend->dispatch_pointer( - event->type, - static_cast(pointer->position_x()), - static_cast(pointer->position_y()), - event->type == Event_Type::pointer_move - ? held_button(pointer->pointer_buttons()) - : pointer->pointer_button(), - pointer->keyboard_modifiers(), viewport, - event->occurred_at.nanoseconds); - } - else if (key) backend->dispatch_key(*key); - if ((wheel && pointer_valid) || pointer_valid || key) event->accept(); - event->mark_dispatch_completed(); -} -void Async_Render_Backend::render( - Shared_Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, - not_null frame, Scene::Event_Batch events, - Submitted submitted, Completed completed) { - implementation_->render(std::move(visuals), parameters, frame, - std::move(events), std::move(submitted), - std::move(completed)); -} -bool Async_Render_Backend::available() const noexcept { - return implementation_->available.load(std::memory_order_acquire); -} -} diff --git a/render_3D/render_3D/detail/Async_Render_Backend.hpp b/render_3D/render_3D/detail/Async_Render_Backend.hpp deleted file mode 100644 index 4ab03e4..0000000 --- a/render_3D/render_3D/detail/Async_Render_Backend.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once -#include "../base/Frame_3D.hpp" -#include "Backend_Types.hpp" -#include -#include -#include -#include -#include -namespace aethera::render_3d::detail { -struct Async_Render_Backend final { -public: - Async_Render_Backend(std::uint32_t gpu_index, bool validation_enabled, - std::vector visuals, - const Scene_3D_Parameters& initial_scene); - ~Async_Render_Backend(); - Async_Render_Backend(const Async_Render_Backend&) = delete; - Async_Render_Backend& operator=(const Async_Render_Backend&) = delete; - using Submitted = std::function, bool)>; - using Completed = - std::function, std::exception_ptr)>; - /* - * 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, - not_null frame, Scene::Event_Batch events, - Submitted submitted, Completed completed); - [[nodiscard]] bool available() const noexcept; -private: - struct Implementation; - std::shared_ptr implementation_; /* 已入队闭包共享实现生命周期。 */ -}; -} diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp deleted file mode 100644 index 67281f8..0000000 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp +++ /dev/null @@ -1,100 +0,0 @@ -#pragma once - -#include "../base/Datoviz_Frame_Observation.hpp" -#include "Backend_Types.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace aethera::render_3d::detail { - -struct Datoviz_Render_Context; - -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_Observation observation{}; /* 与外部 Frame_3D 同帧的原始诊断。 */ - }; - - struct Completed_Frame { - Extent extent{}; /* 已完成 GPU 读回的像素尺寸。 */ - std::vector pixels{}; /* 已完成 GPU 读回的连续 RGBA8 像素。 */ - Datoviz_Frame_Observation observation{}; /* 已补全 GPU 和读回阶段的原始诊断。 */ - }; - - 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, - 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; - - void dispatch_pointer(Event_Type type, float x, float y, - Mouse_Button button, - Keyboard_Modifier modifiers, - Extent viewport, - std::uint64_t occurred_at_ns); - void dispatch_wheel(float x, float y, float delta_x, float delta_y, - Keyboard_Modifier modifiers, - Extent viewport, - std::uint64_t occurred_at_ns); - void dispatch_key(const Key_Event& event); -}; - -} // 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 deleted file mode 100644 index 3ccb855..0000000 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.ipp +++ /dev/null @@ -1,165 +0,0 @@ -#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. */ - not_null scene_buffer; /* Scene-side stable resource label; Scene owns it. */ - owner 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。 */ - not_null 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 { - owner runtime{}; /* 后端拥有的目标槽命令运行时。 */ - owner 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, - std::uint64_t occurred_at_ns); - void dispatch_wheel(float x, float y, float delta_x, float delta_y, - Keyboard_Modifier modifiers, - Extent viewport, - std::uint64_t occurred_at_ns); - void dispatch_key(const Key_Event& event); - void abandon_resources() noexcept; - void destroy(); - - std::shared_ptr render_context_{}; /* 同一 GPU 上所有后端共享的 Device 与分配器。 */ - mutable std::mutex target_mutex_{}; /* 当前 Scene 三个目标槽的唯一生命周期同步源。 */ - std::array runtime_slots_{}; /* 三套独立 runtime/emitter 状态机。 */ - owner 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_{}; /* 随相机变换的三维刻度与轴标题。 */ - owner item_interaction_{}; /* 后端显式销毁的图元交互器。 */ - owner hover_readout_{}; /* 后端显式销毁的悬停提示。 */ - owner camera_controller_{}; /* 后端显式销毁的相机控制器。 */ - owner input_router_{}; /* 后端显式销毁的输入路由器。 */ - owner 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/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp index 9360a73..7f491ad 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp @@ -1,5 +1,6 @@ #include "Gpu_Completion_Service.hpp" #include "Exception.hpp" +#include #include #include #include @@ -62,9 +63,9 @@ Gpu_Completion_Service::Prepare_Result::operator bool() const noexcept { return result == Admission_Result::none; } -void Gpu_Completion_Service::Reservation::watch(VkDevice device, - VkFence fence) { - if (!pending_ || device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE) +void Gpu_Completion_Service::Reservation::watch(std::uintptr_t device, + std::uintptr_t fence) { + if (!pending_ || device == 0 || fence == 0) throw std::logic_error( "GPU completion reservation or fence is invalid"); pending_->service->watch(pending_, device, fence); @@ -85,7 +86,7 @@ Gpu_Completion_Service::Prepare_Result Gpu_Completion_Service::prepare( void Gpu_Completion_Service::watch( const std::shared_ptr& pending, - VkDevice device, VkFence fence) { + std::uintptr_t device, std::uintptr_t fence) { static_cast(*d).watch(pending, device, fence); } @@ -149,7 +150,7 @@ Gpu_Completion_Service::Private::prepare( void Gpu_Completion_Service::Private::watch( const std::shared_ptr& pending, - VkDevice device, VkFence fence) { + std::uintptr_t device, std::uintptr_t fence) { { std::lock_guard lock(service_mutex); if (!pending || pending->service != object || @@ -239,7 +240,7 @@ void Gpu_Completion_Service::Private::poll() noexcept { completion = std::move(pending->completion); on_exception = std::move(pending->on_exception); result.error = error; - result.vulkan_result = vulkan_result; + result.vulkan_result = static_cast(vulkan_result); if (pending->observe) result.wait_duration_ns = elapsed_nanoseconds(pending->watched_at); pending->status = Gpu_Completion_Pending_Fence::Status::canceled; @@ -286,8 +287,8 @@ void Gpu_Completion_Service::Private::poll() noexcept { std::size_t reserved_count{}; bool needs_more{}; for (auto iterator = active.begin(); iterator != active.end();) { - VkDevice device{VK_NULL_HANDLE}; - VkFence fence{VK_NULL_HANDLE}; + std::uintptr_t device{}; + std::uintptr_t fence{}; std::chrono::steady_clock::time_point watched_at{}; Gpu_Completion_Pending_Fence::Status status{}; { @@ -324,7 +325,9 @@ void Gpu_Completion_Service::Private::poll() noexcept { [](State_Access states) { ++states.template get().fence_probe_count; }); - const VkResult result = vkGetFenceStatus(device, fence); + const VkResult result = vkGetFenceStatus( + reinterpret_cast(device), + reinterpret_cast(fence)); if (result == VK_NOT_READY) { needs_more = true; ++iterator; diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.hpp b/render_3D/render_3D/detail/Gpu_Completion_Service.hpp index ac22bce..6fc0361 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.hpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.hpp @@ -1,7 +1,6 @@ #pragma once #include "../Gpu_Completion_State.hpp" #include -#include #include #include #include @@ -50,7 +49,7 @@ struct Gpu_Completion_Service : Def; @@ -63,7 +62,7 @@ struct Gpu_Completion_Service : Def pending) noexcept; @@ -87,7 +86,7 @@ struct Gpu_Completion_Service : Def& pending, - VkDevice device, VkFence fence); + std::uintptr_t device, std::uintptr_t fence); void cancel(const std::shared_ptr& pending); }; } diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.ipp b/render_3D/render_3D/detail/Gpu_Completion_Service.ipp index ba38e6f..777da69 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.ipp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.ipp @@ -12,8 +12,8 @@ struct Gpu_Completion_Pending_Fence { watched, canceled }; - VkDevice device{VK_NULL_HANDLE}; /* fence 所属 Vulkan Device。 */ - VkFence fence{VK_NULL_HANDLE}; /* 受监视的 Vulkan fence。 */ + std::uintptr_t device{}; + std::uintptr_t fence{}; Gpu_Completion_Service::Completion completion{}; /* 完成或隔离后的交付回调。 */ Gpu_Completion_Service::Exception_Handler on_exception{}; /* 回调异常的隔离入口。 */ std::chrono::steady_clock::time_point watched_at{}; /* 开始监视的单调时钟时刻。 */ @@ -46,7 +46,7 @@ struct Gpu_Completion_Service::Private : Prev_Private { Exception_Handler on_exception, bool observe); void watch(const std::shared_ptr& pending, - VkDevice device, VkFence fence); + std::uintptr_t device, std::uintptr_t fence); void cancel(const std::shared_ptr& pending); [[nodiscard]] Gpu_Completion_State state() const; void request_poll(std::chrono::nanoseconds delay = probe_interval) noexcept; diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp deleted file mode 100644 index 6df8964..0000000 --- a/render_3D/render_3D/detail/Render_Domain.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include "Render_Domain.hpp" -#include -#include -#include -#include -#include -#include -namespace aethera::render_3d::detail { -namespace { -struct Render_Domain_Registry { - std::mutex mutex; - std::unordered_map> domains; -}; -Render_Domain_Registry& registry() { - static Render_Domain_Registry value; - return value; -} -} -Render_Domain::Task::Task( - std::function function_value, - std::function exception_handler_value) - : function(std::move(function_value)), - on_exception(std::move(exception_handler_value)) {} -std::shared_ptr Render_Domain::acquire(std::uint32_t gpu_index) { - auto& storage = registry(); - std::lock_guard lock(storage.mutex); - auto& entry = storage.domains[gpu_index]; - if (auto domain = entry.lock()) return domain; - auto domain = std::shared_ptr(new Render_Domain(), - &Render_Domain::destroy); - entry = domain; - return domain; -} -Render_Domain::Render_Domain() : thread_([this] { run(); }) {} -Render_Domain::~Render_Domain() { - request_stop(); - if (thread_.joinable()) thread_.detach(); -} -void Render_Domain::destroy(owner domain) noexcept { - if (!domain) return; - /* - * 最后一个外部引用只关闭准入,不等待线程。run() 会消费完停止边界前已经 - * 入队的命令,再由域线程 detach 并 delete 自身;因此调用方析构路径没有 - * join、自旋或同步栅栏,Datoviz/Vulkan 对象也始终在其唯一写者线程销毁。 - */ - domain->request_stop(); - domain->destroy_on_exit_.store(true, std::memory_order_release); -} -void Render_Domain::request_stop() noexcept { - bool expected = false; - if (!stopping_.compare_exchange_strong( - expected, true, std::memory_order_acq_rel)) return; -} -Render_Domain::Post_Result Render_Domain::post( - std::function function, Exception_Handler on_exception) { - if (!function) throw std::invalid_argument("render domain task is empty"); - if (!on_exception) - throw std::invalid_argument("render domain exception handler is empty"); - if (stopping_.load(std::memory_order_acquire)) return Post_Result::stopping; - auto task = std::make_unique(std::move(function), std::move(on_exception)); - active_posters_.fetch_add(1, std::memory_order_acq_rel); - if (stopping_.load(std::memory_order_acquire)) { - active_posters_.fetch_sub(1, std::memory_order_release); - return Post_Result::stopping; - } - const bool enqueued = tasks_.enqueue(std::move(task)); - if (!enqueued) { - active_posters_.fetch_sub(1, std::memory_order_release); - throw std::bad_alloc{}; - } - active_posters_.fetch_sub(1, std::memory_order_release); - return Post_Result::queued; -} -void Render_Domain::run() noexcept { - current_domain_ = not_null{this}; - for (;;) { - std::unique_ptr task; - bool received = tasks_.wait_dequeue_timed( - task, std::chrono::milliseconds(1)); - if (!received && stopping_.load(std::memory_order_acquire) && - active_posters_.load(std::memory_order_acquire) == 0) { - /* stop 边界关闭生产者后再真实探测队列,不能用 - * size_approx 决定生命周期。 */ - received = tasks_.try_dequeue(task); - } - if (received && task) { - try { - task->function(); - } - catch (...) { - try { - task->on_exception( - contextual_exception("executing render domain task", - std::current_exception())); - } - catch (...) {} - } - task.reset(); - } - // 停止后不再接受新生产者;一次真实的 timed dequeue 为空,才说明 - // 已经消费完所有先于停止边界进入的任务。size_approx() 只能诊断, - // 不能参与线程退出正确性。 - if (!received && stopping_.load(std::memory_order_acquire) && - active_posters_.load(std::memory_order_acquire) == 0) { - current_domain_ = nullptr; - if (destroy_on_exit_.load(std::memory_order_acquire)) { - if (thread_.joinable()) thread_.detach(); - delete this; - } - return; - } - } -} -} diff --git a/render_3D/render_3D/detail/Render_Domain.hpp b/render_3D/render_3D/detail/Render_Domain.hpp deleted file mode 100644 index 6c2302c..0000000 --- a/render_3D/render_3D/detail/Render_Domain.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once -#include "Exception.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -namespace aethera::render_3d::detail { -struct Render_Domain final : public std::enable_shared_from_this { - struct Task { - Task(std::function function_value, - std::function exception_handler_value); - std::function function; /* 仅在所属 GPU Render Domain 线程执行的闭包。 */ - std::function on_exception; /* 闭包 Unknown Failure 的任务隔离出口。 */ - }; -public: - static std::shared_ptr acquire(std::uint32_t gpu_index); - ~Render_Domain(); - Render_Domain(const Render_Domain&) = delete; - Render_Domain& operator=(const Render_Domain&) = delete; - using Exception_Handler = std::function; - enum struct Post_Result : std::uint8_t { queued, stopping }; - /* 把闭包所有权无等待转移到所属 GPU 的单消费者线程。 */ - [[nodiscard]] Post_Result post(std::function function, - Exception_Handler on_exception); -private: - Render_Domain(); - static void destroy(owner domain) noexcept; - void request_stop() noexcept; - void run() noexcept; - static constexpr std::size_t initial_capacity = 64; - inline static thread_local Render_Domain* - current_domain_{}; /* 当前线程正在执行的 GPU 域。 */ - moodycamel::BlockingConcurrentQueue> tasks_{initial_capacity}; /* 多生产者闭包入口与唯一消费队列。 */ - std::atomic_size_t active_posters_{}; /* 停止准入边界内仍可能完成入队的生产者数量。 */ - std::atomic_bool stopping_{}; /* 拒绝新闭包并准备退出。 */ - std::atomic_bool destroy_on_exit_{}; /* 最后一个引用在域线程释放时由该线程自销毁。 */ - std::thread thread_; /* 唯一允许访问 Datoviz/Vulkan 对象的线程。 */ -}; -} diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index 737f2a5..ca82367 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -1,10 +1,16 @@ #pragma once -#include "../detail/Async_Render_Backend.hpp" +#include "Scene_Datoviz_State.hpp" +#include "../detail/Gpu_Completion_Service.hpp" #include #include #include +#include +#include #include +#include +#include #include +#include #include #include #include @@ -12,14 +18,70 @@ namespace aethera::render_3d { namespace detail { template struct Scene_Paint_Context { - std::shared_ptr backend{}; /* Scene 拥有、异步命令延长生命周期的后端。 */ - not_null scene; /* 仅在 Scene 拥有本上下文期间读取当前 Prop。 */ - Scene_3D_Parameters parameters{}; /* 当前 Prepare 读取的 Scene 参数。 */ - std::shared_ptr visuals{ /* 当前 Prepare 发布的数据句柄集合。 */ - std::make_shared()}; + not_null scene; /* 仅在 Scene 拥有本上下文期间读取当前 Prop。 */ + Scene_3D_Parameters parameters{}; /* 当前 Prepare 读取的 Scene 参数。 */ + std::shared_ptr visuals{ + /* 当前 Prepare 发布的数据句柄集合。 */ + std::make_shared() + }; }; +inline float wheel_step(double pixel, double angle) { + if (angle != 0.0) return static_cast(angle / 120.0); + return static_cast(pixel / 100.0); } -struct Render_Scene_3D::Private : Prev_Private { +inline Mouse_Button held_button(Mouse_Button_Mask buttons) { + if ((buttons & 1U) != 0) return Mouse_Button::left; + if ((buttons & 2U) != 0) return Mouse_Button::right; + if ((buttons & 4U) != 0) return Mouse_Button::middle; + return Mouse_Button::none; +} +inline void publish_datoviz_observation( + not_null 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)); +} +inline std::string failure_description(const std::exception_ptr& failure) { + try { + if (failure) std::rethrow_exception(failure); + } + catch (const std::exception& error) { + return error.what(); + } + catch (...) { + return "non-standard exception"; + } + return "empty exception"; +} +} +struct Render_Scene_3D::Private : Prev_Private, + private detail::Scene_Datoviz_State { enum struct Frame_Phase : std::uint8_t { available, preparing, @@ -36,6 +98,8 @@ struct Render_Scene_3D::Private : Prev_Private { std::atomic_bool gpu_submitted{}; std::atomic_bool gpu_completed{}; std::exception_ptr failure{}; + std::optional + datoviz_frame{}; }; /* * 3D 核心线程模型: @@ -58,10 +122,11 @@ struct Render_Scene_3D::Private : Prev_Private { std::atomic_bool completion_busy{}; std::atomic> frame_callback{}; std::atomic> submitted_callback{}; - std::shared_ptr backend{}; /* Scene 拥有的异步后端;已入队命令自行延长实现寿命。 */ - std::shared_ptr paint_context{}; /* Paint 节点读取 Scene 当前 Prop 的生命周期门闩。 */ - Root* camera_component{}; /* Builder 绑定的 Camera 组件。 */ - Root* axes_component{}; /* Builder 绑定的三轴组件。 */ + bool overlaps_gpu{}; + std::atomic_bool backend_available{true}; + std::shared_ptr paint_context{}; /* Paint 节点读取 Scene 当前 Prop 的生命周期门闩。 */ + Root* camera_component{}; /* Builder 绑定的 Camera 组件。 */ + Root* axes_component{}; /* Builder 绑定的三轴组件。 */ Camera_Descriptor (*read_camera)(not_null){}; /* 读取 Camera 当前配置。 */ std::array (*read_axes)(not_null){}; /* 读取三轴当前配置。 */ Frame_Context* active_context{}; /* 只由唯一 CPU Prepare 图访问。 */ @@ -73,102 +138,127 @@ struct Render_Scene_3D::Private : Prev_Private { not_null camera, not_null axes, Camera_Descriptor (*camera_reader)(not_null), std::array (*axes_reader)(not_null)); - template [[nodiscard]] detail::Scene_3D_Parameters parameters(Object* object) const; + template + [[nodiscard]] detail::Scene_3D_Parameters parameters(Object* object) const; [[nodiscard]] Frame_Context* context_for( not_null frame) noexcept; - template void try_release_prepare( + template + void try_release_prepare( Object* object, not_null context); - template void arm_completion(Object* object); - template void consume_completion(Object* object); - template void complete_frame( + template + void arm_completion(Object* object); + template + void consume_completion(Object* object); + template + void complete_frame( Object* object, not_null context); + template + void render_datoviz( + Object* object, not_null context, + detail::Shared_Prepared_Visual_Batch visuals, + detail::Scene_3D_Parameters parameters); + template + void collect_datoviz( + Object* object, not_null context, + std::optional result, + std::exception_ptr failure) noexcept; + void dispatch_datoviz(const std::shared_ptr& event, + Extent viewport); + void fail_datoviz(std::exception_ptr failure) noexcept; /* CRTP 覆盖:绑定 Scene 机制和 Render_Scene_3D 公开薄壳。 */ - template void bind_private_crtp(Object* object); - template void ensure_frame_taskflow(Object* object); - template [[nodiscard]] Render_Result render( + template + void bind_private_crtp(Object* object); + template + void ensure_frame_taskflow(Object* object); + template + [[nodiscard]] Render_Result render( Object* object, not_null frame); - template void set_frame_callback(Object* object, Frame_Callback callback); - template void set_submitted_frame_callback(Object* object, Submitted_Frame_Callback callback); - template void set_view_active(Object* object, bool active); - template void reset_diagnostics(Object* object); + template + void set_frame_callback(Object* object, Frame_Callback callback); + template + void set_submitted_frame_callback(Object* object, Submitted_Frame_Callback callback); + template + void set_view_active(Object* object, bool active); + template + void reset_diagnostics(Object* object); }; template Render_Scene_3D::Builder::Builder() : Base() {} -template template -typename Render_Scene_3D::Builder::Final_Builder& -Render_Scene_3D::Builder::add_renderable( +template +template +typename Render_Scene_3D::Builder::Final_Builder& Render_Scene_3D::Builder::add_renderable( not_null visual_value) { if (std::ranges::any_of(visuals, [visual_value](const Visual_Binding& value) { - return value.object == visual_value; - })) + return value.object == visual_value; + })) throw std::logic_error("Render_Scene_3D cannot attach the same Visual twice"); Visual_Binding binding{ visual_value, Visual_Object::Attached_Object::Specification::family, [](not_null root, std::shared_ptr context) { - using Definition = typename Visual_Object::Attached_Object; - using Prepared = typename Definition::Prepared_Visual; - const not_null object{static_cast(root.get())}; - Base::private_access(object.get()).template get() - .bind_paint_target(std::move(context), [](void* raw_context, const Root* identity, - const Prepared& prepared) { - auto& submission = *static_cast*>(raw_context); - const auto visual_identity = reinterpret_cast(identity); - auto erased = detail::erase_prepared_visual(prepared); - auto& visuals = *submission.visuals; - const auto found = std::ranges::find( - visuals, visual_identity, - &detail::Prepared_Visual_Instance::identity); - if (found == visuals.end()) - throw std::logic_error( - "3D Visual published outside its Scene registration"); - /* Builder 已经为每个 Visual 建立稳定槽位。并行 Submit 节点只写各自 - * 的 visual 成员,不扩容容器,也不互相读写同一对象。 */ - found->visual = std::move(erased); - }); - }}; + using Definition = typename Visual_Object::Attached_Object; + using Prepared = typename Definition::Prepared_Visual; + const not_null object{static_cast(root.get())}; + Base::private_access(object.get()).template get() + .bind_paint_target(std::move(context), [](void* raw_context, const Root* identity, + const Prepared& prepared) { + auto& submission = *static_cast*>(raw_context); + const auto visual_identity = reinterpret_cast(identity); + auto erased = detail::erase_prepared_visual(prepared); + auto& visuals = *submission.visuals; + const auto found = std::ranges::find( + visuals, visual_identity, + &detail::Prepared_Visual_Instance::identity); + if (found == visuals.end()) + throw std::logic_error( + "3D Visual published outside its Scene registration"); + /* Builder 已经为每个 Visual 建立稳定槽位。并行 Submit 节点只写各自 + * 的 visual 成员,不扩容容器,也不互相读写同一对象。 */ + found->visual = std::move(erased); + }); + } + }; visuals.push_back(binding); this->template add_dependency_node(visual_value.get()); return static_cast(*this); } -template template - requires std::derived_from -typename Render_Scene_3D::Builder::Final_Builder& -Render_Scene_3D::Builder::add_camera( +template +template requires std::derived_from +typename Render_Scene_3D::Builder::Final_Builder& Render_Scene_3D::Builder::add_camera( not_null camera_value) { if (camera) throw std::logic_error("Render_Scene_3D accepts one Camera component"); camera = camera_value; read_camera = [](not_null root) { const not_null object{static_cast(root.get())}; const Camera_3D::Prop& prop = - Base::template current_prop(object.get()); - return Camera_Descriptor{prop.initial_view, prop.projection, prop.controller, - prop.turntable_control, prop.arcball_control, - prop.fly_control, prop.panzoom_control, - prop.vertical_field_of_view_degrees, - prop.near_plane, prop.far_plane}; + Base::template current_prop(object.get()); + return Camera_Descriptor{ + prop.initial_view, prop.projection, prop.controller, + prop.turntable_control, prop.arcball_control, + prop.fly_control, prop.panzoom_control, + prop.vertical_field_of_view_degrees, + prop.near_plane, prop.far_plane + }; }; return static_cast(*this); } -template template - requires std::derived_from -typename Render_Scene_3D::Builder::Final_Builder& -Render_Scene_3D::Builder::add_axes( +template +template requires std::derived_from +typename Render_Scene_3D::Builder::Final_Builder& Render_Scene_3D::Builder::add_axes( not_null axes_value) { if (axes) throw std::logic_error("Render_Scene_3D accepts one Axes component"); axes = axes_value; read_axes = [](not_null root) { const not_null object{static_cast(root.get())}; const Axes_3D::Prop& prop = - Base::template current_prop(object.get()); + Base::template current_prop(object.get()); return std::array{prop.x_axis, prop.y_axis, prop.z_axis}; }; return static_cast(*this); } template -typename Render_Scene_3D::Builder::Final_Builder& -Render_Scene_3D::Builder::use_gpu(std::uint32_t gpu_index_value, - bool validation_enabled_value) { +typename Render_Scene_3D::Builder::Final_Builder& Render_Scene_3D::Builder::use_gpu(std::uint32_t gpu_index_value, + bool validation_enabled_value) { gpu_index = gpu_index_value; validation_enabled = validation_enabled_value; return static_cast(*this); @@ -187,7 +277,8 @@ std::expected, Dependency_Graph_Error> Render_Scene_3D:: for (const auto& visual : visuals) registrations.push_back({ reinterpret_cast(visual.object.get()), - visual.family}); + visual.family + }); private_data.initialize_backend(scene.get(), gpu_index, validation_enabled, std::move(registrations), camera, axes, read_camera, read_axes); @@ -199,8 +290,239 @@ detail::Scene_3D_Parameters Render_Scene_3D::Private::parameters(Object* object) const Prop& prop = static_cast( double_buffer::detail::Internal_Access::current_prop(object)); const auto axis = read_axes(axes_component); - return {prop.viewport, prop.clear_color, - read_camera(camera_component), axis[0], axis[1], axis[2]}; + return { + prop.viewport, prop.clear_color, + read_camera(camera_component), axis[0], axis[1], axis[2] + }; +} +inline void Render_Scene_3D::Private::fail_datoviz( + std::exception_ptr failure) noexcept { + backend_available.store(false, std::memory_order_release); + try { + const auto description = detail::failure_description(failure); + std::fprintf(stderr, "Aethera 3D Scene unavailable: %s\n", + description.c_str()); + std::fflush(stderr); + } + catch (...) {} +} +inline void Render_Scene_3D::Private::dispatch_datoviz( + const std::shared_ptr& event, Extent viewport) { + if (!event) return; + const auto* pointer = + dynamic_cast(event.get()); + const auto* wheel = + dynamic_cast(event.get()); + const auto* key = dynamic_cast(event.get()); + const bool pointer_valid = pointer && + std::isfinite(pointer->position_x()) && + std::isfinite(pointer->position_y()) && + pointer->position_x() >= 0.0 && pointer->position_y() >= 0.0 && + pointer->position_x() <= viewport.width && + pointer->position_y() <= viewport.height; + if (wheel && pointer_valid) { + dispatch_wheel( + static_cast(pointer->position_x()), + static_cast(pointer->position_y()), + detail::wheel_step(wheel->pixel_delta_x_value(), + wheel->angle_delta_x_value()), + detail::wheel_step(wheel->pixel_delta_y_value(), + wheel->angle_delta_y_value()), + pointer->keyboard_modifiers(), viewport, + event->occurred_at.nanoseconds); + } + else if (pointer_valid && + (event->type == Event_Type::pointer_move || + event->type == Event_Type::pointer_press || + event->type == Event_Type::pointer_release)) { + dispatch_pointer( + event->type, + static_cast(pointer->position_x()), + static_cast(pointer->position_y()), + event->type == Event_Type::pointer_move + ? detail::held_button(pointer->pointer_buttons()) + : pointer->pointer_button(), + pointer->keyboard_modifiers(), viewport, + event->occurred_at.nanoseconds); + } + else if (key) { + dispatch_key(*key); + } + if ((wheel && pointer_valid) || pointer_valid || key) event->accept(); + event->mark_dispatch_completed(); +} +template +void Render_Scene_3D::Private::render_datoviz( + Object* object, not_null context, + detail::Shared_Prepared_Visual_Batch visuals, + detail::Scene_3D_Parameters scene) { + if (!visuals || visuals->empty()) throw std::invalid_argument("3D Scene requires prepared Visual data"); + if (!backend_available.load(std::memory_order_acquire)) throw std::runtime_error("3D Scene resources are unavailable"); + const not_null frame{context->frame}; + auto reservation = detail::Gpu_Completion_Service::instance().prepare( + [this, object, context](detail::Gpu_Completion_Service::Result result) { + collect_datoviz(object, context, std::move(result), {}); + }, + [this, object, context](std::exception_ptr failure) { + collect_datoviz(object, context, {}, std::move(failure)); + }, + frame->taskflow_trace_requested()); + if (!reservation) { + if (reservation.result == detail::Gpu_Completion_Service:: + Admission_Result::capacity_exhausted) + throw std::runtime_error( + "GPU completion capacity is exhausted"); + throw std::runtime_error("GPU completion service is unavailable"); + } + try { + const auto sequence = frame->identity().sequence; + const bool observe = frame->taskflow_trace_requested(); + const bool readback = frame->output() == Frame_3D_Output::pixels; + std::optional prepared; + if (context->events.empty()) { + frame->mark(Frame_Trace_Marker::backend_prepare_started); + prepared = try_prepare_reused( + scene, *visuals, sequence, observe, readback); + } + if (!prepared) { + for (const auto& event : context->events) { + if (event) event->mark_dispatch_started(sequence); + dispatch_datoviz(event, scene.viewport); + } + context->events.clear(); + frame->mark(Frame_Trace_Marker::backend_prepare_started); + prepared = prepare( + scene, *visuals, sequence, observe, readback); + } + if (!prepared) + throw std::logic_error( + "Datoviz did not provide a frame target after Scene admission"); + frame->mark(Frame_Trace_Marker::backend_prepare_finished); + context->datoviz_frame.emplace(std::move(*prepared)); + context->datoviz_frame->observation.prepare_released_after_submission = + overlaps_gpu; + frame->mark(Frame_Trace_Marker::backend_queue_entered); + frame->mark(Frame_Trace_Marker::backend_submit_queued); + auto completion_reservation = std::make_shared< + detail::Gpu_Completion_Service::Reservation>( + std::move(reservation.reservation)); + submit( + *context->datoviz_frame, + [this, object, context, frame, + completion_reservation = std::move(completion_reservation)]( + std::exception_ptr submission_failure) mutable { + try { + const bool submission_failed = + static_cast(submission_failure); + frame->mark(Frame_Trace_Marker::backend_queue_left); + if (submission_failed) { + fail_datoviz(submission_failure); + if (context->datoviz_frame) { + discard( + std::move(*context->datoviz_frame)); + context->datoviz_frame.reset(); + } + context->failure = std::move(submission_failure); + context->overlaps_gpu = false; + context->gpu_submitted.store( + true, std::memory_order_release); + context->gpu_completed.store( + true, std::memory_order_release); + } + else { + frame->mark(Frame_Trace_Marker::gpu_submitted); + context->overlaps_gpu = overlaps_gpu; + context->gpu_submitted.store( + true, std::memory_order_release); + completion_reservation->watch( + context->datoviz_frame->device, + context->datoviz_frame->fence); + } + try_release_prepare(object, context); + if (context->phase.load(std::memory_order_acquire) == + Frame_Phase::submitted && submission_failed) + arm_completion(object); + } + catch (...) { + collect_datoviz( + object, context, {}, std::current_exception()); + } + }); + } + catch (...) { + auto failure = std::current_exception(); + fail_datoviz(failure); + if (context->datoviz_frame) { + if (context->gpu_submitted.load(std::memory_order_acquire)) quarantine(std::move(*context->datoviz_frame)); + else discard(std::move(*context->datoviz_frame)); + context->datoviz_frame.reset(); + } + context->failure = std::move(failure); + context->overlaps_gpu = false; + context->gpu_submitted.store(true, std::memory_order_release); + context->gpu_completed.store(true, std::memory_order_release); + try_release_prepare(object, context); + if (context->phase.load(std::memory_order_acquire) == + Frame_Phase::submitted) + arm_completion(object); + } +} +template +void Render_Scene_3D::Private::collect_datoviz( + Object* object, not_null context, + std::optional result, + std::exception_ptr failure) noexcept { + try { + const not_null frame{context->frame}; + frame->mark(Frame_Trace_Marker::gpu_completed); + frame->mark(Frame_Trace_Marker::readback_started); + if (!context->datoviz_frame) throw std::logic_error("3D completion lost its Datoviz target"); + if (result) + context->datoviz_frame->observation.gpu_fence_wait_ns = + result->wait_duration_ns; + if (failure) std::rethrow_exception(std::move(failure)); + if (!result || result->error != + detail::Gpu_Completion_Service::Completion_Error::none) { + const auto code = result + ? static_cast(result->error) + : std::numeric_limits::max(); + const auto vulkan = result + ? static_cast(result->vulkan_result) + : -1; + throw std::runtime_error( + "Datoviz GPU completion failed: error=" + + std::to_string(code) + ", VkResult=" + + std::to_string(vulkan)); + } + auto completed = collect( + std::move(*context->datoviz_frame)); + context->datoviz_frame.reset(); + frame->mark(Frame_Trace_Marker::readback_finished); + detail::publish_datoviz_observation( + frame, std::move(completed.observation)); + std::array outputs{frame}; + detail::Frame_3D_Access::assign_pixels( + outputs, completed.extent, std::move(completed.pixels), + frame->identity()); + } + catch (...) { + context->failure = std::current_exception(); + fail_datoviz(context->failure); + if (context->datoviz_frame) { + quarantine(std::move(*context->datoviz_frame)); + context->datoviz_frame.reset(); + } + } + try { + context->gpu_completed.store(true, std::memory_order_release); + try_release_prepare(object, context); + if (context->phase.load(std::memory_order_acquire) == + Frame_Phase::submitted) + arm_completion(object); + } + catch (...) { + fail_datoviz(std::current_exception()); + } } template void Render_Scene_3D::Private::complete_frame( @@ -208,92 +530,89 @@ void Render_Scene_3D::Private::complete_frame( const auto frame = context->frame; const auto callback = frame_callback.load(std::memory_order_acquire); frame->mark(Frame_Trace_Marker::frame_ready); - if (context->trace_started) - aethera::detail::finish_taskflow_trace(*frame); - + if (context->trace_started) aethera::detail::finish_taskflow_trace(*frame); /* Scene 仅借用外部 Frame。完成通知前必须先清除所有借用引用并开放 * Frame_Context;回调之后是否发布、统计或复用只由调用方帧策略决定。 */ context->frame = nullptr; context->events.clear(); context->failure = {}; + context->datoviz_frame.reset(); context->trace_started = false; context->overlaps_gpu = false; context->cpu_finished.store(false, std::memory_order_relaxed); context->gpu_submitted.store(false, std::memory_order_relaxed); context->gpu_completed.store(false, std::memory_order_relaxed); context->phase.store(Frame_Phase::available, std::memory_order_release); - if (callback && *callback) (*callback)(frame); + if (callback && *callback) { + try { + (*callback)(frame); + } + catch (...) { + fail_datoviz(std::current_exception()); + } + } } - -inline Render_Scene_3D::Private::Frame_Context* -Render_Scene_3D::Private::context_for(not_null frame) noexcept { +inline Render_Scene_3D::Private::Frame_Context* Render_Scene_3D::Private::context_for(not_null frame) noexcept { const auto found = std::ranges::find(frame_contexts, frame.get(), - &Frame_Context::frame); + &Frame_Context::frame); return found == frame_contexts.end() - ? nullptr - : &*found; + ? nullptr + : &*found; } - template void Render_Scene_3D::Private::try_release_prepare( Object* object, not_null context) { if (!context->cpu_finished.load(std::memory_order_acquire) || !context->gpu_submitted.load(std::memory_order_acquire) || (!context->overlaps_gpu && - !context->gpu_completed.load(std::memory_order_acquire))) + !context->gpu_completed.load(std::memory_order_acquire))) return; auto expected = Frame_Phase::preparing; if (!context->phase.compare_exchange_strong( - expected, Frame_Phase::submitted, std::memory_order_acq_rel, - std::memory_order_acquire)) + expected, Frame_Phase::submitted, std::memory_order_acq_rel, + std::memory_order_acquire)) return; prepare_active.store(false, std::memory_order_release); if (const auto callback = - submitted_callback.load(std::memory_order_acquire); - callback && *callback) + submitted_callback.load(std::memory_order_acquire); callback && *callback) (*callback)(context->frame, context->overlaps_gpu); - if (context->gpu_completed.load(std::memory_order_acquire)) - arm_completion(object); + if (context->gpu_completed.load(std::memory_order_acquire)) arm_completion(object); } - template void Render_Scene_3D::Private::arm_completion(Object* object) { bool expected{}; if (!completion_busy.compare_exchange_strong( - expected, true, std::memory_order_acq_rel, - std::memory_order_acquire)) + expected, true, std::memory_order_acq_rel, + std::memory_order_acquire)) return; - aethera::schedule_task("render_3d.frame.retire", [this, object] { - consume_completion(object); - }); + consume_completion(object); } - template void Render_Scene_3D::Private::consume_completion(Object* object) { Frame_Context* selected{}; for (auto& context : frame_contexts) { if (context.phase.load(std::memory_order_acquire) != - Frame_Phase::submitted || + Frame_Phase::submitted || !context.gpu_completed.load(std::memory_order_acquire)) continue; if (!selected || context.frame->identity().sequence < - selected->frame->identity().sequence) + selected->frame->identity().sequence) selected = &context; } if (!selected) { completion_busy.store(false, std::memory_order_release); if (std::ranges::any_of(frame_contexts, [](const auto& context) { - return context.phase.load(std::memory_order_acquire) == - Frame_Phase::submitted && - context.gpu_completed.load(std::memory_order_acquire); - })) + return context.phase.load(std::memory_order_acquire) == + Frame_Phase::submitted && + context.gpu_completed.load(std::memory_order_acquire); + })) arm_completion(object); return; } auto expected = Frame_Phase::submitted; if (!selected->phase.compare_exchange_strong( - expected, Frame_Phase::completing, std::memory_order_acq_rel, - std::memory_order_acquire)) { + expected, Frame_Phase::completing, std::memory_order_acq_rel, + std::memory_order_acquire)) { completion_busy.store(false, std::memory_order_release); arm_completion(object); return; @@ -313,8 +632,7 @@ void Render_Scene_3D::Private::consume_completion(Object* object) { if (frame->taskflow_trace_requested()) aethera::detail::run_taskflow( completion_graph, *frame, "render_3d.completion", std::move(done)); - else - aethera::detail::run_taskflow(completion_graph, std::move(done)); + else aethera::detail::run_taskflow(completion_graph, std::move(done)); } template void Render_Scene_3D::Private::initialize_backend( @@ -330,48 +648,63 @@ void Render_Scene_3D::Private::initialize_backend( const auto initial = parameters(object); auto prepared_visuals = std::make_shared(); prepared_visuals->reserve(visuals.size()); - for (const auto& visual : visuals) - prepared_visuals->push_back({visual.identity, {}}); - backend = std::make_shared(gpu_index, validation_enabled, - std::move(visuals), initial); + for (const auto& visual : visuals) prepared_visuals->push_back({visual.identity, {}}); + overlaps_gpu = std::ranges::all_of(visuals, [](const auto& visual) { + switch (visual.family) { + case Visual_Family::point: + case Visual_Family::splat: + case Visual_Family::pixel: + case Visual_Family::sphere: + case Visual_Family::primitive: + case Visual_Family::mesh: + case Visual_Family::path: return true; + case Visual_Family::marker: + case Visual_Family::segment: + case Visual_Family::vector: + case Visual_Family::image: + case Visual_Family::labels: + case Visual_Family::glyph: + case Visual_Family::text: + case Visual_Family::volume: return false; + } + return false; + }); + detail::Scene_Datoviz_State::initialize( + gpu_index, validation_enabled, visuals, initial); paint_context = std::make_shared>( detail::Scene_Paint_Context{ - backend, object, initial, - std::move(prepared_visuals)}); + object, initial, std::move(prepared_visuals) + }); } template void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { if (frame_taskflow) return; - if (!runtime->taskflow) - runtime->taskflow = std::make_unique("scene.render"); - + if (!runtime->taskflow) runtime->taskflow = std::make_unique("scene.render"); frame_taskflow = std::make_unique("render_3d.frame"); auto& graph = *frame_taskflow; graph.describe("owner_kind", "scene") .describe("owner_component", "scene"); auto begin = graph.add("scene.begin", [this, object] { - if (!active_context || !active_context->frame) - throw std::logic_error("3D frame DAG lost its active frame"); + if (!active_context || !active_context->frame) throw std::logic_error("3D frame DAG lost its active frame"); const auto frame = active_context->frame; frame->mark(Frame_Trace_Marker::scene_render_started); frame->mark(Frame_Trace_Marker::event_dispatch_started); active_context->events = take_events(object); frame->mark(Frame_Trace_Marker::event_dispatch_finished); - auto context = std::static_pointer_cast< detail::Scene_Paint_Context>(paint_context); context->parameters = parameters(object); if (!detail::prepared_visual_batch_complete(*context->visuals)) { double_buffer::detail::Internal_Access:: - current_dependency_graph(object).for_each_bound( - [](Renderable* renderable, Renderable::Private&) { - double_buffer::detail::Internal_Access:: - mark_dirty(renderable); - }); + current_dependency_graph(object).for_each_bound( + [](Renderable* renderable, Renderable::Private&) { + double_buffer::detail::Internal_Access:: + mark_dirty(renderable); + }); } if (context->visuals.use_count() != 1) context->visuals = - std::make_shared(*context->visuals); + std::make_shared(*context->visuals); camera_component->advance_object(); axes_component->advance_object(); frame->mark(Frame_Trace_Marker::prepare_started); @@ -379,16 +712,13 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { frame->mark(Frame_Trace_Marker::paint_started); }); begin.describe("dimension", "3D").describe("stage", "frame setup"); - auto renderables = graph.compose("scene.renderables", *runtime->taskflow); renderables.describe("dimension", "3D") .describe("stage", "visual graph") .describe("execution_domain", "Taskflow workers"); - auto submit = graph.add("scene.backend.submit", [this, object] { const auto execution = active_context; - if (!execution || !execution->frame) - throw std::logic_error("3D submit lost its frame context"); + if (!execution || !execution->frame) throw std::logic_error("3D submit lost its frame context"); auto context = std::static_pointer_cast< detail::Scene_Paint_Context>(paint_context); if (!detail::prepared_visual_batch_complete(*context->visuals)) @@ -397,40 +727,8 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { auto& state = static_cast( double_buffer::detail::Internal_Access::pending_state(object)); state.event_statistics = event_statistics.state(); - context->backend->render( - context->visuals, context->parameters, - execution->frame, std::move(execution->events), - [this, object](not_null submitted, bool overlaps_gpu) { - const auto frame_context = context_for(submitted); - if (!frame_context) - throw std::logic_error( - "3D backend submitted a frame not borrowed by Scene"); - frame_context->overlaps_gpu = overlaps_gpu; - frame_context->gpu_submitted.store( - true, std::memory_order_release); - try_release_prepare(object, frame_context); - }, - [this, object](not_null completed, - std::exception_ptr failure) { - const auto frame_context = context_for(completed); - if (!frame_context) - throw std::logic_error( - "3D backend completed a frame not borrowed by Scene"); - frame_context->failure = std::move(failure); - if (frame_context->failure && - !frame_context->gpu_submitted.load( - std::memory_order_acquire)) { - frame_context->overlaps_gpu = false; - frame_context->gpu_submitted.store( - true, std::memory_order_release); - } - frame_context->gpu_completed.store( - true, std::memory_order_release); - try_release_prepare(object, frame_context); - if (frame_context->phase.load(std::memory_order_acquire) == - Frame_Phase::submitted) - arm_completion(object); - }); + render_datoviz(object, execution, context->visuals, + context->parameters); }); submit.describe("dimension", "3D") .describe("backend", "Datoviz") @@ -448,15 +746,15 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render( "Render_Scene_3D requires a frame callback before render"); bool inactive{}; if (!prepare_active.compare_exchange_strong( - inactive, true, std::memory_order_acq_rel, - std::memory_order_acquire)) + inactive, true, std::memory_order_acq_rel, + std::memory_order_acquire)) return Render_Result::frame_pipeline_busy; Frame_Context* execution{}; for (auto& context : frame_contexts) { auto expected = Frame_Phase::available; if (context.phase.compare_exchange_strong( - expected, Frame_Phase::preparing, - std::memory_order_acq_rel, std::memory_order_acquire)) { + expected, Frame_Phase::preparing, + std::memory_order_acq_rel, std::memory_order_acquire)) { execution = &context; break; } @@ -468,6 +766,7 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render( execution->frame = frame; execution->events.clear(); execution->failure = {}; + execution->datoviz_frame.reset(); execution->trace_started = false; execution->overlaps_gpu = false; execution->cpu_finished.store(false, std::memory_order_relaxed); @@ -494,7 +793,7 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render( double_buffer::detail::Internal_Access::current_prop(object)); if (!prop.view_active) return reject_frame(Render_Result::view_inactive); if (prop.viewport.empty()) return reject_frame(Render_Result::empty_viewport); - if (!backend || !backend->available()) + if (!backend_available.load(std::memory_order_acquire)) return reject_frame(Render_Result::backend_unavailable); ensure_frame_taskflow(object); } @@ -504,14 +803,14 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render( } frame->mark(Frame_Trace_Marker::scene_render_requested); execution->trace_started = - aethera::detail::begin_taskflow_trace(*frame); + aethera::detail::begin_taskflow_trace(*frame); try { auto completion = [this, object, execution] { double_buffer::detail::Internal_Access:: - current_dependency_graph(object).for_each_bound( - [](Renderable* renderable, Renderable::Private& data) { - data.publish_renderable_state(renderable); - }); + current_dependency_graph(object).for_each_bound( + [](Renderable* renderable, Renderable::Private& data) { + data.publish_renderable_state(renderable); + }); double_buffer::detail::Internal_Access::publish_state(object); active_context = nullptr; execution->cpu_finished.store(true, std::memory_order_release); @@ -520,8 +819,7 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render( if (frame->taskflow_trace_requested()) aethera::detail::run_taskflow( *frame_taskflow, *frame, "render_3d.frame", std::move(completion)); - else - aethera::detail::run_taskflow(*frame_taskflow, std::move(completion)); + else aethera::detail::run_taskflow(*frame_taskflow, std::move(completion)); } catch (...) { if (execution->trace_started) { @@ -536,17 +834,18 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render( template void Render_Scene_3D::Private::reset_diagnostics(Object* object) { double_buffer::detail::Internal_Access::publish_state(object, - [this](State_Access states) { - event_statistics.reset(); - auto& state = states.template get(); - state.event_statistics = {}; - }); + [this](State_Access states) { + event_statistics.reset(); + auto& state = states.template get(); + state.event_statistics = {}; + }); } template void Render_Scene_3D::Private::set_frame_callback(Object*, Frame_Callback callback) { frame_callback.store( - callback ? std::make_shared(std::move(callback)) - : std::shared_ptr{}, + callback + ? std::make_shared(std::move(callback)) + : std::shared_ptr{}, std::memory_order_release); } template @@ -555,7 +854,7 @@ void Render_Scene_3D::Private::set_submitted_frame_callback( submitted_callback.store( callback ? std::make_shared( - std::move(callback)) + std::move(callback)) : std::shared_ptr{}, std::memory_order_release); } diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/scene/Render_Scene_3D_Datoviz.cpp similarity index 83% rename from render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp rename to render_3D/render_3D/scene/Render_Scene_3D_Datoviz.cpp index 867e852..664c13a 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/scene/Render_Scene_3D_Datoviz.cpp @@ -1,8 +1,15 @@ -#include "Datoviz_Visual_Backend.hpp" -#include "Exception.hpp" +#include "Scene_Datoviz_State.hpp" +#include "../detail/Exception.hpp" +#include #include +#include +#include #include +#include +#include +#include #include +#include #include #include #include @@ -11,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -171,7 +177,6 @@ DvzFont* configure_glyph_text(DvzScene* scene, DvzVisual* visual, const char* te throw std::runtime_error("failed to upload Datoviz text geometry"); return font; } - std::vector utf8_codepoints(std::string_view text) { std::vector result; result.reserve(text.size()); @@ -198,13 +203,11 @@ std::vector utf8_codepoints(std::string_view text) { else { throw std::invalid_argument("3D text contains invalid UTF-8"); } - if (index + length > text.size()) - throw std::invalid_argument("3D text contains truncated UTF-8"); + if (index + length > text.size()) throw std::invalid_argument("3D text contains truncated UTF-8"); for (std::size_t offset = 1; offset < length; ++offset) { const auto continuation = - static_cast(text[index + offset]); - if ((continuation & 0xC0U) != 0x80U) - throw std::invalid_argument("3D text contains invalid UTF-8"); + static_cast(text[index + offset]); + if ((continuation & 0xC0U) != 0x80U) throw std::invalid_argument("3D text contains invalid UTF-8"); codepoint = (codepoint << 6U) | (continuation & 0x3FU); } if ((length == 2 && codepoint < 0x80U) || @@ -218,20 +221,17 @@ std::vector utf8_codepoints(std::string_view text) { } return result; } - void upload_text(DvzVisual* visual, DvzFont* font, const Text_Prepared_Data& data) { if (font == nullptr) throw std::logic_error("Datoviz text visual has no font"); auto atlas_specification = - dvz_text_atlas_spec(DVZ_TEXT_RENDERER_MSDF_ATLAS, 64.0F); + dvz_text_atlas_spec(DVZ_TEXT_RENDERER_MSDF_ATLAS, 64.0F); for (const auto& text : data.strings) if (!dvz_font_atlas_ensure_string( - font, &atlas_specification, text.c_str())) + font, &atlas_specification, text.c_str())) throw std::runtime_error("failed to grow Datoviz text atlas"); const auto* atlas = dvz_font_atlas(font, &atlas_specification); - if (atlas == nullptr || dvz_glyph_set_atlas(visual, atlas) != DVZ_OK) - throw std::runtime_error("failed to bind Datoviz text atlas"); - + if (atlas == nullptr || dvz_glyph_set_atlas(visual, atlas) != DVZ_OK) throw std::runtime_error("failed to bind Datoviz text atlas"); std::vector> positions; std::vector> bounds; std::vector> texture_coordinates; @@ -255,9 +255,11 @@ void upload_text(DvzVisual* visual, DvzFont* font, const float y0 = glyph->yoff * scale; const std::array glyph_bounds{ x0, y0, x0 + glyph->width * scale, - y0 + glyph->height * scale}; + y0 + glyph->height * scale + }; const std::array glyph_texture{ - glyph->uv[0], glyph->uv[1], glyph->uv[2], glyph->uv[3]}; + glyph->uv[0], glyph->uv[1], glyph->uv[2], glyph->uv[3] + }; for (std::uint32_t vertex = 0; vertex < 6; ++vertex) { positions.push_back(data.positions[item_index]); bounds.push_back(glyph_bounds); @@ -269,15 +271,18 @@ void upload_text(DvzVisual* visual, DvzFont* font, } } const auto count = static_cast(positions.size()); - const std::array updates{{ - {"position", positions.data(), count}, - {"bounds", bounds.data(), count}, - {"texcoords", texture_coordinates.data(), count}, - {"color", colors.data(), count}, - {"angle", angles.data(), count}}}; + const std::array updates{ + { + {"position", positions.data(), count}, + {"bounds", bounds.data(), count}, + {"texcoords", texture_coordinates.data(), count}, + {"color", colors.data(), count}, + {"angle", angles.data(), count} + } + }; if (dvz_visual_set_data_many( - visual, updates.data(), - static_cast(updates.size())) != DVZ_OK) + visual, updates.data(), + static_cast(updates.size())) != DVZ_OK) throw std::runtime_error("failed to upload Datoviz text payload"); } bool supports_item_interaction(Visual_Family family) noexcept { @@ -368,23 +373,13 @@ std::string artifact_command_excerpt(const std::string& json, end == std::string::npos ? 2048 : end - first_position); } } // namespace -struct Datoviz_Render_Context final { +struct Datoviz_Render_Context final : + std::enable_shared_from_this { public: [[nodiscard]] static std::shared_ptr acquire( std::uint32_t gpu_index, bool validation_enabled) { - using Key = std::pair; - static std::mutex registry_mutex; - static std::map> registry; - std::lock_guard lock(registry_mutex); - const Key key{gpu_index, validation_enabled}; - if (const auto found = registry.find(key); found != registry.end()) { - if (auto context = found->second.lock()) return context; - registry.erase(found); - } - auto context = std::shared_ptr( + return std::shared_ptr( new Datoviz_Render_Context(gpu_index, validation_enabled)); - registry.emplace(key, context); - return context; } ~Datoviz_Render_Context() { if (gpu_context_ != nullptr) dvz_gpu_ctx_destroy(gpu_context_); @@ -392,11 +387,11 @@ public: [[nodiscard]] not_null gpu_context() const noexcept { return not_null{gpu_context_}; } - [[nodiscard]] std::mutex& api_mutex() noexcept { - return api_mutex_; - } - [[nodiscard]] std::mutex& queue_mutex() noexcept { - return queue_mutex_; + void enqueue_submission(std::function command) { + if (!command) throw std::invalid_argument("empty Datoviz queue command"); + if (!submissions_.enqueue(std::move(command))) throw std::bad_alloc{}; + submission_generation_.fetch_add(1, std::memory_order_release); + arm_submission_drain(); } private: Datoviz_Render_Context(std::uint32_t gpu_index, bool validation_enabled) { @@ -405,48 +400,53 @@ private: dvz_gpu_ctx_config_gpu(&configuration, gpu_index); dvz_gpu_ctx_config_enable_canvas_extensions(&configuration, false); gpu_context_ = dvz_gpu_ctx(&configuration); - if (gpu_context_ == nullptr) throw std::runtime_error("failed to create shared Datoviz GPU context"); + if (gpu_context_ == nullptr) + throw std::runtime_error("failed to create Scene Datoviz GPU context"); } - owner gpu_context_{}; /* 共享 GPU Device 与分配器的唯一所有权。 */ - std::mutex api_mutex_{}; /* 同 GPU Datoviz 对象结构修改的串行化域。 */ - std::mutex queue_mutex_{}; /* VkQueue 及会内部使用主队列的 Datoviz 调用的外部同步域。 */ + void arm_submission_drain() { + if (submission_drain_active_.exchange(true, std::memory_order_acq_rel)) return; + auto lifetime = shared_from_this(); + aethera::schedule_task("datoviz.queue.submit", [lifetime = std::move(lifetime)] { + lifetime->drain_submissions(); + }); + } + void drain_submissions() noexcept { + const auto observed = submission_generation_.load(std::memory_order_acquire); + std::function command; + while (submissions_.try_dequeue(command)) { + try { command(); } + catch (...) {} + command = {}; + } + submission_drain_active_.store(false, std::memory_order_release); + if (submission_generation_.load(std::memory_order_acquire) != observed) + arm_submission_drain(); + } + owner gpu_context_{}; /* 当前 Scene 独占 GPU Device 与分配器。 */ + moodycamel::ConcurrentQueue> submissions_{}; + std::atomic_uint64_t submission_generation_{}; + std::atomic_bool submission_drain_active_{}; }; void retain_quarantined_context( std::shared_ptr& context) noexcept { if (!context) return; - struct Registry { - std::atomic_flag lock = ATOMIC_FLAG_INIT; - std::array, 64> contexts{}; - }; - static Registry registry; - while (registry.lock.test_and_set(std::memory_order_acquire)) std::this_thread::yield(); - for (auto& retained : registry.contexts) { - if (retained.get() == context.get()) { - context.reset(); - registry.lock.clear(std::memory_order_release); - return; - } - } - for (auto& retained : registry.contexts) { - if (!retained) { - retained = std::move(context); - registry.lock.clear(std::memory_order_release); - return; - } - } - registry.lock.clear(std::memory_order_release); - static_cast(new(std::nothrow) - std::shared_ptr(std::move(context))); + static moodycamel::ConcurrentQueue< + std::shared_ptr> quarantined; + if (quarantined.enqueue(context)) + context.reset(); + else + static_cast(new(std::nothrow) + std::shared_ptr(std::move(context))); } -struct Datoviz_Visual_Backend::Private::Frame_Target final { +struct Scene_Datoviz_State::Frame_Target final { public: struct Collection { std::vector pixels; std::optional gpu_timing; }; - Frame_Target(not_null gpu_context, Extent extent, - std::uint64_t generation) - : gpu_context_(gpu_context), extent_(extent), generation_(generation) { + Frame_Target(not_null gpu_context, VkCommandPool command_pool, + Extent extent, std::uint64_t generation) : + gpu_context_(gpu_context), extent_(extent), generation_(generation) { if (extent.empty()) throw std::invalid_argument("invalid Datoviz point frame target"); const std::uint64_t byte_size = static_cast(extent.width) * extent.height * 4ULL; @@ -477,7 +477,7 @@ public: if (dvz_image_views_create(view_) != 0) throw std::runtime_error("failed to create Datoviz point image view"); commands_ = allocate_wrapper(dvz_commands_create_wrapper, "failed to allocate Datoviz commands"); - dvz_commands(device, queue, 1, commands_); + dvz_commands_pool(device, queue, command_pool, 1, commands_); if (dvz_commands_handle(commands_) == VK_NULL_HANDLE) throw std::runtime_error("failed to create Datoviz command buffer"); fence_ = allocate_wrapper(dvz_fence_create_wrapper, "failed to allocate Datoviz fence"); @@ -508,14 +508,34 @@ public: release_without_destroy(); } } - [[nodiscard]] bool can_reuse( + [[nodiscard]] bool can_reuse_commands( std::uint64_t command_revision, bool readback) const noexcept { return available() && recorded_ && recorded_command_revision_ == command_revision && recorded_readback_ == readback; } - void reuse(std::uint64_t command_revision, bool observe, bool readback) { - if (!can_reuse(command_revision, readback)) + [[nodiscard]] bool can_reuse( + std::uint64_t command_revision, std::uint64_t controller_revision, + bool readback) const noexcept { + return can_reuse_commands(command_revision, readback) && + recorded_controller_revision_ == controller_revision; + } + void mark_mvp_uploaded(std::uint64_t controller_revision) { + if (!available() || !recorded_) + throw std::logic_error( + "Datoviz frame target cannot publish an MVP revision"); + recorded_controller_revision_ = controller_revision; + } + void invalidate_recording() { + if (!available()) + throw std::logic_error( + "Datoviz frame target cannot invalidate a busy recording"); + recorded_ = false; + } + void reuse(std::uint64_t command_revision, + std::uint64_t controller_revision, bool observe, + bool readback) { + if (!can_reuse(command_revision, controller_revision, readback)) throw std::logic_error( "Datoviz frame target command recording cannot be reused"); observing_ = observe; @@ -523,11 +543,13 @@ public: dvz_fence_reset(fence_); prepared_ = true; } - void begin(bool observe, bool readback, std::uint64_t command_revision) { + void begin(bool observe, bool readback, std::uint64_t command_revision, + std::uint64_t controller_revision) { if (recording_ || prepared_ || in_flight_) throw std::logic_error("Datoviz frame target is not available"); observing_ = observe; readback_requested_ = readback; recording_command_revision_ = command_revision; + recording_controller_revision_ = controller_revision; /* Reset invalidates the previous recording immediately. Only * finish_recording() may publish the new cache identity. */ recorded_ = false; @@ -678,6 +700,7 @@ public: dvz_submit(submit_); dvz_submit_command(submit_, dvz_commands_handle(commands_)); recorded_command_revision_ = recording_command_revision_; + recorded_controller_revision_ = recording_controller_revision_; recorded_readback_ = readback_requested_; recorded_ = true; prepared_ = true; @@ -911,18 +934,19 @@ private: bool timestamps_supported_{}; std::uint64_t recording_command_revision_{}; std::uint64_t recorded_command_revision_{}; + std::uint64_t recording_controller_revision_{}; + std::uint64_t recorded_controller_revision_{}; bool recorded_{}; bool recorded_readback_{}; bool quarantined_{}; }; -struct Datoviz_Visual_Backend::Private::Frame_Targets { +struct Scene_Datoviz_State::Frame_Targets { static constexpr std::size_t count = 3; std::array, count> values{}; /* 固定三槽,槽地址在后端生命周期内稳定。 */ }; -Datoviz_Visual_Backend::Private::Private() - : targets_(std::make_unique()) {} - -Datoviz_Visual_Backend::Private::~Private() { +Scene_Datoviz_State::Scene_Datoviz_State() : + targets_(std::make_unique()) {} +Scene_Datoviz_State::~Scene_Datoviz_State() { try { destroy(); } @@ -930,19 +954,55 @@ Datoviz_Visual_Backend::Private::~Private() { abandon_resources(); } } - -void Datoviz_Visual_Backend::Private::initialize( +void Scene_Datoviz_State::initialize( std::uint32_t gpu_index, bool validation_enabled, const std::vector& visuals, const Scene_3D_Parameters& initial_scene) { try { render_context_ = Datoviz_Render_Context::acquire(gpu_index, validation_enabled); - std::scoped_lock context_lock(render_context_->api_mutex(), - render_context_->queue_mutex()); const auto gpu_context = render_context_->gpu_context(); + DvzDevice* device = dvz_gpu_ctx_device(gpu_context.get()); + DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context.get(), DVZ_QUEUE_MAIN); + VkDevice vk_device = dvz_device_handle(device); + VkCommandPoolCreateInfo command_pool_info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, + .queueFamilyIndex = dvz_queue_family(queue)}; + VkCommandPool command_pool{VK_NULL_HANDLE}; + if (vkCreateCommandPool(vk_device, &command_pool_info, nullptr, + &command_pool) != VK_SUCCESS) + throw std::runtime_error("failed to create Scene command pool"); + command_pool_ = reinterpret_cast(command_pool); + std::array pool_sizes{{ + {VK_DESCRIPTOR_TYPE_SAMPLER, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, DVZ_MAX_DESCRIPTOR_SETS}, + {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, DVZ_MAX_DESCRIPTOR_SETS}}}; + VkDescriptorPoolCreateInfo descriptor_pool_info{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, + .maxSets = DVZ_MAX_DESCRIPTOR_SETS * + static_cast(pool_sizes.size()), + .poolSizeCount = static_cast(pool_sizes.size()), + .pPoolSizes = pool_sizes.data()}; + VkDescriptorPool descriptor_pool{VK_NULL_HANDLE}; + if (vkCreateDescriptorPool(vk_device, &descriptor_pool_info, nullptr, + &descriptor_pool) != VK_SUCCESS) + throw std::runtime_error("failed to create Scene descriptor pool"); + descriptor_pool_ = reinterpret_cast(descriptor_pool); DvzDrp2RuntimeConfig runtime_configuration = dvz_drp2_runtime_vklite_config( - dvz_gpu_ctx_device(gpu_context.get()), + device, dvz_gpu_ctx_alloc(gpu_context.get())); + dvz_drp2_runtime_vklite_pools( + &runtime_configuration, + command_pool_, descriptor_pool_); for (auto& slot : runtime_slots_) { slot.runtime = dvz_drp2_runtime_vklite(&runtime_configuration); slot.emitter = dvz_frame_plan_emitter(); @@ -959,8 +1019,7 @@ void Datoviz_Visual_Backend::Private::initialize( raise_context("creating Datoviz backend", failure); } } - -void Datoviz_Visual_Backend::Private::create_scene( +void Scene_Datoviz_State::create_scene( const std::vector& registrations, const Scene_3D_Parameters& initial_scene) { if (registrations.empty()) throw std::invalid_argument("Datoviz backend requires at least one Visual registration"); @@ -970,11 +1029,9 @@ void Datoviz_Visual_Backend::Private::create_scene( if (dvz_scene_set_capabilities(scene_, &capabilities) != DVZ_OK) throw std::runtime_error("failed to configure Datoviz scene capabilities"); auto* figure = dvz_figure(scene_, initial_scene.viewport.width, initial_scene.viewport.height, 0); - if (figure == nullptr) - throw std::runtime_error("failed to create Datoviz figure"); + if (figure == nullptr) throw std::runtime_error("failed to create Datoviz figure"); auto* panel = dvz_panel_full(figure); - if (panel == nullptr) - throw std::runtime_error("failed to create Datoviz panel"); + if (panel == nullptr) throw std::runtime_error("failed to create Datoviz panel"); figure_ = not_null{figure}; panel_ = not_null{panel}; bool wants_item_interaction{}; @@ -1024,10 +1081,8 @@ void Datoviz_Visual_Backend::Private::create_scene( if (visual != nullptr && dvz_visual_set_alpha_mode(visual, DVZ_ALPHA_BLENDED) != DVZ_OK) throw std::runtime_error("failed to configure Datoviz visual alpha mode"); - if (visual != nullptr && family == Visual_Family::glyph) - font = configure_glyph_text(scene_, visual, "GLYPH"); - if (visual != nullptr && family == Visual_Family::text) - font = configure_glyph_text(scene_, visual, "TEXT"); + if (visual != nullptr && family == Visual_Family::glyph) font = configure_glyph_text(scene_, visual, "GLYPH"); + if (visual != nullptr && family == Visual_Family::text) font = configure_glyph_text(scene_, visual, "TEXT"); if (visual != nullptr && family == Visual_Family::image) { constexpr std::uint32_t width = 32; constexpr std::uint32_t height = 32; @@ -1106,7 +1161,7 @@ void Datoviz_Visual_Backend::Private::create_scene( const float dz = static_cast(z) - 7.5F; const float distance = std::sqrt(dx * dx + dy * dy + dz * dz); voxels[(z * side + y) * side + x] = - distance < 6.5F ? 1.0F - distance / 6.5F : 0.0F; + distance < 6.5F ? 1.0F - distance / 6.5F : 0.0F; } } } @@ -1159,19 +1214,20 @@ void Datoviz_Visual_Backend::Private::create_scene( DvzFont* font_borrow{}; if (font != nullptr) font_borrow = not_null{font}; DvzText* coordinate_text_borrow{}; - if (coordinate_text != nullptr) - coordinate_text_borrow = not_null{coordinate_text}; + if (coordinate_text != nullptr) coordinate_text_borrow = not_null{coordinate_text}; visuals_.push_back( - {registration.identity, family, not_null{visual}, field_borrow, - font_borrow, coordinate_text_borrow, - family == Visual_Family::volume - ? std::array{16, 16, 16} - : family == Visual_Family::labels - ? std::array{8, 8, 1} - : family == Visual_Family::image - ? std::array{32, 32, 1} - : std::array{}, - 0}); + { + registration.identity, family, not_null{visual}, field_borrow, + font_borrow, coordinate_text_borrow, + family == Visual_Family::volume + ? std::array{16, 16, 16} + : family == Visual_Family::labels + ? std::array{8, 8, 1} + : family == Visual_Family::image + ? std::array{32, 32, 1} + : std::array{}, + 0 + }); } if (wants_item_interaction) { item_interaction_ = dvz_item_interaction(panel, nullptr); @@ -1212,7 +1268,7 @@ void Datoviz_Visual_Backend::Private::create_scene( }; dvz_input_emit_resize(input_router_, &resize); } -void Datoviz_Visual_Backend::Private::apply_axes( +void Scene_Datoviz_State::apply_axes( const Scene_3D_Parameters& scene) { const std::array descriptors{scene.x_axis, scene.y_axis, scene.z_axis}; if (applied_axes_ && *applied_axes_ == descriptors) return; @@ -1323,7 +1379,7 @@ void Datoviz_Visual_Backend::Private::apply_axes( throw std::runtime_error("failed to upload Datoviz 3D axes labels"); applied_axes_ = descriptors; } -void Datoviz_Visual_Backend::Private::apply_camera( +void Scene_Datoviz_State::apply_camera( const Camera_Descriptor& source) { if (applied_camera_ && *applied_camera_ == source) return; if (camera_controller_ != nullptr) { @@ -1421,10 +1477,10 @@ void Datoviz_Visual_Backend::Private::apply_camera( throw std::runtime_error("failed to bind Datoviz camera controller"); applied_camera_ = source; } -bool Datoviz_Visual_Backend::Private::matches_command_structure( +bool Scene_Datoviz_State::matches_command_structure( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared) const { - if (input_changed_ || figure_extent_ != scene.viewport || + if (figure_extent_ != scene.viewport || !applied_camera_ || *applied_camera_ != scene.camera || !applied_axes_ || *applied_axes_ != std::array{scene.x_axis, scene.y_axis, scene.z_axis} || @@ -1436,16 +1492,61 @@ bool Datoviz_Visual_Backend::Private::matches_command_structure( return value.identity == source.identity; }); if (target == visuals_.end()) return false; - if (uses_external_attributes(target->family)) { - if (!target->applied || - !same_visual_structure(*target->applied, source.visual)) - return false; - } - else if (target->applied_revision != source.visual.revision) return false; + if (!target->applied || + !same_visual_structure(*target->applied, source.visual)) + return false; } return true; } -std::uint64_t Datoviz_Visual_Backend::Private::apply( +bool Scene_Datoviz_State::target_runtime_resources_current( + const Prepared_Visual_Batch& prepared, + std::uint8_t target_index) const { + if (target_index >= runtime_slots_.size()) throw std::out_of_range("Datoviz runtime target index is invalid"); + for (const auto& source : prepared) { + const auto target = std::ranges::find_if( + visuals_, [&](const Visual_Instance& value) { + return value.identity == source.identity; + }); + if (target == visuals_.end()) + throw std::logic_error( + "Datoviz runtime update contains an unknown Visual identity"); + if (!uses_external_attributes(target->family) && + target->uploaded_data_revisions[target_index] != + source.visual.data_revision) + return false; + } + return true; +} +std::vector Scene_Datoviz_State::stale_runtime_visuals( + const Prepared_Visual_Batch& prepared, + std::uint8_t target_index) const { + if (target_index >= runtime_slots_.size()) throw std::out_of_range("Datoviz runtime target index is invalid"); + std::vector result; + result.reserve(prepared.size()); + for (const auto& source : prepared) { + const auto target = std::ranges::find_if( + visuals_, [&](const Visual_Instance& value) { + return value.identity == source.identity; + }); + if (target == visuals_.end()) + throw std::logic_error( + "Datoviz runtime update contains an unknown Visual identity"); + if (!uses_external_attributes(target->family) && + target->uploaded_data_revisions[target_index] != + source.visual.data_revision) + result.push_back(target->visual); + } + return result; +} +void Scene_Datoviz_State::mark_target_runtime_resources_uploaded( + std::uint8_t target_index) { + if (target_index >= runtime_slots_.size()) throw std::out_of_range("Datoviz runtime target index is invalid"); + for (auto& visual : visuals_) + if (!uses_external_attributes(visual.family) && visual.applied) + visual.uploaded_data_revisions[target_index] = + visual.applied->data_revision; +} +std::uint64_t Scene_Datoviz_State::apply( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared, std::uint8_t target_index, bool bind_target) { if (figure_extent_ != scene.viewport) { @@ -1473,7 +1574,7 @@ std::uint64_t Datoviz_Visual_Backend::Private::apply( } return uploaded_bytes; } -void Datoviz_Visual_Backend::Private::ensure_external_attributes( +void Scene_Datoviz_State::ensure_external_attributes( Visual_Instance& target, const Prepared_Visual& visual) { const auto item_count = prepared_item_count(visual); if (item_count == 0) return; @@ -1588,7 +1689,7 @@ void Datoviz_Visual_Backend::Private::ensure_external_attributes( target.attributes = std::move(attributes); target.uploaded_data_revisions = {}; } -std::uint64_t Datoviz_Visual_Backend::Private::upload_external_attributes( +std::uint64_t Scene_Datoviz_State::upload_external_attributes( Visual_Instance& target, const Prepared_Visual& visual, std::uint8_t target_index) { const auto item_count = prepared_item_count(visual); @@ -1682,7 +1783,7 @@ std::uint64_t Datoviz_Visual_Backend::Private::upload_external_attributes( } return uploaded_bytes; } -void Datoviz_Visual_Backend::Private::bind_external_attributes( +void Scene_Datoviz_State::bind_external_attributes( Visual_Instance& target, std::uint8_t target_index, std::uint32_t item_count) { for (auto& attribute : target.attributes) { @@ -1694,7 +1795,7 @@ void Datoviz_Visual_Backend::Private::bind_external_attributes( throw std::runtime_error("failed to bind Datoviz external visual attribute"); } } -void Datoviz_Visual_Backend::Private::register_external_attributes( +void Scene_Datoviz_State::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"); @@ -1724,7 +1825,7 @@ void Datoviz_Visual_Backend::Private::register_external_attributes( } } } -std::uint64_t Datoviz_Visual_Backend::Private::apply_visual( +std::uint64_t Scene_Datoviz_State::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"); @@ -1820,7 +1921,7 @@ std::uint64_t Datoviz_Visual_Backend::Private::apply_visual( uploaded_bytes = upload_external_attributes( target, point, target_index); target.uploaded_data_revisions[target_index] = - point.data_revision; + point.data_revision; } if (structure_changed && dvz_visual_set_visible(visual, point.visible) != DVZ_OK) @@ -1965,18 +2066,17 @@ std::uint64_t Datoviz_Visual_Backend::Private::apply_visual( } case Visual_Family::image: { const auto& data = prepared_data(point); - if (!target.field) - throw std::logic_error("Datoviz image has no sampled field"); + if (!target.field) throw std::logic_error("Datoviz image has no sampled field"); auto view = dvz_field_data_view(); view.data = data.field_pixels.data(); view.bytes_per_row = static_cast(data.field_width) * - sizeof(data.field_pixels.front()); + sizeof(data.field_pixels.front()); view.rows_per_image = data.field_height; const std::array extent{data.field_width, data.field_height, 1U}; result = extent == target.field_extent - ? dvz_sampled_field_set_data(target.field, &view) - : dvz_sampled_field_resize( - target.field, extent[0], extent[1], extent[2], &view); + ? dvz_sampled_field_set_data(target.field, &view) + : dvz_sampled_field_resize( + target.field, extent[0], extent[1], extent[2], &view); if (result != DVZ_OK) break; target.field_extent = extent; const std::array updates{ @@ -1991,18 +2091,17 @@ std::uint64_t Datoviz_Visual_Backend::Private::apply_visual( } case Visual_Family::labels: { const auto& data = prepared_data(point); - if (!target.field) - throw std::logic_error("Datoviz labels visual has no sampled field"); + if (!target.field) throw std::logic_error("Datoviz labels visual has no sampled field"); auto view = dvz_field_data_view(); view.data = data.field_labels.data(); view.bytes_per_row = static_cast(data.field_width) * - sizeof(data.field_labels.front()); + sizeof(data.field_labels.front()); view.rows_per_image = data.field_height; const std::array extent{data.field_width, data.field_height, 1U}; result = extent == target.field_extent - ? dvz_sampled_field_set_data(target.field, &view) - : dvz_sampled_field_resize( - target.field, extent[0], extent[1], extent[2], &view); + ? dvz_sampled_field_set_data(target.field, &view) + : dvz_sampled_field_resize( + target.field, extent[0], extent[1], extent[2], &view); if (result != DVZ_OK) break; target.field_extent = extent; const std::array updates{ @@ -2017,20 +2116,21 @@ std::uint64_t Datoviz_Visual_Backend::Private::apply_visual( } case Visual_Family::glyph: { const auto& data = prepared_data(point); - const std::array updates{{ - {"position", data.positions.data(), count}, - {"bounds", data.bounds.data(), count}, - {"texcoords", data.texture_coordinates.data(), count}, - {"color", data.colors.data(), count}, - {"angle", data.angles.data(), count} - }}; + const std::array updates{ + { + {"position", data.positions.data(), count}, + {"bounds", data.bounds.data(), count}, + {"texcoords", data.texture_coordinates.data(), count}, + {"color", data.colors.data(), count}, + {"angle", data.angles.data(), count} + } + }; result = dvz_visual_set_data_many(visual, updates.data(), static_cast(updates.size())); break; } case Visual_Family::text: { - if (!target.font) - throw std::logic_error("Datoviz text visual has no font"); + if (!target.font) throw std::logic_error("Datoviz text visual has no font"); upload_text(visual, target.font, prepared_data(point)); break; @@ -2042,23 +2142,23 @@ std::uint64_t Datoviz_Visual_Backend::Private::apply_visual( throw std::invalid_argument( "3D volume field dimensions must be non-zero"); const auto expected = static_cast(data.field_width) * - data.field_height * data.field_depth; + data.field_height * data.field_depth; if (expected != data.values.size()) throw std::invalid_argument( "3D volume voxel count does not match field dimensions"); - if (!target.field) - throw std::logic_error("Datoviz volume has no sampled field"); + if (!target.field) throw std::logic_error("Datoviz volume has no sampled field"); auto view = dvz_field_data_view(); view.data = data.values.data(); view.bytes_per_row = static_cast(data.field_width) * - sizeof(data.values.front()); + sizeof(data.values.front()); view.rows_per_image = data.field_height; const std::array extent{ - data.field_width, data.field_height, data.field_depth}; + data.field_width, data.field_height, data.field_depth + }; result = extent == target.field_extent - ? dvz_sampled_field_set_data(target.field, &view) - : dvz_sampled_field_resize( - target.field, extent[0], extent[1], extent[2], &view); + ? dvz_sampled_field_set_data(target.field, &view) + : dvz_sampled_field_resize( + target.field, extent[0], extent[1], extent[2], &view); if (result == DVZ_OK) target.field_extent = extent; break; } @@ -2070,13 +2170,16 @@ std::uint64_t Datoviz_Visual_Backend::Private::apply_visual( target.applied = point; return 0; } -void Datoviz_Visual_Backend::Private::dispatch_pointer( +void Scene_Datoviz_State::mark_controller_input_applied() noexcept { + input_changed_ = true; + ++controller_revision_; + if (controller_revision_ == 0) controller_revision_ = 1; +} +void Scene_Datoviz_State::dispatch_pointer( ::aethera::Event_Type event, float x, float y, ::aethera::Mouse_Button mouse_button, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport, std::uint64_t occurred_at_ns) { - std::lock_guard api_lock(render_context_->api_mutex()); - input_changed_ = true; if (applied_camera_ && applied_camera_->controller == Camera_Controller::turntable) { if (mouse_button == ::aethera::Mouse_Button::left && !applied_camera_->turntable_control.rotate_enabled) @@ -2098,13 +2201,12 @@ void Datoviz_Visual_Backend::Private::dispatch_pointer( button(mouse_button), modifiers(keyboard_modifiers), 1.0F, occurred_at_ns, nullptr); + mark_controller_input_applied(); } -void Datoviz_Visual_Backend::Private::dispatch_wheel( +void Scene_Datoviz_State::dispatch_wheel( float x, float y, float delta_x, float delta_y, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport, std::uint64_t occurred_at_ns) { - std::lock_guard api_lock(render_context_->api_mutex()); - input_changed_ = true; if (applied_camera_ && applied_camera_->controller == Camera_Controller::turntable && !applied_camera_->turntable_control.zoom_enabled) @@ -2113,11 +2215,10 @@ void Datoviz_Visual_Backend::Private::dispatch_wheel( input_router_, x, y, static_cast(viewport.width), static_cast(viewport.height), delta_x, delta_y, modifiers(keyboard_modifiers), 1.0F, occurred_at_ns, nullptr); + mark_controller_input_applied(); } -void Datoviz_Visual_Backend::Private::dispatch_key( +void Scene_Datoviz_State::dispatch_key( const ::aethera::Key_Event& event) { - 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_)) { @@ -2132,6 +2233,7 @@ void Datoviz_Visual_Backend::Private::dispatch_key( default: break; } if (reset != DVZ_OK) throw std::runtime_error("failed to reset Datoviz camera controller"); + mark_controller_input_applied(); return; } const DvzKeyboardEventType type = @@ -2143,8 +2245,9 @@ void Datoviz_Visual_Backend::Private::dispatch_key( dvz_keyboard_emit(input_router_, type, key_code(event.key, event.native_key), modifiers(event.modifiers), nullptr); + mark_controller_input_applied(); } -DvzSceneFrameArtifact* Datoviz_Visual_Backend::Private::emit( +DvzSceneFrameArtifact* Scene_Datoviz_State::emit( const Scene_3D_Parameters& scene, std::uint8_t target_index) { if (target_index >= runtime_slots_.size() || runtime_slots_[target_index].emitter == nullptr) @@ -2181,8 +2284,7 @@ DvzSceneFrameArtifact* Datoviz_Visual_Backend::Private::emit( } return artifact; } -std::optional -Datoviz_Visual_Backend::Private::acquire_target(Extent extent) { +std::optional Scene_Datoviz_State::acquire_target(Extent extent) { for (std::uint8_t index = 0; index < targets_->values.size(); ++index) { auto& candidate = targets_->values[index]; if (candidate && candidate->available() && candidate->extent() == extent) return index; @@ -2191,21 +2293,21 @@ Datoviz_Visual_Backend::Private::acquire_target(Extent extent) { auto& candidate = targets_->values[index]; if (candidate && !candidate->available()) continue; candidate = std::make_unique( - render_context_->gpu_context(), extent, ++target_generation_); + render_context_->gpu_context(), + reinterpret_cast(command_pool_), extent, + ++target_generation_); return index; } return std::nullopt; } -Datoviz_Visual_Backend::Private::Frame_Target& -Datoviz_Visual_Backend::Private::target( +Scene_Datoviz_State::Frame_Target& Scene_Datoviz_State::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::Private::reuse_recorded_target( +std::optional Scene_Datoviz_State::reuse_recorded_target( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared, std::uint64_t frame_sequence, bool observe, bool readback) { Datoviz_Frame_Observation observation; @@ -2220,8 +2322,10 @@ Datoviz_Visual_Backend::Private::reuse_recorded_target( target_index < targets_->values.size(); ++target_index) { auto& candidate = targets_->values[target_index]; if (!candidate || candidate->extent() != scene.viewport || - !candidate->can_reuse(command_revision_, readback)) + !candidate->can_reuse( + command_revision_, controller_revision_, readback)) continue; + if (!target_runtime_resources_current(prepared, target_index)) continue; /* The command buffer already binds this target's independent region. * Only immutable frame payload bytes may change on this path. No * Scene graph, emitter, runtime, allocator, camera or axis object is @@ -2242,35 +2346,35 @@ Datoviz_Visual_Backend::Private::reuse_recorded_target( "recorded Datoviz Visual has no immutable payload"); if (prepared_item_count(source.visual) != 0 && visual->uploaded_data_revisions[target_index] != - source.visual.data_revision) { + source.visual.data_revision) { observation.uploaded_bytes += upload_external_attributes( *visual, source.visual, target_index); visual->uploaded_data_revisions[target_index] = - source.visual.data_revision; + source.visual.data_revision; } visual->applied_revision = source.visual.revision; visual->applied = source.visual; } - candidate->reuse(command_revision_, observe, readback); + candidate->reuse( + command_revision_, controller_revision_, observe, readback); observation.apply_ns = trace_now_ns() - started; return Pending_Frame{ - candidate->device(), candidate->fence(), scene.viewport, + reinterpret_cast(candidate->device()), + reinterpret_cast(candidate->fence()), scene.viewport, frame_sequence, target_index, candidate->generation(), std::move(observation) }; } return std::nullopt; } -std::optional -Datoviz_Visual_Backend::Private::try_prepare_reused( +std::optional Scene_Datoviz_State::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( +std::optional Scene_Datoviz_State::prepare( const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback) { if (scene.viewport.empty()) return std::nullopt; @@ -2282,10 +2386,11 @@ Datoviz_Visual_Backend::Private::prepare( * VkQueue。它必须和 Render Domain 的显式 submit 使用同一外部同步域。 * 热路径的已录制目标复用在此锁之前返回,仍可在各 Scene 准备线程并行。 */ - std::scoped_lock context_lock(render_context_->api_mutex(), - render_context_->queue_mutex()); - std::lock_guard target_lock(target_mutex_); - const auto target_index = acquire_target(scene.viewport); + std::optional target_index; + { + std::lock_guard resource_lock(target_mutex_); + target_index = acquire_target(scene.viewport); + } if (!target_index) return std::nullopt; Datoviz_Frame_Observation observation; observation.render_sequence = frame_sequence; @@ -2298,9 +2403,10 @@ Datoviz_Visual_Backend::Private::prepare( if (content_changed) ++command_revision_; auto& frame_target = *targets_->values[*target_index]; const bool bind_target = - !frame_target.can_reuse(command_revision_, readback); + !frame_target.can_reuse_commands(command_revision_, readback); observation.uploaded_bytes = apply( scene, visuals, *target_index, bind_target); + bool query_changed_scene{}; if (item_interaction_ != nullptr) { static_cast(dvz_figure_process_queries( figure_, runtime_slots_[*target_index].runtime, nullptr)); @@ -2308,6 +2414,7 @@ Datoviz_Visual_Backend::Private::prepare( bool resolved{}; while (dvz_scene_poll_query(scene_, &query)) resolved = true; if (resolved) { + query_changed_scene = true; if (hover_readout_ != nullptr) { dvz_pinned_readout_destroy(hover_readout_); hover_readout_ = nullptr; @@ -2334,31 +2441,109 @@ Datoviz_Visual_Backend::Private::prepare( } } } + if (query_changed_scene) ++command_revision_; 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_, readback)) { - frame_target.reuse(command_revision_, observe, readback); + bool runtime_resources_updated{}; + if (frame_target.can_reuse_commands(command_revision_, readback) && + !target_runtime_resources_current(visuals, *target_index)) { + DvzDiagnosticReport report{}; + dvz_diagnostic_report_init(&report); + std::uint64_t uploaded_bytes{}; + bool command_recording_valid{}; + phase_started = trace_now_ns(); + const auto replay_visuals = stale_runtime_visuals( + visuals, *target_index); + std::unique_ptr + update_stream{ + dvz_figure_prepare_runtime_resources( + figure_, runtime_slots_[*target_index].emitter, + replay_visuals.data(), + static_cast(replay_visuals.size()), + &uploaded_bytes, &command_recording_valid, &report), + &dvz_drp2_stream_destroy + }; + observation.emit_ns += trace_now_ns() - phase_started; + if (!update_stream) { + std::string message = + "failed to prepare Datoviz retained runtime resources"; + if (dvz_diagnostic_report_count(&report) != 0) + if (const char* diagnostic = + dvz_diagnostic_report_get(&report, 0)) + message += ": " + std::string(diagnostic); + throw std::runtime_error(std::move(message)); + } + phase_started = trace_now_ns(); + DvzDrp2ValidationResult update_result{}; + { + update_result = dvz_drp2_runtime_execute( + runtime_slots_[*target_index].runtime, + update_stream.get()); + } + observation.execute_ns += trace_now_ns() - phase_started; + if (!update_result.ok) + throw std::runtime_error( + "failed to execute Datoviz retained runtime resources"); + dvz_figure_commit_runtime_resources(figure_); + observation.uploaded_bytes += uploaded_bytes; + mark_target_runtime_resources_uploaded(*target_index); + runtime_resources_updated = true; + if (!command_recording_valid) frame_target.invalidate_recording(); + } + /* Controller input changes only mapped common MVP buffers. The selected + * target is available, so its own runtime allocation can be updated without + * rebuilding the Figure artifact or re-recording commands. Structural Scene + * changes keep using command_revision_ and fall through to full recording. */ + bool mvp_updated{}; + if (frame_target.can_reuse_commands(command_revision_, readback) && + !frame_target.can_reuse( + command_revision_, controller_revision_, readback)) { + std::uint64_t mvp_uploaded_bytes{}; + if (dvz_figure_update_mvp_buffers( + figure_, runtime_slots_[*target_index].emitter, + runtime_slots_[*target_index].runtime, + &mvp_uploaded_bytes)) { + frame_target.mark_mvp_uploaded(controller_revision_); + observation.uploaded_bytes += mvp_uploaded_bytes; + mvp_updated = true; + } + } + if (frame_target.can_reuse( + command_revision_, controller_revision_, readback)) { + { + std::lock_guard target_lock(target_mutex_); + frame_target.reuse( + command_revision_, controller_revision_, observe, readback); + } input_changed_ = false; - observation.path = Datoviz_Frame_Path::reused; + observation.path = (runtime_resources_updated || mvp_updated) + ? Datoviz_Frame_Path::updated + : Datoviz_Frame_Path::reused; return Pending_Frame{ - frame_target.device(), frame_target.fence(), scene.viewport, + reinterpret_cast(frame_target.device()), + reinterpret_cast(frame_target.fence()), scene.viewport, frame_sequence, *target_index, frame_target.generation(), std::move(observation) }; } - frame_target.begin(observe, readback, command_revision_); + { + std::lock_guard recording_lock(target_mutex_); + frame_target.begin( + observe, readback, command_revision_, controller_revision_); + } struct Recording_Scope { Frame_Target& target; /* 异常退出时回收尚未发布的录制槽。 */ + std::mutex& target_mutex; /* 本 Scene Frame Target 的生命周期门。 */ bool released{}; /* finish_recording 成功后禁止回滚。 */ ~Recording_Scope() { - if (!released) target.abort(); + if (released) return; + try { + std::lock_guard recording_lock(target_mutex); + target.abort(); + } + catch (...) {} } - } recording_scope{frame_target}; + } recording_scope{frame_target, target_mutex_}; std::unique_ptr artifact{nullptr, &dvz_scene_frame_artifact_destroy}; @@ -2368,7 +2553,6 @@ Datoviz_Visual_Backend::Private::prepare( observation.emit_ns = trace_now_ns() - phase_started; } catch (...) { - frame_target.abort(); raise_context("preparing Datoviz frame", std::current_exception()); } if (observe) { @@ -2384,13 +2568,17 @@ Datoviz_Visual_Backend::Private::prepare( register_external_attributes(stream, *target_index); const DvzStreamFrame target_frame = frame_target.stream_frame(); 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); - const DvzDrp2ValidationResult result = - attached - ? dvz_drp2_runtime_execute(runtime_slots_[*target_index].runtime, stream) - : DvzDrp2ValidationResult{}; + bool attached{}; + DvzDrp2ValidationResult result{}; + { + attached = stream != nullptr && + dvz_drp2_runtime_attach_frame_target( + runtime_slots_[*target_index].runtime, + color_target_id, &target_frame); + if (attached) + result = dvz_drp2_runtime_execute( + runtime_slots_[*target_index].runtime, stream); + } observation.execute_ns = trace_now_ns() - phase_started; observation.validation_performed = true; observation.validation_ok = attached && result.ok; @@ -2405,11 +2593,9 @@ Datoviz_Visual_Backend::Private::prepare( } artifact.reset(); if (!attached) { - frame_target.abort(); throw std::runtime_error("failed to attach the Datoviz point frame target"); } if (!result.ok) { - frame_target.abort(); std::string message = "failed to execute Datoviz point frame: validation code " + std::to_string(static_cast(result.code)) + @@ -2419,45 +2605,60 @@ Datoviz_Visual_Backend::Private::prepare( observation.artifact_json, result.command_index); throw std::runtime_error(std::move(message)); } - frame_target.finish_recording(); + mark_target_runtime_resources_uploaded(*target_index); + { + std::lock_guard recording_lock(target_mutex_); + frame_target.finish_recording(); + } recording_scope.released = true; input_changed_ = false; return Pending_Frame{ - frame_target.device(), frame_target.fence(), scene.viewport, + reinterpret_cast(frame_target.device()), + reinterpret_cast(frame_target.fence()), scene.viewport, frame_sequence, *target_index, frame_target.generation(), std::move(observation) }; } -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 = trace_now_ns(); - target(pending).submit(); - pending.observation.submit_ns = trace_now_ns() - started; +void Scene_Datoviz_State::submit( + Pending_Frame& pending, + std::function completion) { + if (!completion) + throw std::invalid_argument("Datoviz submission completion is empty"); + const std::uint64_t queued = trace_now_ns(); + render_context_->enqueue_submission( + [this, &pending, queued, completion = std::move(completion)]() mutable { + try { + pending.observation.queue_submit_wait_ns = + trace_now_ns() - queued; + std::lock_guard target_lock(target_mutex_); + const std::uint64_t started = trace_now_ns(); + target(pending).submit(); + pending.observation.submit_ns = trace_now_ns() - started; + completion({}); + } + catch (...) { completion(std::current_exception()); } + }); } -Datoviz_Visual_Backend::Completed_Frame -Datoviz_Visual_Backend::Private::collect( +Scene_Datoviz_State::Completed_Frame Scene_Datoviz_State::collect( Pending_Frame pending) { /* Fence 已完成,读回缓冲与查询池只属于本目标槽;不同 Scene 的映射内存 - * 下载可以并行,且不能反向阻塞 Render_Domain 的下一批提交。 */ + * 下载可以在共享 Task_Resource 上并行。 */ std::lock_guard target_lock(target_mutex_); const std::uint64_t readback_started = trace_now_ns(); auto collection = target(pending).collect(); pending.observation.readback_ns = trace_now_ns() - readback_started; pending.observation.readback_bytes = collection.pixels.size(); pending.observation.gpu = std::move(collection.gpu_timing); - return {pending.extent, std::move(collection.pixels), - std::move(pending.observation)}; + return { + pending.extent, std::move(collection.pixels), + std::move(pending.observation) + }; } -void Datoviz_Visual_Backend::Private::discard(Pending_Frame pending) { +void Scene_Datoviz_State::discard(Pending_Frame pending) { std::lock_guard target_lock(target_mutex_); target(pending).discard_after_completion(); } -void Datoviz_Visual_Backend::Private::quarantine( +void Scene_Datoviz_State::quarantine( Pending_Frame pending) noexcept { quarantined_.store(true, std::memory_order_release); try { @@ -2471,24 +2672,17 @@ void Datoviz_Visual_Backend::Private::quarantine( } catch (...) {} } -void Datoviz_Visual_Backend::Private::abandon_resources() noexcept { +void Scene_Datoviz_State::abandon_resources() noexcept { quarantined_.store(true, std::memory_order_release); static_cast(targets_.release()); retain_quarantined_context(render_context_); } -void Datoviz_Visual_Backend::Private::destroy() { +void Scene_Datoviz_State::destroy() { if (quarantined_.load(std::memory_order_acquire)) { abandon_resources(); return; } auto context = render_context_; - std::unique_lock api_lock; - std::unique_lock queue_lock; - if (context) { - api_lock = std::unique_lock(context->api_mutex(), std::defer_lock); - queue_lock = std::unique_lock(context->queue_mutex(), std::defer_lock); - std::lock(api_lock, queue_lock); - } { std::lock_guard target_lock(target_mutex_); for (auto& slot : runtime_slots_) { @@ -2520,8 +2714,7 @@ void Datoviz_Visual_Backend::Private::destroy() { dvz_pinned_readout_destroy(hover_readout_); hover_readout_ = nullptr; } - if (panel_ && input_router_ != nullptr) - (void)dvz_panel_connect_input(panel_, nullptr); + if (panel_ && input_router_ != nullptr) (void)dvz_panel_connect_input(panel_, nullptr); if (gesture_handler_ != nullptr) { dvz_pointer_gesture_handler_destroy(gesture_handler_); gesture_handler_ = nullptr; @@ -2541,72 +2734,23 @@ void Datoviz_Visual_Backend::Private::destroy() { dvz_scene_destroy(scene_); scene_ = nullptr; } + if (context) { + VkDevice device = dvz_device_handle( + dvz_gpu_ctx_device(context->gpu_context().get())); + if (descriptor_pool_ != 0) { + vkDestroyDescriptorPool( + device, reinterpret_cast(descriptor_pool_), + nullptr); + descriptor_pool_ = 0; + } + if (command_pool_ != 0) { + vkDestroyCommandPool( + device, reinterpret_cast(command_pool_), nullptr); + command_pool_ = 0; + } + } } render_context_.reset(); - if (api_lock.owns_lock()) api_lock.unlock(); - 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, - std::uint64_t occurred_at_ns) { - static_cast(*d).dispatch_pointer( - type, x, y, button, modifiers, viewport, occurred_at_ns); -} - -void Datoviz_Visual_Backend::dispatch_wheel( - float x, float y, float delta_x, float delta_y, - Keyboard_Modifier modifiers, Extent viewport, - std::uint64_t occurred_at_ns) { - static_cast(*d).dispatch_wheel( - x, y, delta_x, delta_y, modifiers, viewport, occurred_at_ns); -} - -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/scene/Scene_Datoviz_State.hpp b/render_3D/render_3D/scene/Scene_Datoviz_State.hpp new file mode 100644 index 0000000..1952bba --- /dev/null +++ b/render_3D/render_3D/scene/Scene_Datoviz_State.hpp @@ -0,0 +1,191 @@ +#pragma once + +#include "../base/Datoviz_Frame_Observation.hpp" +#include "../detail/Backend_Types.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct DvzBuffer; +struct DvzController; +struct DvzDrp2CommandStream; +struct DvzDrp2Runtime; +struct DvzFigure; +struct DvzFont; +struct DvzFramePlanEmitter; +struct DvzInputRouter; +struct DvzItemInteraction; +struct DvzPanel; +struct DvzPinnedReadout; +struct DvzPointerGestureHandler; +struct DvzSampledField; +struct DvzScene; +struct DvzSceneBuffer; +struct DvzSceneFrameArtifact; +struct DvzText; +struct DvzVisual; + +namespace aethera { namespace render_3d { namespace detail { + +struct Datoviz_Render_Context; + +struct Scene_Datoviz_State { + struct Pending_Frame { + std::uintptr_t device{}; + std::uintptr_t fence{}; + Extent extent{}; + std::uint64_t sequence{}; + std::uint8_t target_index{}; + std::uint64_t target_generation{}; + Datoviz_Frame_Observation observation{}; + }; + + struct Completed_Frame { + Extent extent{}; + std::vector pixels{}; + Datoviz_Frame_Observation observation{}; + }; + struct Frame_Target; + struct Frame_Targets; + + struct External_Attribute { + std::string name{}; + std::uint32_t stride{}; + std::uint32_t capacity{}; + not_null scene_buffer; + owner gpu_buffer{}; + std::uint8_t registered_targets{}; + }; + + struct Visual_Instance { + Visual_Identity identity{}; + Visual_Family family{Visual_Family::point}; + not_null visual; + DvzSampledField* field{}; + DvzFont* font{}; + DvzText* coordinate_text{}; + std::array field_extent{}; + std::uint64_t applied_revision{}; + std::array uploaded_data_revisions{}; + std::optional applied{}; + std::vector attributes{}; + }; + + struct Runtime_Slot { + owner runtime{}; + owner emitter{}; + }; + + Scene_Datoviz_State(); + ~Scene_Datoviz_State(); + Scene_Datoviz_State(const Scene_Datoviz_State&) = delete; + Scene_Datoviz_State& operator=(const Scene_Datoviz_State&) = delete; + + 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]] bool target_runtime_resources_current( + const Prepared_Visual_Batch& visuals, + std::uint8_t target_index) const; + [[nodiscard]] std::vector stale_runtime_visuals( + const Prepared_Visual_Batch& visuals, + std::uint8_t target_index) const; + void mark_target_runtime_resources_uploaded(std::uint8_t target_index); + [[nodiscard]] std::optional reuse_recorded_target( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, + std::uint64_t frame_sequence, bool observe, bool readback); + [[nodiscard]] std::optional prepare( + const Scene_3D_Parameters& scene, + const Prepared_Visual_Batch& visuals, + std::uint64_t frame_sequence, bool observe, bool readback); + [[nodiscard]] 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, + std::function completion); + [[nodiscard]] Completed_Frame collect(Pending_Frame pending); + void discard(Pending_Frame pending); + void quarantine(Pending_Frame pending) noexcept; + void mark_controller_input_applied() noexcept; + void dispatch_pointer(Event_Type type, float x, float y, + Mouse_Button button, + Keyboard_Modifier modifiers, + Extent viewport, + std::uint64_t occurred_at_ns); + void dispatch_wheel(float x, float y, float delta_x, float delta_y, + Keyboard_Modifier modifiers, + Extent viewport, + std::uint64_t occurred_at_ns); + void dispatch_key(const Key_Event& event); + void abandon_resources() noexcept; + void destroy(); + + std::shared_ptr render_context_{}; + std::uintptr_t command_pool_{}; + std::uintptr_t descriptor_pool_{}; + mutable std::mutex target_mutex_{}; + std::array runtime_slots_{}; + owner scene_{}; + DvzFigure* figure_{}; + DvzPanel* panel_{}; + std::vector visuals_{}; + std::vector> external_buffers_{}; + DvzVisual* axes_visual_{}; + DvzText* axes_text_{}; + owner item_interaction_{}; + owner hover_readout_{}; + owner camera_controller_{}; + owner input_router_{}; + owner gesture_handler_{}; + std::unique_ptr targets_{}; + Extent figure_extent_{}; + std::uint64_t target_generation_{}; + std::uint64_t command_revision_{1}; + std::uint64_t controller_revision_{1}; + std::optional applied_camera_{}; + std::optional> applied_axes_{}; + bool input_changed_{}; + std::atomic_bool quarantined_{}; +}; + +}}} // namespace aethera::render_3d::detail diff --git a/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h b/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h index 2bcc497..117b425 100644 --- a/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h +++ b/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h @@ -54,6 +54,8 @@ struct DvzDrp2RuntimeConfig uint32_t flags; DvzDevice* device; DvzVma* allocator; + uintptr_t command_pool; + uintptr_t descriptor_pool; bool semantic_only; }; @@ -83,6 +85,9 @@ struct DvzDrp2ExternalBufferDesc DVZ_EXPORT DvzDrp2RuntimeConfig dvz_drp2_runtime_vklite_config(DvzDevice* device, DvzVma* allocator); +DVZ_EXPORT void dvz_drp2_runtime_vklite_pools( + DvzDrp2RuntimeConfig* config, uintptr_t command_pool, uintptr_t descriptor_pool); + /** * Return a default external-buffer descriptor. @@ -213,6 +218,24 @@ DVZ_EXPORT bool dvz_drp2_runtime_copy_texture_to_frame( DvzDrp2Runtime* runtime, uint64_t texture_id, const DvzStreamFrame* frame); +/** + * Upload bytes directly into a live mapped DRP2 buffer. + * + * The requested byte range must fit in a live buffer created with + * `DVZ_DRP2_BUFFER_USAGE_MAP_WRITE`. This updates an existing runtime resource and does not execute + * a command stream or alter semantic object state. + * + * @param runtime the vklite runtime + * @param buffer_id the DRP2 buffer id used in the stream + * @param offset byte offset within the buffer + * @param size number of bytes to write + * @param src source CPU buffer containing at least size bytes + * @return true when the live mapped buffer and byte range are valid and the upload was issued + */ +DVZ_EXPORT bool dvz_drp2_runtime_upload_buffer( + DvzDrp2Runtime* runtime, uint64_t buffer_id, uint64_t offset, uint64_t size, const void* src); + + /** * Download bytes from a DRP2 buffer into CPU memory. * diff --git a/render_3D/third_party/datoviz/include/datoviz/scene.h b/render_3D/third_party/datoviz/include/datoviz/scene.h index b24323b..f7c5fcd 100644 --- a/render_3D/third_party/datoviz/include/datoviz/scene.h +++ b/render_3D/third_party/datoviz/include/datoviz/scene.h @@ -518,6 +518,55 @@ DVZ_EXPORT DvzSceneFrameArtifact* dvz_figure_emit_frame_with_emitter( const DvzFramePlanEmitConfig* cfg); +/** + * Upload the current panel-controller transforms into an already emitted runtime. + * + * This updates only dynamic common MVP buffers retained by the emitter. Existing resources, + * descriptor bindings, and recorded command buffers remain unchanged. The caller must ensure the + * destination runtime is not being consumed by GPU work while its mapped buffers are written. + * + * @param figure the figure whose panel controllers are authoritative + * @param emitter the persistent emitter paired with the destination runtime + * @param runtime the destination DRP2 runtime + * @param uploaded_bytes output number of bytes uploaded, or NULL + * @return true when every retained dynamic MVP buffer was resolved and updated + */ +DVZ_EXPORT bool dvz_figure_update_mvp_buffers( + DvzFigure* figure, DvzFramePlanEmitter* emitter, DvzDrp2Runtime* runtime, + uint64_t* uploaded_bytes); + + +/** + * Realize dirty retained visual resources into an upload-only command stream. + * + * This path performs Scene lowering and stream emission only. The caller executes the returned + * owned stream on its destination runtime and commits the Figure only after execution succeeds. + * `replay_visuals` identifies the Visual payloads missing from that runtime, avoiding a Scene-wide + * dirty replay when independent runtimes consume the same retained Figure. + * + * @param figure the retained figure whose visual data is authoritative + * @param emitter the persistent emitter paired with the destination runtime + * @param replay_visuals borrowed Visual array whose payloads must be replayed for this runtime + * @param replay_visual_count number of entries in `replay_visuals` + * @param uploaded_bytes output number of payload bytes uploaded, or NULL + * @param command_recording_valid output whether existing draw command recording remains valid + * @param report output diagnostic report, or NULL + * @return owned upload-only stream, or NULL when lowering or emission failed + */ +DVZ_EXPORT DvzDrp2CommandStream* dvz_figure_prepare_runtime_resources( + DvzFigure* figure, DvzFramePlanEmitter* emitter, DvzVisual* const* replay_visuals, + uint32_t replay_visual_count, uint64_t* uploaded_bytes, bool* command_recording_valid, + DvzDiagnosticReport* report); + + +/** + * Commit successful upload-only runtime execution to the retained Figure. + * + * @param figure the Figure that produced the executed resource stream + */ +DVZ_EXPORT void dvz_figure_commit_runtime_resources(DvzFigure* figure); + + /** * Destroy a frame artifact. * diff --git a/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h b/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h index 27ca33d..d7ae746 100644 --- a/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h +++ b/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h @@ -87,6 +87,11 @@ DVZ_EXPORT void dvz_commands_free(DvzCommands* cmds); DVZ_EXPORT void dvz_commands(DvzDevice* device, DvzQueue* queue, uint32_t count, DvzCommands* cmds); +/** Allocate command buffers from an explicitly-owned command pool. */ +DVZ_EXPORT void dvz_commands_pool( + DvzDevice* device, DvzQueue* queue, VkCommandPool command_pool, uint32_t count, + DvzCommands* cmds); + /** diff --git a/render_3D/third_party/datoviz/include/datoviz/vklite/descriptors.h b/render_3D/third_party/datoviz/include/datoviz/vklite/descriptors.h index f182594..afc7df8 100644 --- a/render_3D/third_party/datoviz/include/datoviz/vklite/descriptors.h +++ b/render_3D/third_party/datoviz/include/datoviz/vklite/descriptors.h @@ -75,6 +75,10 @@ DVZ_EXPORT DvzDescriptors* dvz_descriptors_create_wrapper(void); */ DVZ_EXPORT void dvz_descriptors(DvzSlots* slots, DvzDescriptors* descriptors); +/** Allocate descriptor sets from an explicitly-owned descriptor pool. */ +DVZ_EXPORT void dvz_descriptors_pool( + DvzSlots* slots, VkDescriptorPool descriptor_pool, DvzDescriptors* descriptors); + /** diff --git a/render_3D/third_party/datoviz/src/drp2/_runtime.h b/render_3D/third_party/datoviz/src/drp2/_runtime.h index a33081e..d60aa61 100644 --- a/render_3D/third_party/datoviz/src/drp2/_runtime.h +++ b/render_3D/third_party/datoviz/src/drp2/_runtime.h @@ -104,6 +104,8 @@ struct DvzDrp2Runtime { DvzDevice* device; DvzVma* allocator; + VkCommandPool command_pool; + VkDescriptorPool descriptor_pool; bool semantic_only; Drp2RuntimeState* semantic_state; #if DVZ_DRP2_HAS_VKLITE @@ -318,7 +320,7 @@ bool _vklite_defer_destroy_object( Drp2VkliteState* state, Drp2VkliteObject* object, VkCommandBuffer command_buffer); void _vklite_flush_deferred_for_command_buffer( Drp2VkliteState* state, VkCommandBuffer command_buffer); -DvzCommands* _vklite_owned_commands_create(DvzDevice* device); +DvzCommands* _vklite_owned_commands_create(DvzDrp2Runtime* runtime); void _vklite_owned_commands_destroy(DvzCommands* cmds); DvzCommands* _vklite_borrowed_frame_commands_create( DvzDevice* device, VkCommandBuffer command_buffer); diff --git a/render_3D/third_party/datoviz/src/drp2/backend.c b/render_3D/third_party/datoviz/src/drp2/backend.c index 594a976..2b7411e 100644 --- a/render_3D/third_party/datoviz/src/drp2/backend.c +++ b/render_3D/third_party/datoviz/src/drp2/backend.c @@ -426,8 +426,10 @@ bool _vklite_attach_frame_target( * @param device the borrowed Vulkan device wrapper * @return owned command-buffer wrapper, or NULL on failure */ -DvzCommands* _vklite_owned_commands_create(DvzDevice* device) +DvzCommands* _vklite_owned_commands_create(DvzDrp2Runtime* runtime) { + ANN(runtime); + DvzDevice* device = runtime->device; ANN(device); DvzQueue* queue = dvz_device_queue(device, DVZ_QUEUE_MAIN); @@ -438,7 +440,10 @@ DvzCommands* _vklite_owned_commands_create(DvzDevice* device) if (cmds == NULL) return NULL; - dvz_commands(device, queue, 1, cmds); + if (runtime->command_pool != VK_NULL_HANDLE) + dvz_commands_pool(device, queue, runtime->command_pool, 1, cmds); + else + dvz_commands(device, queue, 1, cmds); if (dvz_commands_count(cmds) == 0 || dvz_commands_handle(cmds) == VK_NULL_HANDLE) { dvz_commands_free(cmds); diff --git a/render_3D/third_party/datoviz/src/drp2/pass.c b/render_3D/third_party/datoviz/src/drp2/pass.c index f19792c..46c8b2d 100644 --- a/render_3D/third_party/datoviz/src/drp2/pass.c +++ b/render_3D/third_party/datoviz/src/drp2/pass.c @@ -678,7 +678,7 @@ DvzDrp2ValidationResult _vklite_begin_render_pass( } else { - cmds = _vklite_owned_commands_create(state->runtime->device); + cmds = _vklite_owned_commands_create(state->runtime); if (cmds == NULL) return _vklite_fail_destroy_object( pass, DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); @@ -1022,7 +1022,7 @@ DvzDrp2ValidationResult _vklite_begin_compute_pass( if (pass == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); - DvzCommands* cmds = _vklite_owned_commands_create(state->runtime->device); + DvzCommands* cmds = _vklite_owned_commands_create(state->runtime); if (cmds == NULL) return _vklite_fail_destroy_object( pass, DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); @@ -1406,7 +1406,7 @@ DvzDrp2ValidationResult _vklite_resource_barrier( if (size == 0) size = VK_WHOLE_SIZE; - DvzCommands* cmds = _vklite_owned_commands_create(state->runtime->device); + DvzCommands* cmds = _vklite_owned_commands_create(state->runtime); if (cmds == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); if (dvz_cmd_begin_result(cmds) != 0) diff --git a/render_3D/third_party/datoviz/src/drp2/pipeline.c b/render_3D/third_party/datoviz/src/drp2/pipeline.c index f6e5e76..8e24f6f 100644 --- a/render_3D/third_party/datoviz/src/drp2/pipeline.c +++ b/render_3D/third_party/datoviz/src/drp2/pipeline.c @@ -83,9 +83,14 @@ static ShadercSyms g_shaderc = { .result_release = shaderc_result_release, }; #else -static ShadercSyms g_shaderc = {0}; -static bool g_shaderc_loaded = false; -static bool g_shaderc_available = false; +#if defined(_MSC_VER) +#define DVZ_DRP2_THREAD_LOCAL __declspec(thread) +#else +#define DVZ_DRP2_THREAD_LOCAL _Thread_local +#endif +static DVZ_DRP2_THREAD_LOCAL ShadercSyms g_shaderc = {0}; +static DVZ_DRP2_THREAD_LOCAL bool g_shaderc_loaded = false; +static DVZ_DRP2_THREAD_LOCAL bool g_shaderc_available = false; #endif #endif @@ -675,7 +680,11 @@ DvzDrp2ValidationResult _vklite_build_bind_group_descriptors( DvzDescriptors* descriptors = dvz_descriptors_create_wrapper(); if (descriptors == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); - dvz_descriptors(layout->slots, descriptors); + if (state->runtime->descriptor_pool != VK_NULL_HANDLE) + dvz_descriptors_pool( + layout->slots, state->runtime->descriptor_pool, descriptors); + else + dvz_descriptors(layout->slots, descriptors); for (uint32_t i = 0; i < bind_group->bind_group_entry_count; i++) { diff --git a/render_3D/third_party/datoviz/src/drp2/runtime.c b/render_3D/third_party/datoviz/src/drp2/runtime.c index 2324515..d7fe745 100644 --- a/render_3D/third_party/datoviz/src/drp2/runtime.c +++ b/render_3D/third_party/datoviz/src/drp2/runtime.c @@ -64,6 +64,9 @@ /*************************************************************************************************/ #if DVZ_DRP2_HAS_VKLITE +bool _dvz_drp2_runtime_vklite_upload_buffer( + DvzDrp2Runtime* runtime, uint64_t buffer_id, uint64_t offset, uint64_t size, const void* data); + bool _dvz_drp2_runtime_vklite_download_buffer( DvzDrp2Runtime* runtime, uint64_t buffer_id, uint64_t offset, uint64_t size, void* data); #endif @@ -206,6 +209,14 @@ DvzDrp2RuntimeConfig dvz_drp2_runtime_vklite_config(DvzDevice* device, DvzVma* a return cfg; } +void dvz_drp2_runtime_vklite_pools( + DvzDrp2RuntimeConfig* config, uintptr_t command_pool, uintptr_t descriptor_pool) +{ + ANN(config); + config->command_pool = command_pool; + config->descriptor_pool = descriptor_pool; +} + DvzDrp2ExternalBufferDesc dvz_drp2_external_buffer_desc(void) @@ -238,6 +249,8 @@ DvzDrp2Runtime* dvz_drp2_runtime_vklite(const DvzDrp2RuntimeConfig* cfg) ANN(runtime); runtime->device = cfg->device; runtime->allocator = cfg->allocator; + runtime->command_pool = (VkCommandPool)cfg->command_pool; + runtime->descriptor_pool = (VkDescriptorPool)cfg->descriptor_pool; runtime->semantic_only = cfg->semantic_only; return runtime; } @@ -257,6 +270,8 @@ DvzDrp2RuntimeConfig dvz_drp2_runtime_get_config(const DvzDrp2Runtime* runtime) return cfg; cfg.device = runtime->device; cfg.allocator = runtime->allocator; + cfg.command_pool = (uintptr_t)runtime->command_pool; + cfg.descriptor_pool = (uintptr_t)runtime->descriptor_pool; cfg.semantic_only = runtime->semantic_only; return cfg; } @@ -385,6 +400,44 @@ bool dvz_drp2_runtime_register_external_buffer( #if DVZ_DRP2_HAS_VKLITE +/** + * Upload bytes into a live host-writable vklite buffer owned by a DRP2 runtime. + * + * @param runtime the DRP2 runtime + * @param buffer_id the DRP2 buffer id + * @param offset the byte offset + * @param size the byte count to upload + * @param data the source buffer + * @return true when the buffer exists and the upload was issued + */ +bool _dvz_drp2_runtime_vklite_upload_buffer( + DvzDrp2Runtime* runtime, uint64_t buffer_id, uint64_t offset, uint64_t size, const void* data) +{ + if (runtime == NULL || runtime->vklite_state == NULL || data == NULL || size == 0) + return false; + + Drp2VkliteObject* object = _vklite_find(runtime->vklite_state, buffer_id); + if (object == NULL || object->buffer == NULL || runtime->semantic_state == NULL) + return false; + + Drp2Object* semantic = _drp2_find_any_object(runtime->semantic_state, buffer_id); + if (semantic == NULL || semantic->kind != DRP2_OBJECT_BUFFER || + (semantic->usage & DVZ_DRP2_BUFFER_USAGE_MAP_WRITE) == 0) + return false; + if (_drp2_range_overflows(offset, size, semantic->size)) + { + log_error( + "runtime buffer upload [%" PRIu64 ", %" PRIu64 ") exceeds buffer %" PRIu64 + " size %" PRIu64, + offset, offset + size, buffer_id, semantic->size); + return false; + } + + dvz_buffer_upload(object->buffer, offset, size, data); + return true; +} + + /** * Download bytes from a live vklite buffer owned by a DRP2 runtime. * @@ -675,3 +728,19 @@ bool dvz_drp2_runtime_download_buffer( return false; #endif } + + +bool dvz_drp2_runtime_upload_buffer( + DvzDrp2Runtime* runtime, uint64_t buffer_id, uint64_t offset, uint64_t size, const void* src) +{ +#if DVZ_DRP2_HAS_VKLITE + return _dvz_drp2_runtime_vklite_upload_buffer(runtime, buffer_id, offset, size, src); +#else + (void)runtime; + (void)buffer_id; + (void)offset; + (void)size; + (void)src; + return false; +#endif +} diff --git a/render_3D/third_party/datoviz/src/drp2/transfer.c b/render_3D/third_party/datoviz/src/drp2/transfer.c index a50a4ca..668c4c0 100644 --- a/render_3D/third_party/datoviz/src/drp2/transfer.c +++ b/render_3D/third_party/datoviz/src/drp2/transfer.c @@ -375,7 +375,7 @@ DvzDrp2ValidationResult _vklite_write_texture( dvz_buffer_upload(staging, 0, size, upload_src); dvz_free(decoded); - DvzCommands* cmds = _vklite_owned_commands_create(state->runtime->device); + DvzCommands* cmds = _vklite_owned_commands_create(state->runtime); if (cmds == NULL) { dvz_buffer_destroy(staging); @@ -429,7 +429,7 @@ DvzDrp2ValidationResult _vklite_copy_buffer_to_buffer( if (src == NULL || src->buffer == NULL || dst == NULL || dst->buffer == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); - DvzCommands* cmds = _vklite_owned_commands_create(state->runtime->device); + DvzCommands* cmds = _vklite_owned_commands_create(state->runtime); if (cmds == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); @@ -470,7 +470,7 @@ DvzDrp2ValidationResult _vklite_copy_buffer_to_texture( if (!_drp2_texture_format_bytes_per_texel(dst->format, &bytes_per_texel)) return _drp2_fail(DVZ_DRP2_VALIDATION_USAGE, command_index); - DvzCommands* cmds = _vklite_owned_commands_create(state->runtime->device); + DvzCommands* cmds = _vklite_owned_commands_create(state->runtime); if (cmds == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); @@ -520,7 +520,7 @@ DvzDrp2ValidationResult _vklite_copy_texture_to_buffer( if (!_drp2_texture_format_bytes_per_texel(src->format, &bytes_per_texel)) return _drp2_fail(DVZ_DRP2_VALIDATION_USAGE, command_index); - DvzCommands* cmds = _vklite_owned_commands_create(state->runtime->device); + DvzCommands* cmds = _vklite_owned_commands_create(state->runtime); if (cmds == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); @@ -563,7 +563,7 @@ DvzDrp2ValidationResult _vklite_copy_texture_to_texture( if (src == NULL || src->images == NULL || dst == NULL || dst->images == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); - DvzCommands* cmds = _vklite_owned_commands_create(state->runtime->device); + DvzCommands* cmds = _vklite_owned_commands_create(state->runtime); if (cmds == NULL) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); diff --git a/render_3D/third_party/datoviz/src/scene/core/_scene.h b/render_3D/third_party/datoviz/src/scene/core/_scene.h index 61ed135..db34f35 100644 --- a/render_3D/third_party/datoviz/src/scene/core/_scene.h +++ b/render_3D/third_party/datoviz/src/scene/core/_scene.h @@ -385,6 +385,7 @@ DvzId _scene_next_id(DvzScene* scene); bool _scene_runtime_emitter_reset(DvzScene* scene); void _scene_mark_runtime_payloads_dirty(DvzScene* scene); +void _scene_mark_visual_runtime_dirty(DvzVisual* visual); struct DvzPanelView2DResolved { diff --git a/render_3D/third_party/datoviz/src/scene/core/figure_emit.c b/render_3D/third_party/datoviz/src/scene/core/figure_emit.c index 299eb83..987c335 100644 --- a/render_3D/third_party/datoviz/src/scene/core/figure_emit.c +++ b/render_3D/third_party/datoviz/src/scene/core/figure_emit.c @@ -18,9 +18,11 @@ #include #include #include +#include #include "_assertions.h" #include "_compat.h" +#include "frame_plan/emit.h" #include "frame_plan/frame_plan.h" #include "_log.h" #include "scene_emit/scene_emit.h" @@ -35,6 +37,7 @@ #include "core/frame_trace_internal.h" #include "domain/field_internal.h" #include "plot/internal.h" +#include "runtime/_frame_plan_runtime_upload.h" #include "_visual_internal.h" #include "datoviz/scene.h" @@ -792,6 +795,25 @@ DvzDrp2CommandStream* _scene_figure_emit_stream_with_emitter_ex( } +/** + * Copy one MVP into a deterministic uniform payload with zeroed padding. + * + * @param dst the destination MVP + * @param src the source MVP + */ +static void _scene_mvp_uniform_copy(DvzMVP* dst, const DvzMVP* src) +{ + ANN(dst); + ANN(src); + dvz_memset(dst, sizeof(DvzMVP), 0, sizeof(DvzMVP)); + dvz_memcpy(dst->model, sizeof(dst->model), src->model, sizeof(src->model)); + dvz_memcpy(dst->view, sizeof(dst->view), src->view, sizeof(src->view)); + dvz_memcpy(dst->proj, sizeof(dst->proj), src->proj, sizeof(src->proj)); + dst->time = src->time; + dst->flags = src->flags; +} + + DvzDrp2CommandStream* _scene_figure_emit_stream_ex( DvzFigure* figure, const DvzCapabilitySnapshot* caps, DvzDiagnosticReport* report, @@ -854,6 +876,213 @@ DvzSceneFrameArtifact* dvz_figure_emit_frame_with_emitter( +bool dvz_figure_update_mvp_buffers( + DvzFigure* figure, DvzFramePlanEmitter* emitter, DvzDrp2Runtime* runtime, + uint64_t* uploaded_bytes) +{ + if (uploaded_bytes != NULL) + *uploaded_bytes = 0; + if (figure == NULL || emitter == NULL || runtime == NULL) + return false; + + char figure_id[64]; + _scene_figure_id(figure, figure_id, sizeof(figure_id)); + uint64_t total = 0; + for (uint32_t panel_index = 0; panel_index < figure->panel_count; panel_index++) + { + char panel_id[DVZ_SCENE_LABEL_SIZE]; + dvz_snprintf(panel_id, sizeof(panel_id), "%s_p%u", figure_id, panel_index); + size_t panel_id_size = strlen(panel_id); + DvzMVP panel_mvp = {0}; + _scene_panel_apply_mvp(&figure->panels[panel_index], &panel_mvp); + + for (uint32_t slot_index = 0; slot_index < emitter->mvp_panel_count; slot_index++) + { + const char* slot_key = emitter->mvp_panel_ids[slot_index]; + if (!emitter->mvp_dynamic[slot_index] || + strncmp(slot_key, panel_id, panel_id_size) != 0 || + slot_key[panel_id_size] != '_') + continue; + + DvzMVP updated = {0}; + _scene_mvp_uniform_copy(&updated, &panel_mvp); + if (emitter->mvp_preserve_model[slot_index]) + { + dvz_memcpy( + updated.model, sizeof(updated.model), emitter->mvp_cache[slot_index].model, + sizeof(emitter->mvp_cache[slot_index].model)); + } + updated.flags |= emitter->mvp_cache[slot_index].flags; + + char buffer_key[2 * DVZ_SCENE_LABEL_SIZE]; + int key_size = dvz_snprintf( + buffer_key, sizeof(buffer_key), "_common_mvp_buf_%s", slot_key); + if (key_size < 0 || (size_t)key_size >= sizeof(buffer_key)) + return false; + uint64_t buffer_id = dvz_frame_plan_emitter_object_id(emitter, buffer_key); + if (buffer_id == 0 || + !dvz_drp2_runtime_upload_buffer( + runtime, buffer_id, 0, sizeof(DvzMVP), &updated)) + return false; + emitter->mvp_cache[slot_index] = updated; + total += sizeof(DvzMVP); + } + } + if (uploaded_bytes != NULL) + *uploaded_bytes = total; + return true; +} + + + +/** + * Emit only the upload nodes of one retained visual FramePlan. + * + * The runtime-mode emitter normally requires a render node because its public conversion path + * records a complete frame. Resource-only updates deliberately bypass that render conversion and + * reuse the same persistent resource-id map. + */ +static DvzDrp2CommandStream* _scene_emit_runtime_resource_updates( + DvzFramePlanEmitter* emitter, const DvzFramePlan* plan, DvzDiagnosticReport* report) +{ + ANN(emitter); + ANN(plan); + if (!emitter->handshake_sent) + { + (void)dvz_diagnostic_report_add( + report, "runtime resource update requires an already emitted runtime"); + return NULL; + } + + DvzDrp2CommandStream* stream = dvz_drp2_stream(); + if (stream == NULL) + return NULL; + bool ok = true; + for (uint32_t i = 0; ok && i < dvz_frame_plan_node_count(plan); i++) + { + const DvzFramePlanNode* node = dvz_frame_plan_node_get(plan, i); + if (node == NULL || dvz_frame_plan_node_type(node) != DVZ_FRAME_PLAN_NODE_UPLOAD) + continue; + uint64_t resource_id = 0; + ok = _emitter_emit_upload(emitter, stream, node, &resource_id); + } + if (!ok) + { + (void)dvz_diagnostic_report_add(report, "failed to emit runtime resource updates"); + dvz_drp2_stream_destroy(stream); + return NULL; + } + return stream; +} + + + +DvzDrp2CommandStream* dvz_figure_prepare_runtime_resources( + DvzFigure* figure, DvzFramePlanEmitter* emitter, DvzVisual* const* replay_visuals, + uint32_t replay_visual_count, uint64_t* uploaded_bytes, bool* command_recording_valid, + DvzDiagnosticReport* report) +{ + if (uploaded_bytes != NULL) + *uploaded_bytes = 0; + if (command_recording_valid != NULL) + *command_recording_valid = false; + if ( + figure == NULL || figure->scene == NULL || emitter == NULL || + (replay_visual_count > 0 && replay_visuals == NULL)) + return NULL; + + DvzDiagnosticReport local_report; + if (report == NULL) + { + dvz_diagnostic_report_init(&local_report); + report = &local_report; + } + for (uint32_t i = 0; i < replay_visual_count; i++) + _scene_mark_visual_runtime_dirty(replay_visuals[i]); + + if (!_scene_figure_resolve_layouts(figure)) + { + (void)dvz_diagnostic_report_add(report, "scene grid layout resolution failed"); + return NULL; + } + _scene_prepare_guide_visuals(figure); + _scene_prepare_bars_visuals(figure); + _scene_prepare_band_visuals(figure); + + char figure_id[64]; + _scene_figure_id(figure, figure_id, sizeof(figure_id)); + DvzFramePlan* plan = dvz_frame_plan(figure_id, 0); + if (plan == NULL) + return NULL; + _scene_emit_visual_uploads(figure, plan, report); + + DvzDrp2CommandStream* stream = + _scene_emit_runtime_resource_updates(emitter, plan, report); + if (stream == NULL) + { + dvz_frame_plan_destroy(plan); + return NULL; + } + + bool recording_valid = true; + uint64_t total = 0; + bool command_set_valid = true; + for (uint32_t i = 0; i < dvz_drp2_stream_count(stream); i++) + { + const DvzDrp2Command* command = dvz_drp2_stream_get(stream, i); + if (command == NULL) + { + command_set_valid = false; + break; + } + switch (dvz_drp2_command_type(command)) + { + case DVZ_DRP2_COMMAND_CREATE_BUFFER: + case DVZ_DRP2_COMMAND_CREATE_TEXTURE: + recording_valid = false; + break; + case DVZ_DRP2_COMMAND_WRITE_BUFFER: + total += command->u.write_buffer.size; + break; + case DVZ_DRP2_COMMAND_WRITE_TEXTURE: + total += (uint64_t)command->u.write_texture.bytes_per_row * + (uint64_t)command->u.write_texture.rows_per_image * + (uint64_t)command->u.write_texture.depth; + break; + default: + command_set_valid = false; + break; + } + } + + if (!command_set_valid) + (void)dvz_diagnostic_report_add( + report, "runtime resource update emitted a non-resource command"); + if (command_set_valid) + { + if (uploaded_bytes != NULL) + *uploaded_bytes = total; + if (command_recording_valid != NULL) + *command_recording_valid = recording_valid; + } + dvz_frame_plan_destroy(plan); + if (!command_set_valid) + { + dvz_drp2_stream_destroy(stream); + return NULL; + } + return stream; +} + + +void dvz_figure_commit_runtime_resources(DvzFigure* figure) +{ + ANN(figure); + _scene_commit_emit_success(figure); +} + + + void dvz_scene_frame_artifact_destroy(DvzSceneFrameArtifact* artifact) { _scene_frame_artifact_destroy(artifact); diff --git a/render_3D/third_party/datoviz/src/scene/core/scene.c b/render_3D/third_party/datoviz/src/scene/core/scene.c index 7b14890..15c7f8c 100644 --- a/render_3D/third_party/datoviz/src/scene/core/scene.c +++ b/render_3D/third_party/datoviz/src/scene/core/scene.c @@ -168,7 +168,7 @@ uint64_t _scene_next_request_serial(DvzScene* scene) * * @param visual the visual */ -static void _scene_mark_visual_runtime_dirty(DvzVisual* visual) +void _scene_mark_visual_runtime_dirty(DvzVisual* visual) { if (visual == NULL || visual->scene == NULL) return; diff --git a/render_3D/third_party/datoviz/src/scene/frame_plan/emit.h b/render_3D/third_party/datoviz/src/scene/frame_plan/emit.h index 725a599..360781e 100644 --- a/render_3D/third_party/datoviz/src/scene/frame_plan/emit.h +++ b/render_3D/third_party/datoviz/src/scene/frame_plan/emit.h @@ -150,6 +150,8 @@ struct DvzFramePlanEmitter /* Common cache: APPLY and FIXED slots are panel-specific once viewport is part of set 0. */ char mvp_panel_ids[DVZ_SCENE_COMMON_CACHE_CAPACITY][DVZ_SCENE_LABEL_SIZE]; DvzMVP mvp_cache[DVZ_SCENE_COMMON_CACHE_CAPACITY]; + bool mvp_dynamic[DVZ_SCENE_COMMON_CACHE_CAPACITY]; + bool mvp_preserve_model[DVZ_SCENE_COMMON_CACHE_CAPACITY]; uint32_t mvp_panel_count; char viewport_panel_ids[DVZ_SCENE_COMMON_CACHE_CAPACITY][DVZ_SCENE_LABEL_SIZE]; DvzSceneViewportUniform viewport_cache[DVZ_SCENE_COMMON_CACHE_CAPACITY]; diff --git a/render_3D/third_party/datoviz/src/scene/runtime/common_bindings.c b/render_3D/third_party/datoviz/src/scene/runtime/common_bindings.c index c03ccd2..dec6e25 100644 --- a/render_3D/third_party/datoviz/src/scene/runtime/common_bindings.c +++ b/render_3D/third_party/datoviz/src/scene/runtime/common_bindings.c @@ -278,6 +278,9 @@ static bool _resolve_common_set( DvzMVP* mvp_slot = _emitter_mvp_slot(emitter, mvp_slot_key); if (mvp_slot == NULL) return false; + uint32_t mvp_slot_index = (uint32_t)(mvp_slot - emitter->mvp_cache); + emitter->mvp_dynamic[mvp_slot_index] = !fixed; + emitter->mvp_preserve_model[mvp_slot_index] = override_mvp != NULL; if (override_mvp != NULL) mvp_src = override_mvp; else if (fixed) diff --git a/render_3D/third_party/datoviz/src/vklite/_commands.h b/render_3D/third_party/datoviz/src/vklite/_commands.h index 236e83a..a8848db 100644 --- a/render_3D/third_party/datoviz/src/vklite/_commands.h +++ b/render_3D/third_party/datoviz/src/vklite/_commands.h @@ -30,6 +30,7 @@ struct DvzCommands DvzObject obj; DvzDevice* device; DvzQueue* queue; + VkCommandPool command_pool; uint32_t count; uint32_t current; diff --git a/render_3D/third_party/datoviz/src/vklite/commands.c b/render_3D/third_party/datoviz/src/vklite/commands.c index 9411e42..01e368b 100644 --- a/render_3D/third_party/datoviz/src/vklite/commands.c +++ b/render_3D/third_party/datoviz/src/vklite/commands.c @@ -84,7 +84,7 @@ static void _commands_release(DvzCommands* cmds) VkDevice vkd = dvz_device_handle(cmds->device); ANNVK(vkd); - VkCommandPool cpool = dvz_device_command_pool(cmds->device, dvz_queue_family(cmds->queue)); + VkCommandPool cpool = cmds->command_pool; if (cpool == VK_NULL_HANDLE) { log_warn( @@ -103,6 +103,14 @@ static void _commands_release(DvzCommands* cmds) } void dvz_commands(DvzDevice* device, DvzQueue* queue, uint32_t count, DvzCommands* cmds) +{ + dvz_commands_pool( + device, queue, dvz_device_command_pool(device, dvz_queue_family(queue)), count, cmds); +} + +void dvz_commands_pool( + DvzDevice* device, DvzQueue* queue, VkCommandPool command_pool, uint32_t count, + DvzCommands* cmds) { ANN(cmds); ANN(device); @@ -113,11 +121,12 @@ void dvz_commands(DvzDevice* device, DvzQueue* queue, uint32_t count, DvzCommand cmds->device = device; cmds->queue = queue; + cmds->command_pool = command_pool; cmds->count = count; VkCommandBufferAllocateInfo info = {0}; info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - info.commandPool = dvz_device_command_pool(device, dvz_queue_family(queue)); + info.commandPool = command_pool; info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; info.commandBufferCount = count; VkResult res = vkAllocateCommandBuffers(dvz_device_handle(device), &info, cmds->cmds); diff --git a/render_3D/third_party/datoviz/src/vklite/descriptors.c b/render_3D/third_party/datoviz/src/vklite/descriptors.c index da4e4ad..60c5668 100644 --- a/render_3D/third_party/datoviz/src/vklite/descriptors.c +++ b/render_3D/third_party/datoviz/src/vklite/descriptors.c @@ -64,6 +64,14 @@ DvzDescriptors* dvz_descriptors_create_wrapper(void) void dvz_descriptors(DvzSlots* slots, DvzDescriptors* descriptors) +{ + ANN(slots); + dvz_descriptors_pool( + slots, dvz_device_descriptor_pool(dvz_slots_device(slots)), descriptors); +} + +void dvz_descriptors_pool( + DvzSlots* slots, VkDescriptorPool descriptor_pool, DvzDescriptors* descriptors) { ANN(slots); ANN(descriptors); @@ -81,7 +89,7 @@ void dvz_descriptors(DvzSlots* slots, DvzDescriptors* descriptors) descriptors->device = device; descriptors->slots = slots; VkDevice vkd = dvz_device_handle(device); - VkDescriptorPool dpool = dvz_device_descriptor_pool(device); + VkDescriptorPool dpool = descriptor_pool; ANNVK(vkd); ANNVK(dpool); diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 66986e9..de5814f 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -101,7 +101,7 @@ type Datoviz_Frame_Observation = { gpu_timing_requested: boolean; readback_requested: boolean; controller_input_applied: boolean; - timings_ms: {render_domain_queue_wait: number; apply: number; emit: number; execute: number; + timings_ms: {queue_submit_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}; @@ -1777,7 +1777,7 @@ function taskflow_component_state(node: Taskflow_Node_Trace, components: Compone return {owner: node.owner, prop: node.prop ?? {}, state: node.state ?? {}}; } -function taskflow_render_domain_state(node: Taskflow_Node_Trace, frame?: Taskflow_Frame_Trace) { +function taskflow_backend_state(node: Taskflow_Node_Trace, frame?: Taskflow_Frame_Trace) { if (!frame || (node.attributes?.gpu_submit_owner !== "GPU Render Domain" && node.attributes?.backend !== "Datoviz")) return null; const markers = frame.markers ?? {}; @@ -1806,7 +1806,7 @@ function taskflow_node_state(node: Taskflow_Node_Trace, components: Component[], if (component) return component; if (node.attributes?.owner === "gallery" && gallery_state) return gallery_state; - return taskflow_render_domain_state(node, frame); + return taskflow_backend_state(node, frame); } /* @@ -2378,7 +2378,7 @@ function Datoviz_Frame_Summary({observation}: {observation: Datoviz_Frame_Observ
本帧 Datoviz 路径观测数据归属于 Frame_3D #{observation.render_sequence},不读取独立历史状态。
{structural ? "重新录制命令" : "复用录制命令"}
-
Render Domain 排队
{milliseconds(timing.render_domain_queue_wait)}
+
VkQueue 排队
{milliseconds(timing.queue_submit_wait)}
Datoviz Apply
{milliseconds(timing.apply)}
Frame Plan emit
{milliseconds(timing.emit)}
DRP2 execute
{milliseconds(timing.execute)}