diff --git a/render_3D/render_3D/base/Frame_3D.cpp b/render_3D/render_3D/base/Frame_3D.cpp index ca79437..ba559a8 100644 --- a/render_3D/render_3D/base/Frame_3D.cpp +++ b/render_3D/render_3D/base/Frame_3D.cpp @@ -4,11 +4,13 @@ namespace aethera::render_3d { struct Frame_3D::Private { Extent extent{}; /* 本帧 GPU 读回图像尺寸。 */ std::vector pixels{}; /* 本帧按 RGBA8 连续存储的 GPU 读回像素。 */ + Frame_3D_Output output{Frame_3D_Output::pixels}; /* 调用方要求的最终帧输出。 */ }; -Frame_3D::Frame_3D(Frame_Identity identity) : Render_Frame(identity), d(std::make_unique()) {} +Frame_3D::Frame_3D(Frame_Identity identity, Frame_3D_Output output) : Render_Frame(identity), d(std::make_unique()) { d->output = output; } Frame_3D::~Frame_3D() = default; Extent Frame_3D::extent() const noexcept { return d->extent; } std::span Frame_3D::pixels() const noexcept { return d->pixels; } +Frame_3D_Output Frame_3D::output() const noexcept { return d->output; } void detail::Frame_3D_Access::assign_pixels(Frame_3D* frame, Extent extent, std::vector pixels) { if (!frame) throw std::invalid_argument("Render_Scene_3D requires a non-null external frame"); frame->d->extent = extent; diff --git a/render_3D/render_3D/base/Frame_3D.hpp b/render_3D/render_3D/base/Frame_3D.hpp index ebc86d4..b460d3d 100644 --- a/render_3D/render_3D/base/Frame_3D.hpp +++ b/render_3D/render_3D/base/Frame_3D.hpp @@ -5,6 +5,10 @@ #include #include namespace aethera::render_3d { +enum class Frame_3D_Output : std::uint8_t { + pixels, + diagnostics +}; class Frame_3D; namespace detail { struct Frame_3D_Access { @@ -13,10 +17,11 @@ struct Frame_3D_Access { } class Frame_3D final : public Render_Frame { public: - explicit Frame_3D(Frame_Identity identity); + explicit Frame_3D(Frame_Identity identity, Frame_3D_Output output = Frame_3D_Output::pixels); ~Frame_3D() override; [[nodiscard]] Extent extent() const noexcept; [[nodiscard]] std::span pixels() const noexcept; + [[nodiscard]] Frame_3D_Output output() const noexcept; private: struct Private; std::unique_ptr d; /* 本帧最终三维读回结果的唯一所有权。 */ diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp index 0f004c6..eabcaa3 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.cpp @@ -29,10 +29,9 @@ struct Async_Render_Backend::Implementation : std::enable_shared_from_this backend_frame{}; /* 已提交并等待 GPU fence 的后端帧。 */ Frame_3D* output{}; /* 调用方拥有且保证存活到完成回调的输出帧。 */ - Render_Domain::Frame_Completion domain_completion{}; /* 回读结束后释放同 GPU 的完整帧门闩。 */ }; struct Submission { - Prepared_Visual visual{}; /* 本帧不可变的 CPU Prepared 数据。 */ + Prepared_Visual visual{}; /* 共享 Prepare 发布的不可变 GPU 字段快照。 */ Scene_3D_Parameters parameters{}; /* 本帧 viewport 与清屏参数。 */ Frame_3D* output{}; /* 调用方拥有的最终输出帧。 */ }; @@ -71,24 +70,23 @@ void Async_Render_Backend::Implementation::render(const Prepared_Visual& visual, } void Async_Render_Backend::Implementation::submit(Submission submission) { auto self = shared_from_this(); - auto prepared = std::make_shared(std::move(submission.visual)); + auto prepared = std::move(submission.visual); auto pending = std::make_shared(Pending{std::nullopt, submission.output}); const auto parameters = submission.parameters; const auto sequence = submission.output->identity().sequence; - const auto queued = render_domain->try_post_frame([self, parameters, sequence, prepared, pending](Render_Domain::Frame_Completion domain_completion) { - pending->domain_completion = std::move(domain_completion); + const auto queued = render_domain->try_post([self, parameters, sequence, prepared, pending] { pending->output->mark(Frame_Trace_Marker::backend_queue_left); auto reservation = Gpu_Completion_Service::instance().prepare([self, pending](Gpu_Completion_Service::Result result) { self->finish(pending, std::move(result)); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }, true); - if (!reservation) { self->complete(pending->output); auto completion = std::exchange(pending->domain_completion, {}); if (completion) completion(); return; } - pending->backend_frame = self->backend->submit(parameters, *prepared, sequence, true); - if (!pending->backend_frame) { self->complete(pending->output); auto completion = std::exchange(pending->domain_completion, {}); if (completion) completion(); return; } + if (!reservation) { self->complete(pending->output); return; } + pending->backend_frame = self->backend->submit(parameters, prepared, sequence, true, pending->output->output() == Frame_3D_Output::pixels); + if (!pending->backend_frame) { self->complete(pending->output); return; } record_datoviz_trace(pending->output, pending->backend_frame->trace); pending->output->mark(Frame_Trace_Marker::gpu_submitted); reservation.reservation.watch(pending->backend_frame->device, pending->backend_frame->fence); }, [self, output = submission.output](std::exception_ptr value) { self->fail(std::move(value)); self->complete(output); }); if (queued != Render_Domain::Try_Post_Result::queued) { std::lock_guard lock(render_mutex); - pending_submissions.push_front(Submission{*prepared, parameters, submission.output}); + pending_submissions.push_front(Submission{std::move(prepared), parameters, submission.output}); frame_in_flight = false; } } @@ -108,9 +106,7 @@ void Async_Render_Backend::Implementation::finish(std::shared_ptr pendi } else self->backend->discard(std::move(*pending->backend_frame)); } self->complete(pending->output); - auto domain_completion = std::exchange(pending->domain_completion, {}); - if (domain_completion) domain_completion(); - }, [self, pending](std::exception_ptr value) { self->fail(std::move(value)); auto domain_completion = std::exchange(pending->domain_completion, {}); if (domain_completion) domain_completion(); }); + }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); if (result != Render_Domain::Admission_Result::none) fail(std::make_exception_ptr(std::runtime_error("render domain stopped before GPU completion collection"))); } catch (...) { fail(std::current_exception()); } } diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp index 607b29d..6869d5e 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -137,6 +140,45 @@ void configure_glyph_text(DvzScene* scene, DvzVisual* visual, const char* text) throw std::runtime_error("failed to upload Datoviz text geometry"); } } // namespace +class Datoviz_Render_Context final { +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( + 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_); + } + + [[nodiscard]] DvzGpuCtx* gpu_context() const noexcept { return gpu_context_; } + +private: + Datoviz_Render_Context(std::uint32_t gpu_index, bool validation_enabled) { + DvzGpuCtxConfig configuration = dvz_gpu_ctx_config(); + dvz_gpu_ctx_config_validation(&configuration, validation_enabled); + dvz_gpu_ctx_config_gpu(&configuration, gpu_index); + dvz_gpu_ctx_config_enable_canvas_extensions(&configuration, false); + gpu_context_ = dvz_gpu_ctx(&configuration); + if (gpu_context_ == nullptr) + throw std::runtime_error("failed to create shared Datoviz GPU context"); + } + + DvzGpuCtx* gpu_context_{}; /* 共享 GPU Device 与分配器的唯一所有权。 */ +}; + class Datoviz_Visual_Backend::Frame_Target final { public: struct Collection { @@ -198,9 +240,10 @@ public: ~Frame_Target() noexcept(false) { destroy(); } - void begin(bool observe) { + void begin(bool observe, bool readback) { if (in_flight_) throw std::logic_error("Datoviz frame target is still in flight"); observing_ = observe; + readback_requested_ = readback; if (observing_ && !timestamps_initialized_) { initialize_timestamps( dvz_gpu_ctx_device(gpu_context_), @@ -219,7 +262,7 @@ public: VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); } - else { + else if (completed_layout_ == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) { dvz_barrier_image_stage(image_barrier, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); dvz_barrier_image_access( @@ -227,6 +270,14 @@ public: VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); } + else { + dvz_barrier_image_stage(image_barrier, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + dvz_barrier_image_access( + image_barrier, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + } dvz_barrier_image_layout(image_barrier, completed_layout_, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); dvz_barrier_image_aspect(image_barrier, VK_IMAGE_ASPECT_COLOR_BIT); @@ -273,43 +324,49 @@ public: VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, query_pool_, 1); } - DvzBarriers image_barriers{}; - dvz_barriers(&image_barriers); - auto* image_barrier = - dvz_barriers_image(&image_barriers, dvz_image_handle(image_, 0)); - dvz_barrier_image_stage(image_barrier, - VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_2_TRANSFER_BIT); - dvz_barrier_image_access(image_barrier, - VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, - VK_ACCESS_2_TRANSFER_READ_BIT); - dvz_barrier_image_layout(image_barrier, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); - dvz_barrier_image_aspect(image_barrier, VK_IMAGE_ASPECT_COLOR_BIT); - dvz_barrier_image_mip(image_barrier, 0, 1); - dvz_barrier_image_layers(image_barrier, 0, 1); - dvz_cmd_barriers(commands_, &image_barriers); - if (observing_ && timestamps_supported_) { - vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, + if (readback_requested_) { + DvzBarriers image_barriers{}; + dvz_barriers(&image_barriers); + auto* image_barrier = + dvz_barriers_image(&image_barriers, dvz_image_handle(image_, 0)); + dvz_barrier_image_stage(image_barrier, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_2_TRANSFER_BIT); + dvz_barrier_image_access(image_barrier, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_2_TRANSFER_READ_BIT); + dvz_barrier_image_layout(image_barrier, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + dvz_barrier_image_aspect(image_barrier, VK_IMAGE_ASPECT_COLOR_BIT); + dvz_barrier_image_mip(image_barrier, 0, 1); + dvz_barrier_image_layers(image_barrier, 0, 1); + dvz_cmd_barriers(commands_, &image_barriers); + if (observing_ && timestamps_supported_) { + vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, + query_pool_, 2); + } + DvzImageRegion region{}; + dvz_image_region(®ion); + dvz_image_region_extent(®ion, extent_.width, extent_.height, 1); + dvz_cmd_copy_image_to_buffer( + commands_, dvz_image_handle(image_, 0), + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, ®ion, + dvz_buffer_handle(readback_), 0); + DvzBarriers buffer_barriers{}; + dvz_barriers(&buffer_barriers); + auto* buffer_barrier = dvz_barriers_buffer( + &buffer_barriers, dvz_buffer_handle(readback_), 0, byte_size_); + dvz_barrier_buffer_stage(buffer_barrier, VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_PIPELINE_STAGE_2_HOST_BIT); + dvz_barrier_buffer_access(buffer_barrier, VK_ACCESS_2_TRANSFER_WRITE_BIT, + VK_ACCESS_2_HOST_READ_BIT); + dvz_cmd_barriers(commands_, &buffer_barriers); + } + else if (observing_ && timestamps_supported_) { + vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, query_pool_, 2); } - DvzImageRegion region{}; - dvz_image_region(®ion); - dvz_image_region_extent(®ion, extent_.width, extent_.height, 1); - dvz_cmd_copy_image_to_buffer( - commands_, dvz_image_handle(image_, 0), - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, ®ion, - dvz_buffer_handle(readback_), 0); - DvzBarriers buffer_barriers{}; - dvz_barriers(&buffer_barriers); - auto* buffer_barrier = dvz_barriers_buffer( - &buffer_barriers, dvz_buffer_handle(readback_), 0, byte_size_); - dvz_barrier_buffer_stage(buffer_barrier, VK_PIPELINE_STAGE_2_TRANSFER_BIT, - VK_PIPELINE_STAGE_2_HOST_BIT); - dvz_barrier_buffer_access(buffer_barrier, VK_ACCESS_2_TRANSFER_WRITE_BIT, - VK_ACCESS_2_HOST_READ_BIT); - dvz_cmd_barriers(commands_, &buffer_barriers); if (observing_ && timestamps_supported_) { vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, @@ -325,29 +382,34 @@ public: dvz_fence_handle(fence_)) != VK_SUCCESS) throw std::runtime_error("failed to submit Datoviz point frame"); in_flight_ = true; - completed_layout_ = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + completed_layout_ = readback_requested_ ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; } [[nodiscard]] Collection collect() { if (!in_flight_) throw std::logic_error("Datoviz frame target has no pending frame"); Collection result; try { - result.pixels.resize(static_cast(byte_size_)); - dvz_buffer_download(readback_, 0, byte_size_, result.pixels.data()); + if (readback_requested_) { + result.pixels.resize(static_cast(byte_size_)); + dvz_buffer_download(readback_, 0, byte_size_, result.pixels.data()); + } result.gpu_timing = collect_gpu_timing(); } catch (...) { in_flight_ = false; observing_ = false; + readback_requested_ = false; raise_context("submitting Datoviz frame target", std::current_exception()); } in_flight_ = false; observing_ = false; + readback_requested_ = false; return result; } void discard_after_completion() { if (!in_flight_) throw std::logic_error("Datoviz frame target has no pending frame"); in_flight_ = false; observing_ = false; + readback_requested_ = false; } [[nodiscard]] VkDevice device() const { return dvz_device_handle(dvz_gpu_ctx_device(gpu_context_)); @@ -362,6 +424,7 @@ public: if (recording_ && commands_ != nullptr) dvz_cmd_reset(commands_); recording_ = false; observing_ = false; + readback_requested_ = false; } private: void initialize_timestamps(DvzDevice* device, DvzQueue* queue) noexcept { @@ -434,12 +497,14 @@ private: ? std::numeric_limits::max() : static_cast(nanoseconds); }; - return Datoviz_Gpu_Timing{ + auto timing = Datoviz_Gpu_Timing{ elapsed(timestamps[0], timestamps[1]), elapsed(timestamps[1], timestamps[2]), elapsed(timestamps[2], timestamps[3]), elapsed(timestamps[0], timestamps[3]) }; + if (!readback_requested_) timing.copy_ns = 0; + return timing; } void destroy() { if (in_flight_) @@ -498,6 +563,7 @@ private: bool recording_{}; bool in_flight_{}; bool observing_{}; + bool readback_requested_{}; bool timestamps_initialized_{}; bool timestamps_supported_{}; }; @@ -505,14 +571,10 @@ Datoviz_Visual_Backend::Datoviz_Visual_Backend( std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, const Scene_3D_Parameters& initial_scene) : domain_thread_(std::this_thread::get_id()) { try { - DvzGpuCtxConfig configuration = dvz_gpu_ctx_config(); - dvz_gpu_ctx_config_validation(&configuration, validation_enabled); - dvz_gpu_ctx_config_gpu(&configuration, gpu_index); - dvz_gpu_ctx_config_enable_canvas_extensions(&configuration, false); - gpu_context_ = dvz_gpu_ctx(&configuration); - if (gpu_context_ == nullptr) throw std::runtime_error("failed to create Datoviz GPU context"); + render_context_ = Datoviz_Render_Context::acquire(gpu_index, validation_enabled); + auto* gpu_context = render_context_->gpu_context(); DvzDrp2RuntimeConfig runtime_configuration = dvz_drp2_runtime_vklite_config( - dvz_gpu_ctx_device(gpu_context_), dvz_gpu_ctx_alloc(gpu_context_)); + dvz_gpu_ctx_device(gpu_context), dvz_gpu_ctx_alloc(gpu_context)); runtime_ = dvz_drp2_runtime_vklite(&runtime_configuration); if (runtime_ == nullptr) throw std::runtime_error("failed to create Datoviz DRP2 runtime"); create_scene(visual_family, initial_scene); @@ -725,6 +787,8 @@ void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_P } void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepared_Visual& point) { require_domain(); + if (!point.data) throw std::logic_error("3D prepared visual has no immutable payload"); + const auto& data = *point.data; if (target_extent_ != scene.viewport) { if (dvz_figure_resize(figure_, scene.viewport.width, scene.viewport.height) != DVZ_OK) @@ -753,23 +817,23 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa if (dvz_visual_set_transform(visual_, transform) != DVZ_OK || dvz_visual_set_depth_test(visual_, point.depth_test) != DVZ_OK || dvz_visual_set_visible( - visual_, point.visible && !point.positions.empty()) != DVZ_OK) + visual_, point.visible && !data.positions.empty()) != DVZ_OK) throw std::runtime_error("failed to apply Datoviz visual state"); } - if (point.positions.empty()) { + if (data.positions.empty()) { if (dvz_visual_set_visible(visual_, false) != DVZ_OK) throw std::runtime_error("failed to hide empty Datoviz point visual"); applied_visual_revision_ = point.revision; return; } - const auto count = static_cast(point.positions.size()); + const auto count = static_cast(data.positions.size()); DvzResult result = DVZ_OK; switch (point.family) { case Visual_Family::point: { const std::array updates{ { - {"position", point.positions.data(), count}, - {"color", point.colors.data(), count}, - {"diameter_px", point.sizes.data(), count} + {"position", data.positions.data(), count}, + {"color", data.colors.data(), count}, + {"diameter_px", data.sizes.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 3); @@ -778,10 +842,10 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::splat: { const std::array updates{ { - {"position", point.positions.data(), count}, - {"color", point.colors.data(), count}, - {"sigma", point.sigma.data(), count}, - {"angle", point.angles.data(), count} + {"position", data.positions.data(), count}, + {"color", data.colors.data(), count}, + {"sigma", data.sigma.data(), count}, + {"angle", data.angles.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 4); @@ -790,9 +854,9 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::pixel: { const std::array updates{ { - {"position", point.positions.data(), count}, - {"color", point.colors.data(), count}, - {"pixel_size_px", point.sizes.data(), count} + {"position", data.positions.data(), count}, + {"color", data.colors.data(), count}, + {"pixel_size_px", data.sizes.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 3); @@ -801,11 +865,11 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::marker: { const std::array updates{ { - {"position", point.positions.data(), count}, - {"color", point.colors.data(), count}, - {"diameter_px", point.sizes.data(), count}, - {"angle", point.angles.data(), count}, - {"shape", point.shapes.data(), count} + {"position", data.positions.data(), count}, + {"color", data.colors.data(), count}, + {"diameter_px", data.sizes.data(), count}, + {"angle", data.angles.data(), count}, + {"shape", data.shapes.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 5); @@ -814,9 +878,9 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::sphere: { const std::array updates{ { - {"position", point.positions.data(), count}, - {"color", point.colors.data(), count}, - {"radius", point.sizes.data(), count} + {"position", data.positions.data(), count}, + {"color", data.colors.data(), count}, + {"radius", data.sizes.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 3); @@ -825,10 +889,10 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::segment: { const std::array updates{ { - {"position_start", point.positions.data(), count}, - {"position_end", point.secondary_positions.data(), count}, - {"color", point.colors.data(), count}, - {"stroke_width_px", point.sizes.data(), count} + {"position_start", data.positions.data(), count}, + {"position_end", data.secondary_positions.data(), count}, + {"color", data.colors.data(), count}, + {"stroke_width_px", data.sizes.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 4); @@ -837,10 +901,10 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::vector: { const std::array updates{ { - {"position", point.positions.data(), count}, - {"vector", point.secondary_positions.data(), count}, - {"color", point.colors.data(), count}, - {"stroke_width_px", point.sizes.data(), count} + {"position", data.positions.data(), count}, + {"vector", data.secondary_positions.data(), count}, + {"color", data.colors.data(), count}, + {"stroke_width_px", data.sizes.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 4); @@ -850,9 +914,9 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::mesh: { const std::array updates{ { - {"position", point.positions.data(), count}, - {"color", point.colors.data(), count}, - {"normal", point.normals.data(), count} + {"position", data.positions.data(), count}, + {"color", data.colors.data(), count}, + {"normal", data.normals.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 3); @@ -861,9 +925,9 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::path: { const std::array updates{ { - {"position", point.positions.data(), count}, - {"color", point.colors.data(), count}, - {"stroke_width_px", point.sizes.data(), count} + {"position", data.positions.data(), count}, + {"color", data.colors.data(), count}, + {"stroke_width_px", data.sizes.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 3); @@ -873,8 +937,8 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::labels: { const std::array updates{ { - {"position", point.positions.data(), count}, - {"extent", point.extents.data(), count} + {"position", data.positions.data(), count}, + {"extent", data.extents.data(), count} } }; result = dvz_visual_set_data_many(visual_, updates.data(), 2); @@ -966,7 +1030,7 @@ DvzSceneFrameArtifact* Datoviz_Visual_Backend::emit( } std::optional Datoviz_Visual_Backend::submit( const Scene_3D_Parameters& scene, const Prepared_Visual& point, - std::uint64_t frame_sequence, bool observe) { + std::uint64_t frame_sequence, bool observe, bool readback) { require_domain(); if (scene.viewport.empty()) return std::nullopt; Datoviz_Frame_Trace trace; @@ -978,10 +1042,10 @@ std::optional Datoviz_Visual_Backend::sub if (target_ == nullptr || target_extent_ != scene.viewport) { target_.reset(); target_extent_ = scene.viewport; - target_ = std::make_unique(gpu_context_, target_extent_, + target_ = std::make_unique(render_context_->gpu_context(), target_extent_, ++target_generation_); } - target_->begin(observe); + target_->begin(observe, readback); DvzSceneFrameArtifact* artifact{}; try { if (observe) phase_started = trace_now_ns(); @@ -1095,10 +1159,7 @@ void Datoviz_Visual_Backend::destroy() { dvz_scene_destroy(scene_); scene_ = nullptr; } - if (gpu_context_ != nullptr) { - dvz_gpu_ctx_destroy(gpu_context_); - gpu_context_ = nullptr; - } + render_context_.reset(); } } // namespace aethera::render_3d::detail diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp index 6fcea06..a1e07a7 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp @@ -14,6 +14,7 @@ #include #include namespace aethera::render_3d::detail { +class Datoviz_Render_Context; class Datoviz_Visual_Backend final { public: struct Pending_Frame { @@ -36,7 +37,7 @@ public: Datoviz_Visual_Backend& operator=(const Datoviz_Visual_Backend&) = delete; [[nodiscard]] std::optional submit( const Scene_3D_Parameters& scene, const Prepared_Visual& visual, - std::uint64_t frame_sequence, bool observe); + std::uint64_t frame_sequence, bool observe, bool readback); [[nodiscard]] Completed_Frame collect(Pending_Frame pending); void discard(Pending_Frame pending); void dispatch_pointer(Event_Type type, float x, float y, @@ -55,7 +56,7 @@ private: [[nodiscard]] DvzSceneFrameArtifact* emit(const Scene_3D_Parameters& scene); void destroy(); std::thread::id domain_thread_; /* 唯一允许访问 Datoviz 对象的线程。 */ - DvzGpuCtx* gpu_context_{}; /* Datoviz GPU 上下文;由本类拥有。 */ + std::shared_ptr render_context_; /* 同一 GPU 上所有后端共享的 Device 与分配器。 */ DvzDrp2Runtime* runtime_{}; /* DRP2 Vulkan 运行时;由本类拥有。 */ DvzScene* scene_{}; /* Datoviz Scene;由本类拥有。 */ DvzFigure* figure_{}; /* 当前离屏 Figure。 */ diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp index 8dfa1f5..6538dd5 100644 --- a/render_3D/render_3D/detail/Render_Domain.cpp +++ b/render_3D/render_3D/detail/Render_Domain.cpp @@ -144,58 +144,6 @@ Render_Domain::Try_Post_Result Render_Domain::try_post(std::function fun raise_context("trying to post render domain task", std::current_exception()); } } -Render_Domain::Try_Post_Result Render_Domain::try_post_frame(Frame_Task function, Exception_Handler on_exception) { - if (!function) throw std::invalid_argument("render domain frame task is empty"); - if (!on_exception) throw std::invalid_argument("render domain frame exception handler is empty"); - if (stopping_.load(std::memory_order_acquire)) return Try_Post_Result::stopping; - auto task = std::make_shared(Frame_Task_Entry{std::move(function), std::move(on_exception)}); - { - std::lock_guard lock(frame_task_mutex_); - if (frame_task_in_flight_) { - if (frame_tasks_.size() >= static_cast(default_capacity - 1)) return Try_Post_Result::queue_full; - frame_tasks_.push_back(std::move(task)); - return Try_Post_Result::queued; - } - frame_task_in_flight_ = true; - } - const auto queued = try_post([this, task] { run_frame_task(task); }, [this, task](std::exception_ptr exception) { - task->on_exception(std::move(exception)); - complete_frame_task(); - }); - if (queued != Try_Post_Result::queued) { - std::lock_guard lock(frame_task_mutex_); - frame_task_in_flight_ = false; - } - return queued; -} -void Render_Domain::run_frame_task(std::shared_ptr task) { - auto completed = std::make_shared(); - auto completion = [this, completed] { - if (completed->exchange(true, std::memory_order_acq_rel)) return; - complete_frame_task(); - }; - try { - std::invoke(task->function, completion); - } - catch (...) { - task->on_exception(contextual_exception("executing render domain frame task", std::current_exception())); - completion(); - } -} -void Render_Domain::complete_frame_task() { - if (current_domain_ != this) throw std::logic_error("render domain frame completion must run on its domain"); - std::shared_ptr next; - { - std::lock_guard lock(frame_task_mutex_); - if (frame_tasks_.empty()) { - frame_task_in_flight_ = false; - return; - } - next = std::move(frame_tasks_.front()); - frame_tasks_.pop_front(); - } - run_frame_task(std::move(next)); -} Render_Domain::Admission_Result Render_Domain::post(Prepared_Task task) { if (!task.task_ || task.domain_.get() != this) throw std::logic_error("render domain prepared task is invalid"); if (stopping_.load(std::memory_order_acquire)) return Admission_Result::stopping; diff --git a/render_3D/render_3D/detail/Render_Domain.hpp b/render_3D/render_3D/detail/Render_Domain.hpp index f127559..f567a19 100644 --- a/render_3D/render_3D/detail/Render_Domain.hpp +++ b/render_3D/render_3D/detail/Render_Domain.hpp @@ -66,8 +66,6 @@ public: Render_Domain(const Render_Domain&) = delete; Render_Domain& operator=(const Render_Domain&) = delete; using Exception_Handler = std::function; - using Frame_Completion = std::function; - using Frame_Task = std::function; [[nodiscard]] Prepare_Result prepare(std::function function, Exception_Handler on_exception); [[nodiscard]] Admission_Result post(std::function function, @@ -76,9 +74,6 @@ public: /* 无等待入队;Paint Taskflow 节点只允许使用此入口。 */ [[nodiscard]] Try_Post_Result try_post(std::function function, Exception_Handler on_exception); - /* 无等待登记完整 GPU 帧;同一 Render Domain 只允许一帧处于提交至回读生命周期。 */ - [[nodiscard]] Try_Post_Result try_post_frame(Frame_Task function, - Exception_Handler on_exception); [[nodiscard]] Admission_Result post(Prepared_Task task); template struct Invoke_Result { @@ -132,16 +127,10 @@ public: } [[nodiscard]] Statistics statistics() const noexcept; private: - struct Frame_Task_Entry { - Frame_Task function; /* 启动一帧并接收其异步完成出口。 */ - Exception_Handler on_exception; /* 帧任务 Unknown Failure 的隔离回调。 */ - }; Render_Domain(); static void destroy(Render_Domain* domain) noexcept; void request_stop() noexcept; static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept; - void run_frame_task(std::shared_ptr task); - void complete_frame_task(); void acquire_admission(); [[nodiscard]] bool try_acquire_admission() noexcept; void release_admission() noexcept; @@ -152,9 +141,6 @@ private: std::mutex task_mutex_; /* 保护有界准入后的任务队列。 */ std::condition_variable task_condition_; /* 新任务或停止哨兵的唤醒源。 */ std::deque> tasks_; /* Render Domain 单消费者任务队列。 */ - std::mutex frame_task_mutex_; /* 保护跨后端的完整 GPU 帧队列。 */ - std::deque> frame_tasks_; /* 等待前一帧完成回读的完整 GPU 帧。 */ - bool frame_task_in_flight_{}; /* 是否已有一帧占用同 GPU 的 Datoviz 生命周期。 */ std::atomic_size_t admitted_{}; /* 已获准任务数。 */ std::atomic_size_t peak_admitted_{}; /* 历史最大获准任务数。 */ std::atomic_size_t queued_{}; /* 当前队列任务数。 */ diff --git a/render_3D/render_3D/visual/Basic_Visual.ipp b/render_3D/render_3D/visual/Basic_Visual.ipp index 1d0b8c1..32c8e2c 100644 --- a/render_3D/render_3D/visual/Basic_Visual.ipp +++ b/render_3D/render_3D/visual/Basic_Visual.ipp @@ -41,7 +41,7 @@ template bool Basic_Visual::Prop::operator==(const Prop&) template bool Basic_Visual::State::operator==(const State&) const = default; template bool Basic_Visual::Private::valid_items(const std::vector& items) { return std::ranges::all_of(items, [](const Item& item) { return Spec::valid(item); }); } template void Basic_Visual::Private::bind_paint_target(std::shared_ptr context, Enqueue_Paint enqueue) { paint_target = {std::move(context), enqueue}; } -template template void Basic_Visual::Private::prepare_data(Object* object) { using Visual_Tag = typename Basic_Visual::Base_Tag; const auto& prop = object->template read_prop(); if (!valid_items(prop.items) || !detail::finite(prop.transform)) throw std::invalid_argument("3D visual property contains invalid data"); auto& output = object->template pending_buffer(); output = {}; output.family = Spec::family; output.transform = prop.transform; output.visible = prop.visible; output.depth_test = prop.depth_test; output.revision = next_revision++; if constexpr (requires { prop.style; }) output.point_style = prop.style; Spec::prepare(prop.items, output); object->template update_state<&State::prepared_item_count, &State::prepared_revision>([&](State_Access states) { auto& state = states.template get(); state.prepared_item_count = output.positions.size(); state.prepared_revision = output.revision; }); } +template template void Basic_Visual::Private::prepare_data(Object* object) { using Visual_Tag = typename Basic_Visual::Base_Tag; const auto& prop = object->template read_prop(); if (!valid_items(prop.items) || !detail::finite(prop.transform)) throw std::invalid_argument("3D visual property contains invalid data"); auto data = std::make_shared(); Spec::prepare(prop.items, *data); auto& output = object->template pending_buffer(); output = {}; output.family = Spec::family; output.transform = prop.transform; output.visible = prop.visible; output.depth_test = prop.depth_test; output.revision = next_revision++; output.data = std::move(data); if constexpr (requires { prop.style; }) output.point_style = prop.style; object->template update_state<&State::prepared_item_count, &State::prepared_revision>([&](State_Access states) { auto& state = states.template get(); state.prepared_item_count = prop.items.size(); state.prepared_revision = output.revision; }); } template template tf::Taskflow Basic_Visual::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { if (auto context = paint_target.context.lock(); context && paint_target.enqueue) { const auto& pending = object->template pending_buffer(); const auto& current = object->template current_buffer(); paint_target.enqueue(context.get(), pending.revision >= current.revision ? pending : current); } }).name("render_3d.paint.enqueue"); return graph; } template template bool Basic_Visual::Private::should_paint(Object*, const State&, bool) { return paint_target.enqueue != nullptr && !paint_target.context.expired(); } template template void Basic_Visual::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } diff --git a/render_3D/render_3D/visual/Prepared_Visual.hpp b/render_3D/render_3D/visual/Prepared_Visual.hpp index 1848280..4d1670c 100644 --- a/render_3D/render_3D/visual/Prepared_Visual.hpp +++ b/render_3D/render_3D/visual/Prepared_Visual.hpp @@ -1,14 +1,9 @@ #pragma once #include "../base/Types.hpp" +#include namespace aethera::render_3d::detail { struct Prepared_Visual_Tag {}; -struct Prepared_Visual { - Visual_Family family{Visual_Family::point}; /* Datoviz Visual 类型。 */ - Matrix4 transform{}; /* 本帧提交的对象变换。 */ - Point_Style point_style{}; /* point family 使用的点样式。 */ - bool visible{true}; /* 本帧是否绘制。 */ - bool depth_test{true}; /* 本帧是否启用深度测试。 */ - std::uint64_t revision{}; /* 数据或样式改变时递增的提交版本。 */ +struct Prepared_Visual_Data { std::vector> positions{}; /* 主位置字段。 */ std::vector> colors{}; /* RGBA8 颜色字段。 */ std::vector sizes{}; /* 点径、半径或线宽字段。 */ @@ -19,4 +14,13 @@ struct Prepared_Visual { std::vector> normals{}; /* Primitive、Mesh 法线字段。 */ std::vector> extents{}; /* Image、Labels 尺寸字段。 */ }; +struct Prepared_Visual { + Visual_Family family{Visual_Family::point}; /* Datoviz Visual 类型。 */ + Matrix4 transform{}; /* 本帧提交的对象变换。 */ + Point_Style point_style{}; /* point family 使用的点样式。 */ + bool visible{true}; /* 本帧是否绘制。 */ + bool depth_test{true}; /* 本帧是否启用深度测试。 */ + std::uint64_t revision{}; /* 数据或样式改变时递增的提交版本。 */ + std::shared_ptr data{}; /* Prepare 发布的不可变 GPU 字段;异步提交只共享所有权。 */ +}; } diff --git a/render_3D/render_3D/visual/Visuals.hpp b/render_3D/render_3D/visual/Visuals.hpp index d707881..f530dbd 100644 --- a/render_3D/render_3D/visual/Visuals.hpp +++ b/render_3D/render_3D/visual/Visuals.hpp @@ -111,7 +111,7 @@ struct Name##_Spec { \ using Settings = Settings_Type; \ static constexpr Visual_Family family = Visual_Family::Family_Value; \ [[nodiscard]] static bool valid(const Item& item) noexcept; \ - static void prepare(const std::vector& items, Prepared_Visual& output); \ + static void prepare(const std::vector& items, Prepared_Visual_Data& output); \ } AETHERA_VISUAL_SPEC(Point, Point, Point_Settings, point); AETHERA_VISUAL_SPEC(Splat, Splat, Visual_Settings, splat); diff --git a/render_3D/render_3D/visual/Visuals.ipp b/render_3D/render_3D/visual/Visuals.ipp index 0777236..f2c287b 100644 --- a/render_3D/render_3D/visual/Visuals.ipp +++ b/render_3D/render_3D/visual/Visuals.ipp @@ -1,35 +1,35 @@ #pragma once namespace aethera::render_3d::detail { inline std::array channels(Color value) { return {value.red, value.green, value.blue, value.alpha}; } -inline void reserve_common(Prepared_Visual& output, std::size_t count) { output.positions.reserve(count); output.colors.reserve(count); output.sizes.reserve(count); } +inline void reserve_common(Prepared_Visual_Data& output, std::size_t count) { output.positions.reserve(count); output.colors.reserve(count); output.sizes.reserve(count); } inline bool Point_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.diameter_px) && item.diameter_px > 0.0F; } -inline void Point_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.diameter_px); } } +inline void Point_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.diameter_px); } } inline bool Splat_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.sigma) && item.sigma.x > 0.0F && item.sigma.y > 0.0F && finite(item.angle); } -inline void Splat_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); output.sigma.reserve(items.size()); output.angles.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sigma.push_back({item.sigma.x, item.sigma.y}); output.angles.push_back(item.angle); } } +inline void Splat_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); output.sigma.reserve(items.size()); output.angles.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sigma.push_back({item.sigma.x, item.sigma.y}); output.angles.push_back(item.angle); } } inline bool Pixel_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.size_px) && item.size_px > 0.0F; } -inline void Pixel_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.size_px); } } +inline void Pixel_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.size_px); } } inline bool Marker_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.diameter_px) && item.diameter_px > 0.0F && finite(item.angle); } -inline void Marker_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); output.angles.reserve(items.size()); output.shapes.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.diameter_px); output.angles.push_back(item.angle); output.shapes.push_back(static_cast(item.shape)); } } +inline void Marker_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); output.angles.reserve(items.size()); output.shapes.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.diameter_px); output.angles.push_back(item.angle); output.shapes.push_back(static_cast(item.shape)); } } inline bool Sphere_Spec::valid(const Item& item) noexcept { return finite(item.center) && finite(item.radius) && item.radius > 0.0F; } -inline void Sphere_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); for (const auto& item : items) { output.positions.push_back({item.center.x, item.center.y, item.center.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.radius); } } +inline void Sphere_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); for (const auto& item : items) { output.positions.push_back({item.center.x, item.center.y, item.center.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.radius); } } inline bool Segment_Spec::valid(const Item& item) noexcept { return finite(item.start) && finite(item.end) && finite(item.width_px) && item.width_px > 0.0F; } -inline void Segment_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); output.secondary_positions.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.start.x, item.start.y, item.start.z}); output.secondary_positions.push_back({item.end.x, item.end.y, item.end.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.width_px); } } +inline void Segment_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); output.secondary_positions.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.start.x, item.start.y, item.start.z}); output.secondary_positions.push_back({item.end.x, item.end.y, item.end.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.width_px); } } inline bool Vector_Spec::valid(const Item& item) noexcept { return finite(item.origin) && finite(item.direction) && finite(item.width_px) && item.width_px > 0.0F; } -inline void Vector_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); output.secondary_positions.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.origin.x, item.origin.y, item.origin.z}); output.secondary_positions.push_back({item.direction.x, item.direction.y, item.direction.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.width_px); } } +inline void Vector_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); output.secondary_positions.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.origin.x, item.origin.y, item.origin.z}); output.secondary_positions.push_back({item.direction.x, item.direction.y, item.direction.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.width_px); } } inline bool Primitive_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.normal); } -inline void Primitive_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); output.normals.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.normals.push_back({item.normal.x, item.normal.y, item.normal.z}); } } +inline void Primitive_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); output.normals.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.normals.push_back({item.normal.x, item.normal.y, item.normal.z}); } } inline bool Mesh_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.normal) && finite(item.texture_coordinate); } -inline void Mesh_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); output.normals.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.normals.push_back({item.normal.x, item.normal.y, item.normal.z}); } } +inline void Mesh_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); output.normals.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.normals.push_back({item.normal.x, item.normal.y, item.normal.z}); } } inline bool Path_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.width_px) && item.width_px > 0.0F; } -inline void Path_Spec::prepare(const std::vector& items, Prepared_Visual& output) { reserve_common(output, items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.width_px); } } +inline void Path_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { reserve_common(output, items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.width_px); } } inline bool Image_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.extent) && item.extent.x > 0.0F && item.extent.y > 0.0F && finite(item.texture_rectangle); } -inline void Image_Spec::prepare(const std::vector& items, Prepared_Visual& output) { output.positions.reserve(items.size()); output.extents.reserve(items.size()); output.colors.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.extents.push_back({item.extent.x, item.extent.y}); output.colors.push_back(channels(item.tint)); } } +inline void Image_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { output.positions.reserve(items.size()); output.extents.reserve(items.size()); output.colors.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.extents.push_back({item.extent.x, item.extent.y}); output.colors.push_back(channels(item.tint)); } } inline bool Labels_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.extent) && item.extent.x > 0.0F && item.extent.y > 0.0F; } -inline void Labels_Spec::prepare(const std::vector& items, Prepared_Visual& output) { output.positions.reserve(items.size()); output.extents.reserve(items.size()); output.colors.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.extents.push_back({item.extent.x, item.extent.y}); output.colors.push_back(channels(item.tint)); } } +inline void Labels_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { output.positions.reserve(items.size()); output.extents.reserve(items.size()); output.colors.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.extents.push_back({item.extent.x, item.extent.y}); output.colors.push_back(channels(item.tint)); } } inline bool Glyph_Spec::valid(const Item& item) noexcept { return finite(item.position) && finite(item.bounds) && finite(item.texture_coordinates) && finite(item.angle); } -inline void Glyph_Spec::prepare(const std::vector& items, Prepared_Visual& output) { output.positions.reserve(items.size()); output.colors.reserve(items.size()); output.angles.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.angles.push_back(item.angle); } } +inline void Glyph_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { output.positions.reserve(items.size()); output.colors.reserve(items.size()); output.angles.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.angles.push_back(item.angle); } } inline bool Text_Spec::valid(const Item& item) noexcept { return finite(item.position) && !item.text.empty() && finite(item.size_px) && item.size_px > 0.0F; } -inline void Text_Spec::prepare(const std::vector& items, Prepared_Visual& output) { output.positions.reserve(items.size()); output.colors.reserve(items.size()); output.sizes.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.size_px); } } +inline void Text_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { output.positions.reserve(items.size()); output.colors.reserve(items.size()); output.sizes.reserve(items.size()); for (const auto& item : items) { output.positions.push_back({item.position.x, item.position.y, item.position.z}); output.colors.push_back(channels(item.color)); output.sizes.push_back(item.size_px); } } inline bool Volume_Spec::valid(const Item& item) noexcept { return finite(item.value); } -inline void Volume_Spec::prepare(const std::vector& items, Prepared_Visual& output) { output.sizes.reserve(items.size()); for (const auto& item : items) output.sizes.push_back(item.value); } +inline void Volume_Spec::prepare(const std::vector& items, Prepared_Visual_Data& output) { output.sizes.reserve(items.size()); for (const auto& item : items) output.sizes.push_back(item.value); } } diff --git a/web_server/src/Graph_WebSocket.cpp b/web_server/src/Graph_WebSocket.cpp index 6cde83e..0158d25 100644 --- a/web_server/src/Graph_WebSocket.cpp +++ b/web_server/src/Graph_WebSocket.cpp @@ -18,7 +18,7 @@ struct Graph_WebSocket::Private { }; Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr plot) : d(std::make_unique()) { d->connection = connection; d->plot = std::move(plot); d->owner = connection.get(); } Graph_WebSocket::~Graph_WebSocket() { close(); } -void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->plot->attach(d->owner, [weak](std::shared_ptr frame) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (!connection || !connection->connected()) return; connection->send(frame->metadata, drogon::WebSocketMessageType::Text); connection->send(frame->pixels.data(), frame->pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; } +void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->plot->attach(d->owner, [weak](std::shared_ptr frame) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (!connection || !connection->connected()) return; connection->send(frame->metadata, drogon::WebSocketMessageType::Text); if (!frame->pixels.empty()) connection->send(frame->pixels.data(), frame->pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; } void Graph_WebSocket::receive(std::string_view message) { const auto json = nlohmann::json::parse(message, nullptr, false); if (json.is_discarded() || !json.is_object()) return; @@ -31,6 +31,8 @@ void Graph_WebSocket::receive(std::string_view message) { request.correlation_id = value->get(); if (const auto value = json.find("time"); value != json.end() && value->is_number()) request.time_milliseconds = value->get(); + if (const auto value = json.find("delivery"); value != json.end() && value->is_string()) + request.delivery = *value == "diagnostics" ? Plot_Frame_Delivery::diagnostics : Plot_Frame_Delivery::pixels; if (const auto viewport = json.find("viewport"); viewport != json.end() && viewport->is_object()) { if (const auto value = viewport->find("width"); value != viewport->end() && value->is_number_unsigned()) request.width = std::clamp(value->get(), 160U, 1920U); diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index 9cf8f73..e126bb0 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -33,7 +33,7 @@ using Frequency_Axis_Object = Impl; using Numeric_Axis_Object = Impl; using Time_Axis_Object = Impl; using Selection_Object = Impl; -constexpr std::uint16_t frame_protocol_version{4}; +constexpr std::uint16_t frame_protocol_version{5}; enum class Frame_Pacing_Mode : std::uint32_t { manual, fixed_rate, @@ -70,6 +70,13 @@ std::optional parse_pacing_mode(std::string_view value) { if (value == "maximum_rate") return Frame_Pacing_Mode::maximum_rate; return std::nullopt; } +std::string_view delivery_name(Plot_Frame_Delivery delivery) { + switch (delivery) { + case Plot_Frame_Delivery::pixels: return "pixels"; + case Plot_Frame_Delivery::diagnostics: return "diagnostics"; + } + throw std::logic_error("unknown plot frame delivery"); +} Frame_Pacing_Properties Frame_Policy::snapshot() const { std::lock_guard lock(mutex); return pacing; @@ -107,7 +114,7 @@ nlohmann::json Frame_Policy::write_prop(std::string_view key, const nlohmann::js } return {{"success", false}, {"error", "unknown frame runtime property"}}; } -nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, std::uint32_t height, std::size_t byte_length, const Frame_Pacing_Properties& pacing) { +nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, std::uint32_t height, std::size_t byte_length, Plot_Frame_Delivery delivery, const Frame_Pacing_Properties& pacing) { nlohmann::json markers = nlohmann::json::object(); for (const auto& point : frame.trace_points()) markers[std::string(magic_enum::enum_name(point.marker))] = point.elapsed_ns; nlohmann::json measurements = nlohmann::json::object(); @@ -116,42 +123,44 @@ nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, std::uin return { {"kind", "frame_metadata"}, {"protocol", "aethera.frame"}, {"version", frame_protocol_version}, {"sequence", identity.sequence}, {"correlation_id", identity.correlation_id}, - {"created_time_unix_ms", static_cast(frame.created_time_unix_ns()) / 1'000'000.0}, + {"created_time_unix_ms", static_cast(frame.created_time_unix_ns()) / 1'000'000.0}, {"delivery", delivery_name(delivery)}, {"pixel", {{"width", width}, {"height", height}, {"format", "rgba8"}, {"byte_length", byte_length}}}, {"pacing", {{"mode", pacing_mode_name(pacing.mode)}, {"fixed_rate_fps", pacing.fixed_rate_fps}, {"minimum_latency_headroom", pacing.minimum_latency_headroom}}}, {"trace", {{"clock", "steady_elapsed_ns"}, {"markers", std::move(markers)}, {"measurements", std::move(measurements)}}} }; } -std::shared_ptr encode_frame(Frame_2D* frame, const Frame_Pacing_Properties& pacing) { +std::shared_ptr encode_frame(Frame_2D* frame, Plot_Frame_Delivery delivery, const Frame_Pacing_Properties& pacing) { frame->mark(Frame_Trace_Marker::websocket_publish_started); const Image_View image = frame->image(); std::string output; - output.reserve(static_cast(image.width) * image.height * 4); - for (int y = 0; y < image.height; ++y) { - const auto* row = reinterpret_cast( - image.data + static_cast(y) * image.stride); - for (int x = 0; x < image.width; ++x) { - const auto* pixel = row + x * 4; - output.push_back(static_cast(pixel[2])); - output.push_back(static_cast(pixel[1])); - output.push_back(static_cast(pixel[0])); - output.push_back(static_cast(pixel[3])); + if (delivery == Plot_Frame_Delivery::pixels) { + output.reserve(static_cast(image.width) * image.height * 4); + for (int y = 0; y < image.height; ++y) { + const auto* row = reinterpret_cast( + image.data + static_cast(y) * image.stride); + for (int x = 0; x < image.width; ++x) { + const auto* pixel = row + x * 4; + output.push_back(static_cast(pixel[2])); + output.push_back(static_cast(pixel[1])); + output.push_back(static_cast(pixel[0])); + output.push_back(static_cast(pixel[3])); + } } } frame->mark(Frame_Trace_Marker::websocket_publish_finished); auto message = std::make_shared(); message->pixels = std::move(output); - message->metadata = frame_metadata(*frame, static_cast(image.width), static_cast(image.height), message->pixels.size(), pacing).dump(); + message->metadata = frame_metadata(*frame, static_cast(image.width), static_cast(image.height), message->pixels.size(), delivery, pacing).dump(); return message; } -std::shared_ptr encode_frame(Frame_3D* frame, const Frame_Pacing_Properties& pacing) { +std::shared_ptr encode_frame(Frame_3D* frame, Plot_Frame_Delivery delivery, const Frame_Pacing_Properties& pacing) { frame->mark(Frame_Trace_Marker::websocket_publish_started); const auto pixels = frame->pixels(); auto message = std::make_shared(); - message->pixels.assign(reinterpret_cast(pixels.data()), pixels.size()); + if (delivery == Plot_Frame_Delivery::pixels) message->pixels.assign(reinterpret_cast(pixels.data()), pixels.size()); frame->mark(Frame_Trace_Marker::websocket_publish_finished); const auto extent = frame->extent(); - message->metadata = frame_metadata(*frame, extent.width, extent.height, message->pixels.size(), pacing).dump(); + message->metadata = frame_metadata(*frame, extent.width, extent.height, message->pixels.size(), delivery, pacing).dump(); return message; } struct Schema_Query { @@ -273,7 +282,8 @@ nlohmann::json Plot::Private::schema() const { Plot::Private::Managed_Frame Plot::Private::make_frame(Frame_Submission submission) { const Frame_Identity identity{next_frame_sequence++, submission.request.correlation_id}; if (std::holds_alternative>(scene)) return {submission.owner, std::move(submission.request), std::make_unique(identity)}; - return {submission.owner, std::move(submission.request), std::make_unique(identity)}; + const auto output = submission.request.delivery == Plot_Frame_Delivery::pixels ? Frame_3D_Output::pixels : Frame_3D_Output::diagnostics; + return {submission.owner, std::move(submission.request), std::make_unique(identity, output)}; } void Plot::Private::request_frame(Frame_Submission submission) { auto frame = make_frame(std::move(submission)); @@ -316,8 +326,8 @@ void Plot::Private::render_frame(Managed_Frame frame) { void Plot::Private::publish_completed_frame(Render_Frame* frame) { if (!active_frame) throw std::logic_error("frame callback has no externally owned active frame"); const auto pacing = frame_policy.snapshot(); - if (auto* frame_2d = std::get_if>(&active_frame->frame); frame_2d && frame_2d->get() == frame) publish(active_frame->owner, encode_frame(frame_2d->get(), pacing)); - else if (auto* frame_3d = std::get_if>(&active_frame->frame); frame_3d && frame_3d->get() == frame) publish(active_frame->owner, encode_frame(frame_3d->get(), pacing)); + if (auto* frame_2d = std::get_if>(&active_frame->frame); frame_2d && frame_2d->get() == frame) publish(active_frame->owner, encode_frame(frame_2d->get(), active_frame->request.delivery, pacing)); + else if (auto* frame_3d = std::get_if>(&active_frame->frame); frame_3d && frame_3d->get() == frame) publish(active_frame->owner, encode_frame(frame_3d->get(), active_frame->request.delivery, pacing)); else throw std::logic_error("frame callback does not match the externally owned active frame"); frame_completed(); } diff --git a/web_server/src/Plot.hpp b/web_server/src/Plot.hpp index ada1eb8..dad385b 100644 --- a/web_server/src/Plot.hpp +++ b/web_server/src/Plot.hpp @@ -24,11 +24,16 @@ struct Plot_Input_Event { std::uint32_t native_key{}; bool auto_repeat{}; }; +enum class Plot_Frame_Delivery : std::uint8_t { + pixels, + diagnostics +}; struct Plot_Frame_Request { std::uint64_t correlation_id{}; /* 浏览器请求标识;完成帧元数据原样返回。 */ double time_milliseconds{}; /* 使用层推进图表时间轴的本地日内毫秒。 */ std::uint32_t width{720}; /* 请求帧宽度,单位为物理像素。 */ std::uint32_t height{420}; /* 请求帧高度,单位为物理像素。 */ + Plot_Frame_Delivery delivery{Plot_Frame_Delivery::pixels}; /* 本次请求返回完整像素或仅返回诊断元数据。 */ }; struct Plot_Frame_Message { std::string metadata{}; /* 单帧 JSON 元数据 WebSocket 文本消息。 */ diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 392f1c5..68328e9 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -25,18 +25,20 @@ type Schema = {protocol: "aethera.plot.inspector"; version: 2; components: Compo type State_Histories = Record; type Stream_Status = "CONNECTING" | "LIVE" | "OFFLINE"; type Frame_Pacing_Mode = "manual" | "fixed_rate" | "minimum_latency" | "maximum_rate"; -type Frame_Metadata = {kind: "frame_metadata"; protocol: "aethera.frame"; version: 4; sequence: number; correlation_id: number; +type Frame_Delivery = "pixels" | "diagnostics"; +type Frame_Metadata = {kind: "frame_metadata"; protocol: "aethera.frame"; version: 5; sequence: number; correlation_id: number; + delivery: Frame_Delivery; created_time_unix_ms: number; pixel: {width: number; height: number; format: "rgba8"; byte_length: number}; pacing: {mode: Frame_Pacing_Mode; fixed_rate_fps: number; minimum_latency_headroom: number}; trace: {clock: "steady_elapsed_ns"; markers: Record; measurements: Record}}; type Frame_Stage_Values = Record; -type Frame_Sample = {sequence: number; correlation_id: number; received_at_ms: number; values: Frame_Stage_Values}; +type Frame_Sample = {sequence: number; correlation_id: number; delivery: Frame_Delivery; received_at_ms: number; values: Frame_Stage_Values}; type Frame_Diagnostics = {metadata: Frame_Metadata; samples: Frame_Sample[]; frame_rate_fps: number; interval_jitter_p95_ms: number; request_to_pixels_average_ms: number; request_to_pixels_p50_ms: number; request_to_pixels_p95_ms: number; request_to_pixels_p99_ms: number; frame_interval_average_ms: number; frame_interval_p95_ms: number; dropped_sequence_count: number; latest: Frame_Stage_Values}; type Frame_Metrics = {sequence: number; generated_time_unix_ms: number; request_to_pixels_ms: number; average_request_to_pixels_ms: number; p95_request_to_pixels_ms: number; p99_request_to_pixels_ms: number; frame_rate_fps: number; p95_frame_interval_jitter_ms: number; - pacing_mode: Frame_Pacing_Mode; fixed_rate_fps: number; minimum_latency_headroom: number}; + pacing_mode: Frame_Pacing_Mode; fixed_rate_fps: number; minimum_latency_headroom: number; delivery: Frame_Delivery}; type Frame_Policy_Event = {plot_id: string; key: "pacing_mode" | "fixed_rate_fps" | "minimum_latency_headroom"; value: unknown}; type Stage_Statistic = "average" | "variability" | "p95" | "p99"; type Stage_Unit = "value" | "percentage"; @@ -51,8 +53,9 @@ function local_time_milliseconds() { function valid_frame_metadata(value: unknown): value is Frame_Metadata { if (!value || typeof value !== "object") return false; const frame = value as Partial; - return frame.kind === "frame_metadata" && frame.protocol === "aethera.frame" && frame.version === 4 + return frame.kind === "frame_metadata" && frame.protocol === "aethera.frame" && frame.version === 5 && typeof frame.sequence === "number" && typeof frame.correlation_id === "number" + && (frame.delivery === "pixels" || frame.delivery === "diagnostics") && Boolean(frame.pixel) && frame.pixel?.format === "rgba8" && Boolean(frame.trace); } @@ -225,12 +228,18 @@ function build_frame_diagnostics(metadata: Frame_Metadata, samples: Frame_Sample }; } -function use_plot_stream(plot: Plot, canvas_ref: React.RefObject) { +function use_plot_stream(plot: Plot, canvas_ref: React.RefObject, stream_pixels: boolean) { const [status, set_status] = useState("CONNECTING"); const [metrics, set_metrics] = useState(null); const socket_ref = useRef(null); const pending_pointer_move = useRef | null>(null); const viewport_ref = useRef({width: 720, height: 420}); + const stream_pixels_ref = useRef(stream_pixels); + + useEffect(() => { + stream_pixels_ref.current = stream_pixels; + window.dispatchEvent(new CustomEvent("aethera-frame-delivery", {detail: {plot_id: plot.id}})); + }, [plot.id, stream_pixels]); const envelope = useCallback((kind: "frame" | "input", event?: Record) => { const canvas = canvas_ref.current; @@ -280,6 +289,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject { if (timer) window.clearTimeout(timer); timer = 0; }; const clear_diagnostics_timer = () => { if (diagnostics_timer) window.clearTimeout(diagnostics_timer); diagnostics_timer = 0; }; const target_interval = () => { + if (!stream_pixels_ref.current) return plot.dimension === "2D" ? 500 : 1000; if (pacing.mode === "maximum_rate") return 0; if (pacing.mode === "fixed_rate") return 1000 / Math.max(0.1, pacing.fixed_rate_fps); if (pacing.mode === "minimum_latency") return latest_metrics @@ -289,7 +299,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject { clear_timer(); - if (stopped || pacing.mode === "manual") return; + if (stopped || (stream_pixels_ref.current && pacing.mode === "manual")) return; const due = immediate || previous_request_time === 0 ? performance.now() : previous_request_time + target_interval(); timer = window.setTimeout(() => request_frame(false), Math.max(0, due - performance.now())); }; @@ -310,7 +320,8 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject sample.delivery === latest_metadata!.delivery); + if (delivery_samples.length === 0) return; + const diagnostics = build_frame_diagnostics(latest_metadata, delivery_samples); const latest = diagnostics.latest; latest_metrics = { sequence: latest_metadata.sequence, @@ -348,7 +361,8 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject { + const started_at = request_started_at.get(pair.value.correlation_id); + if (started_at === undefined) return; + frame_pending = false; + request_started_at.delete(pair.value.correlation_id); + latest_metadata = pair.value; + synchronize_pacing(pair.value); + const sample_capacity = plot.dimension === "2D" ? 900 : 600; + samples = [...samples, { + sequence: pair.value.sequence, + correlation_id: pair.value.correlation_id, + delivery: pair.value.delivery, + received_at_ms: completed_at, + values: pipeline_stage_values(frame_stage_values(pair.value, started_at, pair.received_at, completed_at, canvas_upload_ms), plot.dimension) + }].slice(-sample_capacity); + const delivery_sample_count = samples.filter(sample => sample.delivery === pair.value.delivery).length; + publish_diagnostics(delivery_sample_count === 1); + if (presentation) mark_presentation_opportunity(pair.value.sequence, completed_at); + if (manual_frame_pending) request_frame(true); else schedule_next(); + }; const receive_pixels = (bytes: ArrayBuffer) => { const pair = pending_metadata; pending_metadata = null; const canvas = canvas_ref.current; if (!pair || !canvas) return; - const started_at = request_started_at.get(pair.value.correlation_id); - if (started_at === undefined) return; - frame_pending = false; - const pixels_received_at = performance.now(); + const completed_at = performance.now(); const canvas_upload_ms = draw_pixels(canvas, bytes, pair.value); - request_started_at.delete(pair.value.correlation_id); - if (canvas_upload_ms !== null) { - latest_metadata = pair.value; - synchronize_pacing(pair.value); - samples = [...samples, { - sequence: pair.value.sequence, - correlation_id: pair.value.correlation_id, - received_at_ms: pixels_received_at, - values: pipeline_stage_values(frame_stage_values(pair.value, started_at, pair.received_at, pixels_received_at, canvas_upload_ms), plot.dimension) - }].slice(-10_000); - publish_diagnostics(samples.length === 1); - mark_presentation_opportunity(pair.value.sequence, pixels_received_at); + if (canvas_upload_ms !== null) complete_frame(pair, completed_at, canvas_upload_ms, true); + else { + frame_pending = false; + request_started_at.delete(pair.value.correlation_id); + schedule_next(); } - if (manual_frame_pending) request_frame(true); else schedule_next(); }; const on_policy_change = (event: Event) => { const detail = (event as CustomEvent).detail; @@ -430,17 +453,26 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject { + const detail = (event as CustomEvent<{plot_id: string}>).detail; + if (detail?.plot_id === plot.id && !frame_pending) schedule_next(true); + }; window.addEventListener("aethera-frame-policy", on_policy_change); window.addEventListener("aethera-manual-frame", on_manual_frame); window.addEventListener("aethera-reset-camera", on_camera_reset); window.addEventListener("aethera-reset-frame-diagnostics", on_diagnostics_reset); + window.addEventListener("aethera-frame-delivery", on_delivery_change); socket.onopen = () => { frame_pending = false; pending_metadata = null; request_started_at.clear(); set_status("LIVE"); request_frame(true); }; socket.onclose = () => { clear_timer(); frame_pending = false; pending_metadata = null; request_started_at.clear(); if (!stopped) set_status("CONNECTING"); }; socket.onmessage = event => { if (typeof event.data === "string") { try { const decoded: unknown = JSON.parse(event.data); - if (valid_frame_metadata(decoded)) pending_metadata = {value: decoded, received_at: performance.now()}; + if (valid_frame_metadata(decoded)) { + const pair = {value: decoded, received_at: performance.now()}; + if (decoded.delivery === "diagnostics") { pending_metadata = null; complete_frame(pair, pair.received_at, 0, false); } + else pending_metadata = pair; + } } catch { return; } return; } @@ -457,6 +489,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject dimension === "2D" ? pipeline_2d_definitions : pipeline_3d_definitions; +const pipeline_definitions = (dimension: Plot["dimension"], delivery: Frame_Delivery) => { + const core = (dimension === "2D" ? pipeline_2d_definitions : pipeline_3d_definitions).map(definition => { + if (delivery === "pixels") return definition; + const [key] = definition; + if (key === "pipeline_2d_encode_ms") return [key, "2D 诊断编码", "跳过 BGRA 像素转换,仅生成二维诊断元数据的耗时。"] as Pipeline_Stage_Definition; + if (key === "pipeline_3d_gpu_copy_ms") return [key, "GPU 回读复制(跳过)", "诊断帧不复制像素到回读资源;该项应接近零。"] as Pipeline_Stage_Definition; + if (key === "pipeline_3d_readback_ms") return [key, "3D 完成收集", "收集 GPU 完成状态和时间戳、但不下载 RGBA 像素的耗时。"] as Pipeline_Stage_Definition; + if (key === "pipeline_3d_encode_ms") return [key, "3D 诊断编码", "跳过像素消息,仅生成三维诊断元数据的耗时。"] as Pipeline_Stage_Definition; + return definition; + }); + return [...core, ...(delivery === "pixels" ? pipeline_common_finish : [])]; +}; function diagnostic_value(value: number, key: string) { if (key === "payload_megabytes") return `${value.toFixed(2)} MiB`; @@ -761,18 +803,22 @@ function Frame_Timeline_Chart({diagnostics, dimension, paused, on_context_menu}: const chart = chart_ref.current; if (!chart) return; const visible_samples = diagnostics.samples; + const total_series: [string, string, string] = diagnostics.metadata.delivery === "pixels" + ? ["request_to_presentation_opportunity_ms", "完整像素流水线", "#5ce4c2"] + : ["request_to_pixels_ms", "诊断往返", "#5ce4c2"]; + const pixel_delivery = diagnostics.metadata.delivery === "pixels"; const series_keys: Array<[string, string, string]> = dimension === "2D" ? [ - ["request_to_presentation_opportunity_ms", "完整流水线", "#5ce4c2"], + total_series, ["pipeline_2d_prepare_ms", "2D Prepare", "#62a8ff"], ["pipeline_2d_paint_ms", "Blend2D 绘制", "#f4bd63"], - ["pipeline_payload_transport_ms", "像素传输", "#ff7d9c"], - ["pipeline_presentation_wait_ms", "呈现等待", "#b998ff"] + pixel_delivery ? ["pipeline_payload_transport_ms", "像素传输", "#ff7d9c"] : ["pipeline_2d_encode_ms", "诊断编码", "#ff7d9c"], + pixel_delivery ? ["pipeline_presentation_wait_ms", "呈现等待", "#b998ff"] : ["pipeline_2d_frame_handoff_ms", "发布衔接", "#b998ff"] ] : [ - ["request_to_presentation_opportunity_ms", "完整流水线", "#5ce4c2"], + total_series, ["pipeline_3d_prepare_ms", "Visual Prepare", "#62a8ff"], ["pipeline_3d_backend_queue_ms", "后端排队", "#f4bd63"], ["pipeline_3d_gpu_render_ms", "GPU Render", "#ff7d9c"], - ["pipeline_3d_readback_ms", "CPU 回读", "#b998ff"] + pixel_delivery ? ["pipeline_3d_readback_ms", "CPU 回读", "#b998ff"] : ["pipeline_3d_gpu_sync_ms", "GPU 同步", "#b998ff"] ]; chart.setOption({ backgroundColor: "transparent", @@ -809,20 +855,22 @@ function Frame_Diagnostics_View({diagnostics, dimension, on_reset}: {diagnostics const displayed = paused ? snapshot : diagnostics; if (!displayed) return
等待流水线样本采样独立于图形面板可见性;收到第一帧后开始统计。
; const copy = async () => { await navigator.clipboard.writeText(JSON.stringify(displayed.metadata, null, 2)); set_copied(true); window.setTimeout(() => set_copied(false), 1200); }; - const definitions = pipeline_definitions(dimension); + const definitions = pipeline_definitions(dimension, displayed.metadata.delivery); const stage_values = definitions.map(([key, label, description]) => { const history = displayed.samples.map(sample => sample.values[key]).filter(Number.isFinite); return [key, label, description, stage_statistic(history, stage_statistic_mode)] as const; }); const total_value = stage_values.reduce((sum, [, , , value]) => sum + (Number.isFinite(value) ? value : 0), 0); const statistic_labels: Record = {average: "滑动平均", variability: "波动", p95: "P95", p99: "P99"}; + const pixel_delivery = displayed.metadata.delivery === "pixels"; + const completion = pixel_delivery ? "完整像素到达浏览器" : "诊断元数据到达浏览器"; const summaries: Array<[string, string, string]> = [ - ["帧率", `${displayed.frame_rate_fps.toFixed(1)} FPS`, "当前统计区间内每秒完成的帧数。"], - ["端到端平均", `${displayed.request_to_pixels_average_ms.toFixed(2)} ms`, "帧请求发出到像素完整到达浏览器的平均耗时。"], + [pixel_delivery ? "像素帧率" : "诊断频率", `${displayed.frame_rate_fps.toFixed(1)} FPS`, `当前统计区间内每秒完成的${pixel_delivery ? "完整像素帧" : "后台诊断帧"}数量。`], + ["端到端平均", `${displayed.request_to_pixels_average_ms.toFixed(2)} ms`, `帧请求发出到${completion}的平均耗时。`], ["端到端 P50", `${displayed.request_to_pixels_p50_ms.toFixed(2)} ms`, "一半样本不超过该端到端耗时。"], ["端到端 P95", `${displayed.request_to_pixels_p95_ms.toFixed(2)} ms`, "95% 样本不超过该端到端耗时,用于观察长尾。"], ["端到端 P99", `${displayed.request_to_pixels_p99_ms.toFixed(2)} ms`, "99% 样本不超过该端到端耗时,用于观察极端长尾。"], - ["帧间隔平均", `${displayed.frame_interval_average_ms.toFixed(2)} ms`, "相邻两帧像素到达浏览器的平均间隔。"], + ["帧间隔平均", `${displayed.frame_interval_average_ms.toFixed(2)} ms`, `相邻两次${completion}的平均间隔。`], ["帧间隔抖动 P95", `${displayed.interval_jitter_p95_ms.toFixed(2)} ms`, "帧间隔相对中位数偏差的第 95 百分位。"], ["请求 ID 缺口", displayed.dropped_sequence_count.toLocaleString("zh-CN"), "相邻已完成请求 ID 之间缺失的数量;可能表示请求未形成完整样本。"] ]; @@ -837,7 +885,7 @@ function Frame_Diagnostics_View({diagnostics, dimension, on_reset}: {diagnostics }; const reset = () => { set_paused(false); set_snapshot(null); set_context_menu(null); on_reset(); }; return
-
当前区间 {displayed.samples.length} 帧;图表右键可暂停并缩放查看。暂停只冻结视图,采样始终继续。“呈现机会”不等同于物理屏幕扫描时刻。
+
当前为{pixel_delivery ? "完整像素" : "后台诊断"}模式,区间内 {displayed.samples.length} 帧;图表右键可暂停并缩放查看。{pixel_delivery ? "“呈现机会”不等同于物理屏幕扫描时刻。" : "离屏诊断仍执行真实渲染,但跳过 GPU 像素回读、像素传输和 Canvas 写入。"}
{summaries.map(([label, value, description]) =>
{label}
{value}
)}
流水线阶段统计#{displayed.metadata.sequence} / 请求 {displayed.metadata.correlation_id}
@@ -847,7 +895,7 @@ function Frame_Diagnostics_View({diagnostics, dimension, on_reset}: {diagnostics
{(["value", "percentage"] as Stage_Unit[]).map(unit => )}
-
浏览器请求{definitions.map(([key, label, description]) => {label})}浏览器呈现
+
浏览器请求{definitions.map(([key, label, description]) => {label})}{pixel_delivery ? "浏览器呈现" : "浏览器收到诊断"}
{stage_values.map(([key, label, description, value]) =>
{label}
{!Number.isFinite(value) ? "--" : stage_unit === "percentage" ? `${(total_value > 0 ? value / total_value * 100 : 0).toFixed(1)}%` : diagnostic_value(value, key)}
)}
@@ -924,7 +972,7 @@ function Frame_Policy_Pane({plot, analysis, busy, on_refresh, on_update, on_manu }) { const fields = analysis?.fields.filter(field => field.editable) ?? []; return
-
持续采样控制采样独立于图形面板可见性;暂停统计图不会停止采样。
+
{plot.dimension} 持续采样控制选中或可见时传输完整像素;离屏时继续真实渲染并只回传轻量诊断。2D 与 3D 使用各自的采样频率和阶段模型。
{plot.dimension === "3D" ? : null}
{analysis ?
{fields.map(field => on_update(analysis, field, value)}/>)}
:

正在读取帧策略…

}
; @@ -938,15 +986,24 @@ function Data_Generation_Pane({plot, analysis, busy, on_refresh, on_generated}: function Frame_Statistics_Pane({plot, diagnostics, busy, on_refresh, on_reset}: {plot: Plot; diagnostics: Frame_Diagnostics | null; busy: boolean; on_refresh: () => void; on_reset: () => void}) { return
-
; +
; } const Plot_Card = memo(function Plot_Card({plot, selected, on_select}: {plot: Plot; selected: boolean; on_select: (plot: Plot) => void}) { + const card_ref = useRef(null); const canvas_ref = useRef(null); - const {status, metrics} = use_plot_stream(plot, canvas_ref); + const [in_view, set_in_view] = useState(false); + useEffect(() => { + const card = card_ref.current; + if (!card) return; + const observer = new IntersectionObserver(entries => set_in_view(entries.some(entry => entry.isIntersecting)), {threshold: 0.05}); + observer.observe(card); + return () => observer.disconnect(); + }, []); + const {status, metrics} = use_plot_stream(plot, canvas_ref, selected || in_view); const generated_time = metrics ? new Date(metrics.generated_time_unix_ms).toLocaleTimeString("zh-CN", {hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, fractionalSecondDigits: 3}) : "尚未生成帧"; - return
on_select(plot)} onFocusCapture={() => on_select(plot)}>
绘图组件 · {plot.dimension}

{plot_labels[plot.id] ?? plot.title}

{{CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}{metrics ? enum_label(metrics.pacing_mode) : "等待策略"}
{metrics ? metrics.frame_rate_fps.toFixed(1) : "--.-"} FPSE2E {metrics ? metrics.request_to_pixels_ms.toFixed(1) : "--.-"} msP95 {metrics ? metrics.p95_request_to_pixels_ms.toFixed(1) : "--.-"} msP99 {metrics ? metrics.p99_request_to_pixels_ms.toFixed(1) : "--.-"} ms
+ return
on_select(plot)} onFocusCapture={() => on_select(plot)}>
绘图组件 · {plot.dimension}

{plot_labels[plot.id] ?? plot.title}

{{CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}{metrics?.delivery === "diagnostics" ? "后台诊断" : metrics ? enum_label(metrics.pacing_mode) : "等待策略"}
{metrics ? metrics.frame_rate_fps.toFixed(1) : "--.-"} FPSE2E {metrics ? metrics.request_to_pixels_ms.toFixed(1) : "--.-"} msP95 {metrics ? metrics.p95_request_to_pixels_ms.toFixed(1) : "--.-"} msP99 {metrics ? metrics.p99_request_to_pixels_ms.toFixed(1) : "--.-"} ms
{plot.description ?

{plot.description}

: null}
; });