From 686bb637c02a8b96668a5b8b6d01723f362398a8 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sat, 29 Aug 2026 18:12:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=9F=E5=A7=8B=E5=83=8F=E7=B4=A0=E5=A4=AA?= =?UTF-8?q?=E5=8D=A1=20=E8=B5=B0ffmpeg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Project_detail_specification.md | 2 +- kernel/src/kernel/frame.cpp | 54 +++ kernel/src/kernel/frame.hpp | 27 ++ kernel/src/kernel/frame_statistics.hpp | 27 ++ mcp/core/runtime/Plot.cpp | 16 + mcp/tests/Control_Path_Benchmarks.cpp | 68 ++++ .../base/Datoviz_Frame_Observation.hpp | 27 ++ render_3D/render_3D/scene/Render_Scene_3D.ipp | 58 ++- .../scene/detail/Datoviz_Frame_Pipeline.ipp | 41 +- .../scene/detail/Datoviz_Scene_Common.ipp | 66 +++- .../datoviz/include/datoviz/drp2/runtime.h | 11 + .../datoviz/include/datoviz/scene/types.h | 20 + .../datoviz/include/datoviz/vklite/commands.h | 16 +- .../third_party/datoviz/src/drp2/_runtime.h | 9 +- .../third_party/datoviz/src/drp2/backend.c | 14 +- render_3D/third_party/datoviz/src/drp2/pass.c | 6 +- .../third_party/datoviz/src/drp2/transfer.c | 56 ++- .../datoviz/src/scene/core/figure_emit.c | 69 +++- .../datoviz/src/scene/core/frame_artifact.c | 16 +- .../src/scene/core/frame_artifact_internal.h | 3 +- .../datoviz/src/scene/frame_plan/core.c | 39 +- .../datoviz/src/scene/frame_plan/emit.h | 1 + .../datoviz/src/scene/frame_plan/frame_plan.h | 2 + .../datoviz/src/scene/runtime/state.c | 10 + .../third_party/datoviz/src/vklite/commands.c | 25 +- .../Drogon_WebSocket_Receiver_Benchmark.cpp | 37 +- web_server/src/Gallery_Pixel_Stream.cpp | 104 +++-- web_server/src/Gallery_Pixel_Stream.hpp | 7 +- web_server/src/Gallery_Pixel_Stream.ipp | 13 +- web_server/src/Gallery_WebSocket.cpp | 44 ++- web_server/src/Gallery_WebSocket.hpp | 7 +- web_server/src/Web_Server.cpp | 2 +- web_server/src/detail/Gallery_Frame_Atlas.cpp | 187 --------- web_server/src/detail/Gallery_Frame_Atlas.hpp | 67 ---- web_server/src/detail/Gallery_Frame_Batch.cpp | 128 +++++++ web_server/src/detail/Gallery_Frame_Batch.hpp | 70 ++++ .../src/frame_sampling/Frame_Sampler.cpp | 48 +-- .../src/frame_sampling/Frame_Sampler.hpp | 10 +- .../websocket_pixel/WebSocket_Pixel_Frame.cpp | 88 +++-- .../websocket_pixel/WebSocket_Pixel_Frame.hpp | 7 +- web_server/tests/Frame_Sampler_Tests.cpp | 83 +++- .../tests/Gallery_Frame_Atlas_Tests.cpp | 187 --------- .../tests/Gallery_Frame_Batch_Tests.cpp | 97 +++++ webapp_gallery/src/app.tsx | 360 ++++++++++++------ 44 files changed, 1461 insertions(+), 768 deletions(-) delete mode 100644 web_server/src/detail/Gallery_Frame_Atlas.cpp delete mode 100644 web_server/src/detail/Gallery_Frame_Atlas.hpp create mode 100644 web_server/src/detail/Gallery_Frame_Batch.cpp create mode 100644 web_server/src/detail/Gallery_Frame_Batch.hpp delete mode 100644 web_server/tests/Gallery_Frame_Atlas_Tests.cpp create mode 100644 web_server/tests/Gallery_Frame_Batch_Tests.cpp diff --git a/Project_detail_specification.md b/Project_detail_specification.md index 080027a..1d3f68a 100644 --- a/Project_detail_specification.md +++ b/Project_detail_specification.md @@ -30,7 +30,7 @@ * Plot 使用服务端帧时钟调用 `Scene::render(frame*)`;`fixed_rate` 只由 Frame_Scheduler 周期时钟驱动,完成回调不得改变其 deadline。`maximum_rate` 是唯一允许完成回调在释放 Plot 准入后异步投递下一帧请求的特殊模式;回调不得直接重入 `render()` 、不得同步等待,并且物理帧槽耗尽时只能由帧退役机制解除背压。Scene 回调发布 2D 原生 BGRA 或 3D 原生 RGBA;Gallery - 像素流按固定时钟采样 latest 并组装图集,通过 Drogon WebSocket 发送唯一的原始像素协议。每个连接只允许一帧在途, + 像素流按固定时钟采样各 Plot 的 latest,只把本周期真实变化的 tile 通过 Drogon WebSocket 原始像素批协议发送;服务端不组装图集,浏览器由各 Plot 的 WebGL Canvas 直接上传自身 tile。每个连接只允许一批在途, 浏览器实际提交 Canvas 后 ACK,服务端只发送当时最新帧;Plot 输入和控制也统一使用 Drogon WebSocket。 * 帧策略及其时钟实现统一归属 `kernel/src/kernel/Frame_Policy`。配置变更和运行事实通过 MPMC 事件流进入唯一 consumer,由 `double_buffer/model.hpp` 发布 `Frame_Policy::State`;查询只读取已发布 State。手动帧捕获必须把帧创建边界的策略 State diff --git a/kernel/src/kernel/frame.cpp b/kernel/src/kernel/frame.cpp index 754c3d5..3a96316 100644 --- a/kernel/src/kernel/frame.cpp +++ b/kernel/src/kernel/frame.cpp @@ -127,13 +127,29 @@ Frame_Statistics_Sample Render_Frame::statistics() const { Frame_Statistic::backend_structure_check_ms, Frame_Statistic::backend_query_ms, Frame_Statistic::backend_runtime_plan_ms, + Frame_Statistic::backend_runtime_plan_cpu_ms, Frame_Statistic::backend_runtime_execute_ms, + Frame_Statistic::backend_runtime_execute_cpu_ms, Frame_Statistic::backend_mvp_update_ms, Frame_Statistic::backend_frame_begin_ms, Frame_Statistic::backend_frame_plan_ms, + Frame_Statistic::backend_frame_plan_cpu_ms, + Frame_Statistic::backend_emit_replay_dirty_ms, + Frame_Statistic::backend_emit_layout_ms, + Frame_Statistic::backend_emit_prepare_ms, + Frame_Statistic::backend_emit_plan_build_ms, + Frame_Statistic::backend_emit_contract_ms, + Frame_Statistic::backend_emit_stream_ms, + Frame_Statistic::backend_emit_stream_freeze_ms, + Frame_Statistic::backend_emit_commit_ms, + Frame_Statistic::backend_emit_plan_reset_ms, + Frame_Statistic::backend_emit_artifact_create_ms, + Frame_Statistic::backend_emit_artifact_freeze_ms, + Frame_Statistic::backend_emit_packet_encode_ms, Frame_Statistic::backend_external_register_ms, Frame_Statistic::backend_frame_attach_ms, Frame_Statistic::backend_frame_execute_ms, + Frame_Statistic::backend_frame_execute_cpu_ms, Frame_Statistic::backend_frame_finish_ms, Frame_Statistic::backend_drp_validation_ms, Frame_Statistic::backend_drp_state_ms, @@ -145,6 +161,17 @@ Frame_Statistics_Sample Render_Frame::statistics() const { Frame_Statistic::backend_drp_pipeline_create_ms, Frame_Statistic::backend_drp_binding_create_ms, Frame_Statistic::backend_drp_upload_ms, + Frame_Statistic::backend_drp_upload_decode_ms, + Frame_Statistic::backend_drp_upload_vulkan_allocate_ms, + Frame_Statistic::backend_drp_upload_host_copy_ms, + Frame_Statistic::backend_drp_upload_command_allocate_ms, + Frame_Statistic::backend_drp_upload_command_record_ms, + Frame_Statistic::backend_drp_upload_fence_create_ms, + Frame_Statistic::backend_drp_upload_submit_enqueue_ms, + Frame_Statistic::backend_drp_upload_submit_queue_wait_ms, + Frame_Statistic::backend_drp_upload_queue_submit_ms, + Frame_Statistic::backend_drp_upload_fence_wait_ms, + Frame_Statistic::backend_drp_upload_retire_ms, Frame_Statistic::backend_drp_transfer_ms, Frame_Statistic::backend_drp_record_ms, Frame_Statistic::backend_submit_queue_ms, @@ -164,13 +191,29 @@ Frame_Statistics_Sample Render_Frame::statistics() const { Frame_Trace_Measurement::backend_structure_check_ns, Frame_Trace_Measurement::backend_query_ns, Frame_Trace_Measurement::backend_runtime_plan_ns, + Frame_Trace_Measurement::backend_runtime_plan_cpu_ns, Frame_Trace_Measurement::backend_runtime_execute_ns, + Frame_Trace_Measurement::backend_runtime_execute_cpu_ns, Frame_Trace_Measurement::backend_mvp_update_ns, Frame_Trace_Measurement::backend_frame_begin_ns, Frame_Trace_Measurement::backend_frame_plan_ns, + Frame_Trace_Measurement::backend_frame_plan_cpu_ns, + Frame_Trace_Measurement::backend_emit_replay_dirty_ns, + Frame_Trace_Measurement::backend_emit_layout_ns, + Frame_Trace_Measurement::backend_emit_prepare_ns, + Frame_Trace_Measurement::backend_emit_plan_build_ns, + Frame_Trace_Measurement::backend_emit_contract_ns, + Frame_Trace_Measurement::backend_emit_stream_ns, + Frame_Trace_Measurement::backend_emit_stream_freeze_ns, + Frame_Trace_Measurement::backend_emit_commit_ns, + Frame_Trace_Measurement::backend_emit_plan_reset_ns, + Frame_Trace_Measurement::backend_emit_artifact_create_ns, + Frame_Trace_Measurement::backend_emit_artifact_freeze_ns, + Frame_Trace_Measurement::backend_emit_packet_encode_ns, Frame_Trace_Measurement::backend_external_register_ns, Frame_Trace_Measurement::backend_frame_attach_ns, Frame_Trace_Measurement::backend_frame_execute_ns, + Frame_Trace_Measurement::backend_frame_execute_cpu_ns, Frame_Trace_Measurement::backend_frame_finish_ns, Frame_Trace_Measurement::backend_drp_validation_ns, Frame_Trace_Measurement::backend_drp_state_ns, @@ -182,6 +225,17 @@ Frame_Statistics_Sample Render_Frame::statistics() const { Frame_Trace_Measurement::backend_drp_pipeline_create_ns, Frame_Trace_Measurement::backend_drp_binding_create_ns, Frame_Trace_Measurement::backend_drp_upload_ns, + Frame_Trace_Measurement::backend_drp_upload_decode_ns, + Frame_Trace_Measurement::backend_drp_upload_vulkan_allocate_ns, + Frame_Trace_Measurement::backend_drp_upload_host_copy_ns, + Frame_Trace_Measurement::backend_drp_upload_command_allocate_ns, + Frame_Trace_Measurement::backend_drp_upload_command_record_ns, + Frame_Trace_Measurement::backend_drp_upload_fence_create_ns, + Frame_Trace_Measurement::backend_drp_upload_submit_enqueue_ns, + Frame_Trace_Measurement::backend_drp_upload_submit_queue_wait_ns, + Frame_Trace_Measurement::backend_drp_upload_queue_submit_ns, + Frame_Trace_Measurement::backend_drp_upload_fence_wait_ns, + Frame_Trace_Measurement::backend_drp_upload_retire_ns, Frame_Trace_Measurement::backend_drp_transfer_ns, Frame_Trace_Measurement::backend_drp_record_ns, Frame_Trace_Measurement::backend_submit_queue_ns, diff --git a/kernel/src/kernel/frame.hpp b/kernel/src/kernel/frame.hpp index dcf5bfe..741d9b8 100644 --- a/kernel/src/kernel/frame.hpp +++ b/kernel/src/kernel/frame.hpp @@ -58,13 +58,29 @@ enum struct Frame_Trace_Measurement : std::uint8_t { backend_structure_check_ns, backend_query_ns, backend_runtime_plan_ns, + backend_runtime_plan_cpu_ns, backend_runtime_execute_ns, + backend_runtime_execute_cpu_ns, backend_mvp_update_ns, backend_frame_begin_ns, backend_frame_plan_ns, + backend_frame_plan_cpu_ns, + backend_emit_replay_dirty_ns, + backend_emit_layout_ns, + backend_emit_prepare_ns, + backend_emit_plan_build_ns, + backend_emit_contract_ns, + backend_emit_stream_ns, + backend_emit_stream_freeze_ns, + backend_emit_commit_ns, + backend_emit_plan_reset_ns, + backend_emit_artifact_create_ns, + backend_emit_artifact_freeze_ns, + backend_emit_packet_encode_ns, backend_external_register_ns, backend_frame_attach_ns, backend_frame_execute_ns, + backend_frame_execute_cpu_ns, backend_frame_finish_ns, backend_drp_validation_ns, backend_drp_state_ns, @@ -76,6 +92,17 @@ enum struct Frame_Trace_Measurement : std::uint8_t { backend_drp_pipeline_create_ns, backend_drp_binding_create_ns, backend_drp_upload_ns, + backend_drp_upload_decode_ns, + backend_drp_upload_vulkan_allocate_ns, + backend_drp_upload_host_copy_ns, + backend_drp_upload_command_allocate_ns, + backend_drp_upload_command_record_ns, + backend_drp_upload_fence_create_ns, + backend_drp_upload_submit_enqueue_ns, + backend_drp_upload_submit_queue_wait_ns, + backend_drp_upload_queue_submit_ns, + backend_drp_upload_fence_wait_ns, + backend_drp_upload_retire_ns, backend_drp_transfer_ns, backend_drp_record_ns, backend_submit_queue_ns, diff --git a/kernel/src/kernel/frame_statistics.hpp b/kernel/src/kernel/frame_statistics.hpp index 420dca7..20995fb 100644 --- a/kernel/src/kernel/frame_statistics.hpp +++ b/kernel/src/kernel/frame_statistics.hpp @@ -23,13 +23,29 @@ enum struct Frame_Statistic : std::uint8_t { backend_structure_check_ms, backend_query_ms, backend_runtime_plan_ms, + backend_runtime_plan_cpu_ms, backend_runtime_execute_ms, + backend_runtime_execute_cpu_ms, backend_mvp_update_ms, backend_frame_begin_ms, backend_frame_plan_ms, + backend_frame_plan_cpu_ms, + backend_emit_replay_dirty_ms, + backend_emit_layout_ms, + backend_emit_prepare_ms, + backend_emit_plan_build_ms, + backend_emit_contract_ms, + backend_emit_stream_ms, + backend_emit_stream_freeze_ms, + backend_emit_commit_ms, + backend_emit_plan_reset_ms, + backend_emit_artifact_create_ms, + backend_emit_artifact_freeze_ms, + backend_emit_packet_encode_ms, backend_external_register_ms, backend_frame_attach_ms, backend_frame_execute_ms, + backend_frame_execute_cpu_ms, backend_frame_finish_ms, backend_drp_validation_ms, backend_drp_state_ms, @@ -41,6 +57,17 @@ enum struct Frame_Statistic : std::uint8_t { backend_drp_pipeline_create_ms, backend_drp_binding_create_ms, backend_drp_upload_ms, + backend_drp_upload_decode_ms, + backend_drp_upload_vulkan_allocate_ms, + backend_drp_upload_host_copy_ms, + backend_drp_upload_command_allocate_ms, + backend_drp_upload_command_record_ms, + backend_drp_upload_fence_create_ms, + backend_drp_upload_submit_enqueue_ms, + backend_drp_upload_submit_queue_wait_ms, + backend_drp_upload_queue_submit_ms, + backend_drp_upload_fence_wait_ms, + backend_drp_upload_retire_ms, backend_drp_transfer_ms, backend_drp_record_ms, backend_submit_queue_ms, diff --git a/mcp/core/runtime/Plot.cpp b/mcp/core/runtime/Plot.cpp index a45fb51..a66d5b3 100644 --- a/mcp/core/runtime/Plot.cpp +++ b/mcp/core/runtime/Plot.cpp @@ -359,13 +359,29 @@ nlohmann::json datoviz_observation_json( {"apply", milliseconds(value.apply_ns)}, {"query", milliseconds(value.query_ns)}, {"runtime_plan", milliseconds(value.runtime_plan_ns)}, + {"runtime_plan_cpu", milliseconds(value.runtime_plan_cpu_ns)}, {"runtime_execute", milliseconds(value.runtime_execute_ns)}, + {"runtime_execute_cpu", milliseconds(value.runtime_execute_cpu_ns)}, {"mvp_update", milliseconds(value.mvp_update_ns)}, {"frame_begin", milliseconds(value.frame_begin_ns)}, {"frame_plan", milliseconds(value.frame_plan_ns)}, + {"frame_plan_cpu", milliseconds(value.frame_plan_cpu_ns)}, + {"emit_replay_dirty", milliseconds(value.emit_replay_dirty_ns)}, + {"emit_layout", milliseconds(value.emit_layout_ns)}, + {"emit_prepare", milliseconds(value.emit_prepare_ns)}, + {"emit_plan_build", milliseconds(value.emit_plan_build_ns)}, + {"emit_contract", milliseconds(value.emit_contract_ns)}, + {"emit_stream", milliseconds(value.emit_stream_ns)}, + {"emit_stream_freeze", milliseconds(value.emit_stream_freeze_ns)}, + {"emit_commit", milliseconds(value.emit_commit_ns)}, + {"emit_plan_reset", milliseconds(value.emit_plan_reset_ns)}, + {"emit_artifact_create", milliseconds(value.emit_artifact_create_ns)}, + {"emit_artifact_freeze", milliseconds(value.emit_artifact_freeze_ns)}, + {"emit_packet_encode", milliseconds(value.emit_packet_encode_ns)}, {"external_register", milliseconds(value.external_register_ns)}, {"frame_attach", milliseconds(value.frame_attach_ns)}, {"frame_execute", milliseconds(value.frame_execute_ns)}, + {"frame_execute_cpu", milliseconds(value.frame_execute_cpu_ns)}, {"frame_finish", milliseconds(value.frame_finish_ns)}, {"drp_validation", milliseconds(value.drp_validation_ns)}, {"drp_state", milliseconds(value.drp_state_ns)}, diff --git a/mcp/tests/Control_Path_Benchmarks.cpp b/mcp/tests/Control_Path_Benchmarks.cpp index 76c4fad..f1e7baa 100644 --- a/mcp/tests/Control_Path_Benchmarks.cpp +++ b/mcp/tests/Control_Path_Benchmarks.cpp @@ -395,14 +395,31 @@ protected: "backend_structure_check_ms", "backend_query_ms", "backend_runtime_plan_ms", + "backend_runtime_plan_cpu_ms", "backend_runtime_execute_ms", + "backend_runtime_execute_cpu_ms", "backend_mvp_update_ms", "backend_frame_begin_ms", "backend_frame_plan_ms", + "backend_frame_plan_cpu_ms", "backend_external_register_ms", "backend_frame_attach_ms", "backend_frame_execute_ms", + "backend_frame_execute_cpu_ms", "backend_frame_finish_ms"}; + constexpr std::array detailed_emit_statistics{ + "backend_emit_replay_dirty_ms", + "backend_emit_layout_ms", + "backend_emit_prepare_ms", + "backend_emit_plan_build_ms", + "backend_emit_contract_ms", + "backend_emit_stream_ms", + "backend_emit_stream_freeze_ms", + "backend_emit_commit_ms", + "backend_emit_plan_reset_ms", + "backend_emit_artifact_create_ms", + "backend_emit_artifact_freeze_ms", + "backend_emit_packet_encode_ms"}; constexpr std::array detailed_drp_statistics{ "backend_drp_validation_ms", "backend_drp_state_ms", @@ -414,6 +431,17 @@ protected: "backend_drp_pipeline_create_ms", "backend_drp_binding_create_ms", "backend_drp_upload_ms", + "backend_drp_upload_decode_ms", + "backend_drp_upload_vulkan_allocate_ms", + "backend_drp_upload_host_copy_ms", + "backend_drp_upload_command_allocate_ms", + "backend_drp_upload_command_record_ms", + "backend_drp_upload_fence_create_ms", + "backend_drp_upload_submit_enqueue_ms", + "backend_drp_upload_submit_queue_wait_ms", + "backend_drp_upload_queue_submit_ms", + "backend_drp_upload_fence_wait_ms", + "backend_drp_upload_retire_ms", "backend_drp_transfer_ms", "backend_drp_record_ms"}; for (const std::string_view name : detailed_backend_statistics) { @@ -422,6 +450,12 @@ protected: state.counters[prefix + std::string{name} + "/max"] = statistic_value(name, "maximum"); } + for (const std::string_view name : detailed_emit_statistics) { + state.counters[prefix + std::string{name} + "/p95"] = + statistic_value(name, "p95"); + state.counters[prefix + std::string{name} + "/max"] = + statistic_value(name, "maximum"); + } for (const std::string_view name : detailed_drp_statistics) { state.counters[prefix + std::string{name} + "/p95"] = statistic_value(name, "p95"); @@ -615,13 +649,17 @@ public: std::pair{"structure", "backend_structure_check_ms"}, std::pair{"query", "backend_query_ms"}, std::pair{"runtime-plan", "backend_runtime_plan_ms"}, + std::pair{"runtime-plan-cpu", "backend_runtime_plan_cpu_ms"}, std::pair{"runtime-exec", "backend_runtime_execute_ms"}, + std::pair{"runtime-exec-cpu", "backend_runtime_execute_cpu_ms"}, std::pair{"mvp", "backend_mvp_update_ms"}, std::pair{"frame-begin", "backend_frame_begin_ms"}, std::pair{"frame-plan", "backend_frame_plan_ms"}, + std::pair{"frame-plan-cpu", "backend_frame_plan_cpu_ms"}, std::pair{"external", "backend_external_register_ms"}, std::pair{"attach", "backend_frame_attach_ms"}, std::pair{"frame-exec", "backend_frame_execute_ms"}, + std::pair{"frame-exec-cpu", "backend_frame_execute_cpu_ms"}, std::pair{"finish", "backend_frame_finish_ms"}}; output << " backend-detail " << id; for (const auto& [label, statistic] : @@ -630,6 +668,25 @@ public: << counter(prefix + statistic + "/p95") << "/" << counter(prefix + statistic + "/max"); output << '\n'; + constexpr std::array detailed_emit_statistics{ + std::pair{"replay", "backend_emit_replay_dirty_ms"}, + std::pair{"layout", "backend_emit_layout_ms"}, + std::pair{"prepare", "backend_emit_prepare_ms"}, + std::pair{"build", "backend_emit_plan_build_ms"}, + std::pair{"contract", "backend_emit_contract_ms"}, + std::pair{"stream", "backend_emit_stream_ms"}, + std::pair{"stream-freeze", "backend_emit_stream_freeze_ms"}, + std::pair{"commit", "backend_emit_commit_ms"}, + std::pair{"reset", "backend_emit_plan_reset_ms"}, + std::pair{"artifact", "backend_emit_artifact_create_ms"}, + std::pair{"artifact-freeze", "backend_emit_artifact_freeze_ms"}, + std::pair{"packet", "backend_emit_packet_encode_ms"}}; + output << " emit-detail " << id; + for (const auto& [label, statistic] : detailed_emit_statistics) + output << " " << label << "95/max=" + << counter(prefix + statistic + "/p95") << "/" + << counter(prefix + statistic + "/max"); + output << '\n'; constexpr std::array detailed_drp_statistics{ std::pair{"validate", "backend_drp_validation_ms"}, std::pair{"state", "backend_drp_state_ms"}, @@ -641,6 +698,17 @@ public: std::pair{"pipeline", "backend_drp_pipeline_create_ms"}, std::pair{"binding", "backend_drp_binding_create_ms"}, std::pair{"upload", "backend_drp_upload_ms"}, + std::pair{"decode", "backend_drp_upload_decode_ms"}, + std::pair{"vk-alloc", "backend_drp_upload_vulkan_allocate_ms"}, + std::pair{"host-copy", "backend_drp_upload_host_copy_ms"}, + std::pair{"cmd-alloc", "backend_drp_upload_command_allocate_ms"}, + std::pair{"cmd-record", "backend_drp_upload_command_record_ms"}, + std::pair{"fence-create", "backend_drp_upload_fence_create_ms"}, + std::pair{"enqueue", "backend_drp_upload_submit_enqueue_ms"}, + std::pair{"queue-wait", "backend_drp_upload_submit_queue_wait_ms"}, + std::pair{"queue-submit", "backend_drp_upload_queue_submit_ms"}, + std::pair{"fence-wait", "backend_drp_upload_fence_wait_ms"}, + std::pair{"retire", "backend_drp_upload_retire_ms"}, std::pair{"transfer", "backend_drp_transfer_ms"}, std::pair{"record", "backend_drp_record_ms"}}; output << " drp-detail " << id; diff --git a/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp b/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp index 877981f..753944e 100644 --- a/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp +++ b/render_3D/render_3D/base/Datoviz_Frame_Observation.hpp @@ -36,13 +36,29 @@ struct Datoviz_Frame_Observation { std::uint64_t apply_ns{}; /* 应用 Camera、Axes 和 Visual 数据的墙钟耗时。 */ std::uint64_t query_ns{}; /* 处理 Datoviz 查询与悬浮读数的墙钟耗时。 */ std::uint64_t runtime_plan_ns{}; /* 生成增量 Runtime 资源命令流的墙钟耗时。 */ + std::uint64_t runtime_plan_cpu_ns{}; /* 生成增量 Runtime 资源命令流的当前线程 CPU 耗时。 */ std::uint64_t runtime_execute_ns{}; /* 执行增量 Runtime 资源命令流的墙钟耗时。 */ + std::uint64_t runtime_execute_cpu_ns{}; /* 执行增量 Runtime 资源命令流的当前线程 CPU 耗时。 */ std::uint64_t mvp_update_ns{}; /* 更新可复用命令所引用 MVP 缓冲的墙钟耗时。 */ std::uint64_t frame_begin_ns{}; /* 重置并开始目标命令缓冲录制的墙钟耗时。 */ std::uint64_t frame_plan_ns{}; /* 生成完整 Datoviz 帧计划的墙钟耗时。 */ + std::uint64_t frame_plan_cpu_ns{}; /* 生成完整 Datoviz 帧计划的当前线程 CPU 耗时。 */ + std::uint64_t emit_replay_dirty_ns{}; /* 恢复本 Runtime 需要重放的 payload dirty 状态。 */ + std::uint64_t emit_layout_ns{}; /* 布局与屏幕缩放状态求解墙钟。 */ + std::uint64_t emit_prepare_ns{}; /* Guide、bar、band 等 Scene 装饰准备墙钟。 */ + std::uint64_t emit_plan_build_ns{}; /* Upload、compute 与 panel render 节点建图墙钟。 */ + std::uint64_t emit_contract_ns{}; /* 透明模式与 FramePlan contract 验证墙钟。 */ + std::uint64_t emit_stream_ns{}; /* FramePlan 转换为增量 DRP2 stream 的墙钟。 */ + std::uint64_t emit_stream_freeze_ns{}; /* DRP2 stream 借用 payload 固化墙钟。 */ + std::uint64_t emit_commit_ns{}; /* 成功发布后提交 Scene dirty 状态的墙钟。 */ + std::uint64_t emit_plan_reset_ns{}; /* FramePlan 语义清理且保留 arena 容量的墙钟。 */ + std::uint64_t emit_artifact_create_ns{}; /* 帧 artifact 本体创建墙钟。 */ + std::uint64_t emit_artifact_freeze_ns{}; /* artifact 边界的 payload 固化复查墙钟。 */ + std::uint64_t emit_packet_encode_ns{}; /* setup/update/frame 三段 packet 编码墙钟。 */ std::uint64_t external_register_ns{}; /* 注册外部属性缓冲的墙钟耗时。 */ std::uint64_t frame_attach_ns{}; /* 把借用目标附加到 DRP2 Runtime 的墙钟耗时。 */ std::uint64_t frame_execute_ns{}; /* 执行完整帧命令流的墙钟耗时。 */ + std::uint64_t frame_execute_cpu_ns{}; /* 执行完整帧命令流的当前线程 CPU 耗时。 */ std::uint64_t frame_finish_ns{}; /* 完成命令录制并建立提交描述的墙钟耗时。 */ std::uint64_t drp_validation_ns{}; /* DRP2 语义验证与状态复制耗时。 */ std::uint64_t drp_state_ns{}; /* DRP2 状态建立与提交耗时。 */ @@ -54,6 +70,17 @@ struct Datoviz_Frame_Observation { std::uint64_t drp_pipeline_create_ns{}; /* Vulkan Pipeline 创建命令耗时。 */ std::uint64_t drp_binding_create_ns{}; /* Sampler、Layout 与 Bind Group 创建命令耗时。 */ std::uint64_t drp_upload_ns{}; /* Buffer 与 Texture 写入命令耗时。 */ + std::uint64_t drp_upload_decode_ns{}; /* Base64 解码。 */ + std::uint64_t drp_upload_vulkan_allocate_ns{}; /* Staging Vulkan allocation。 */ + std::uint64_t drp_upload_host_copy_ns{}; /* Host 到 mapped buffer 的 memcpy。 */ + std::uint64_t drp_upload_command_allocate_ns{}; /* 临时 command pool/buffer allocation。 */ + std::uint64_t drp_upload_command_record_ns{}; /* Upload copy 命令录制。 */ + std::uint64_t drp_upload_fence_create_ns{}; /* Upload fence 创建。 */ + std::uint64_t drp_upload_submit_enqueue_ns{}; /* 进入唯一 VkQueue 提交通道。 */ + std::uint64_t drp_upload_submit_queue_wait_ns{}; /* 在提交通道中的排队墙钟。 */ + std::uint64_t drp_upload_queue_submit_ns{}; /* vkQueueSubmit2 调用墙钟。 */ + std::uint64_t drp_upload_fence_wait_ns{}; /* 协作让出期间的 fence 完成墙钟。 */ + std::uint64_t drp_upload_retire_ns{}; /* 已完成临时资源退役。 */ std::uint64_t drp_transfer_ns{}; /* 显式 Copy 命令耗时。 */ std::uint64_t drp_record_ns{}; /* Render/Compute Pass 录制命令耗时。 */ std::uint32_t drp_pipeline_create_count{}; /* 本帧实际创建的图形/计算管线数。 */ diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index e460cb4..ce5cc24 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -28,6 +28,7 @@ struct DvzPinnedReadout; struct DvzPointerGestureHandler; struct DvzScene; struct DvzSceneFrameArtifact; +struct DvzSceneFrameEmitTiming; namespace aethera { namespace render_3d { namespace detail { @@ -108,7 +109,8 @@ struct Scene_Datoviz_State { 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); + const Scene_3D_Parameters& scene, std::uint8_t target_index, + DvzSceneFrameEmitTiming& timing); [[nodiscard]] std::optional acquire_target(Extent extent); [[nodiscard]] Datoviz_Frame_Target& target(const Datoviz_Pending_Frame& pending); /* 提交不等待 GPU;完成通知由 Gpu_Completion_Service 驱动 collect()。 */ @@ -227,20 +229,52 @@ inline void publish_datoviz_observation( observation.query_ns}, std::pair{Frame_Trace_Measurement::backend_runtime_plan_ns, observation.runtime_plan_ns}, + std::pair{Frame_Trace_Measurement::backend_runtime_plan_cpu_ns, + observation.runtime_plan_cpu_ns}, std::pair{Frame_Trace_Measurement::backend_runtime_execute_ns, observation.runtime_execute_ns}, + std::pair{Frame_Trace_Measurement::backend_runtime_execute_cpu_ns, + observation.runtime_execute_cpu_ns}, std::pair{Frame_Trace_Measurement::backend_mvp_update_ns, observation.mvp_update_ns}, std::pair{Frame_Trace_Measurement::backend_frame_begin_ns, observation.frame_begin_ns}, std::pair{Frame_Trace_Measurement::backend_frame_plan_ns, observation.frame_plan_ns}, + std::pair{Frame_Trace_Measurement::backend_frame_plan_cpu_ns, + observation.frame_plan_cpu_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_replay_dirty_ns, + observation.emit_replay_dirty_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_layout_ns, + observation.emit_layout_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_prepare_ns, + observation.emit_prepare_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_plan_build_ns, + observation.emit_plan_build_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_contract_ns, + observation.emit_contract_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_stream_ns, + observation.emit_stream_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_stream_freeze_ns, + observation.emit_stream_freeze_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_commit_ns, + observation.emit_commit_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_plan_reset_ns, + observation.emit_plan_reset_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_artifact_create_ns, + observation.emit_artifact_create_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_artifact_freeze_ns, + observation.emit_artifact_freeze_ns}, + std::pair{Frame_Trace_Measurement::backend_emit_packet_encode_ns, + observation.emit_packet_encode_ns}, std::pair{Frame_Trace_Measurement::backend_external_register_ns, observation.external_register_ns}, std::pair{Frame_Trace_Measurement::backend_frame_attach_ns, observation.frame_attach_ns}, std::pair{Frame_Trace_Measurement::backend_frame_execute_ns, observation.frame_execute_ns}, + std::pair{Frame_Trace_Measurement::backend_frame_execute_cpu_ns, + observation.frame_execute_cpu_ns}, std::pair{Frame_Trace_Measurement::backend_frame_finish_ns, observation.frame_finish_ns}, std::pair{Frame_Trace_Measurement::backend_drp_validation_ns, @@ -263,6 +297,28 @@ inline void publish_datoviz_observation( observation.drp_binding_create_ns}, std::pair{Frame_Trace_Measurement::backend_drp_upload_ns, observation.drp_upload_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_decode_ns, + observation.drp_upload_decode_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_vulkan_allocate_ns, + observation.drp_upload_vulkan_allocate_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_host_copy_ns, + observation.drp_upload_host_copy_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_command_allocate_ns, + observation.drp_upload_command_allocate_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_command_record_ns, + observation.drp_upload_command_record_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_fence_create_ns, + observation.drp_upload_fence_create_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_submit_enqueue_ns, + observation.drp_upload_submit_enqueue_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_submit_queue_wait_ns, + observation.drp_upload_submit_queue_wait_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_queue_submit_ns, + observation.drp_upload_queue_submit_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_fence_wait_ns, + observation.drp_upload_fence_wait_ns}, + std::pair{Frame_Trace_Measurement::backend_drp_upload_retire_ns, + observation.drp_upload_retire_ns}, std::pair{Frame_Trace_Measurement::backend_drp_transfer_ns, observation.drp_transfer_ns}, std::pair{Frame_Trace_Measurement::backend_drp_record_ns, diff --git a/render_3D/render_3D/scene/detail/Datoviz_Frame_Pipeline.ipp b/render_3D/render_3D/scene/detail/Datoviz_Frame_Pipeline.ipp index 9f7b2b7..d5cd232 100644 --- a/render_3D/render_3D/scene/detail/Datoviz_Frame_Pipeline.ipp +++ b/render_3D/render_3D/scene/detail/Datoviz_Frame_Pipeline.ipp @@ -35,6 +35,17 @@ void accumulate_execution_timing( command(DVZ_DRP2_COMMAND_CREATE_BIND_GROUP); target.drp_upload_ns += command(DVZ_DRP2_COMMAND_WRITE_BUFFER) + command(DVZ_DRP2_COMMAND_WRITE_TEXTURE); + target.drp_upload_decode_ns += source.upload_decode_ns; + target.drp_upload_vulkan_allocate_ns += source.upload_vulkan_allocate_ns; + target.drp_upload_host_copy_ns += source.upload_host_copy_ns; + target.drp_upload_command_allocate_ns += source.upload_command_allocate_ns; + target.drp_upload_command_record_ns += source.upload_command_record_ns; + target.drp_upload_fence_create_ns += source.upload_fence_create_ns; + target.drp_upload_submit_enqueue_ns += source.upload_submit_enqueue_ns; + target.drp_upload_submit_queue_wait_ns += source.upload_submit_queue_wait_ns; + target.drp_upload_queue_submit_ns += source.upload_queue_submit_ns; + target.drp_upload_fence_wait_ns += source.upload_fence_wait_ns; + target.drp_upload_retire_ns += source.upload_retire_ns; target.drp_transfer_ns += command(DVZ_DRP2_COMMAND_COPY_BUFFER_TO_BUFFER) + command(DVZ_DRP2_COMMAND_COPY_BUFFER_TO_TEXTURE) + @@ -62,7 +73,8 @@ void accumulate_execution_timing( /* Datoviz Scene 根据当前权威资源生成 DRP2 命令流描述,不提交 GPU。 */ DvzSceneFrameArtifact* Scene_Datoviz_State::emit( - const Scene_3D_Parameters& scene, std::uint8_t target_index) { + const Scene_3D_Parameters& scene, std::uint8_t target_index, + DvzSceneFrameEmitTiming& timing) { if (target_index >= runtime_slots_.size() || runtime_slots_[target_index].emitter == nullptr) throw std::logic_error("Datoviz frame target emitter is unavailable"); @@ -77,6 +89,7 @@ DvzSceneFrameArtifact* Scene_Datoviz_State::emit( configuration.clear_color[1] = scene.clear_color.green; configuration.clear_color[2] = scene.clear_color.blue; configuration.clear_color[3] = scene.clear_color.alpha; + configuration.timing = &timing; const auto capabilities = offscreen_capabilities(); DvzDiagnosticReport report{}; dvz_diagnostic_report_init(&report); @@ -268,6 +281,7 @@ std::optional Scene_Datoviz_State::prepare( std::uint64_t uploaded_bytes{}; bool command_recording_valid{}; phase_started = trace_now_ns(); + const std::uint64_t cpu_started = trace_thread_cpu_ns(); const auto replay_visuals = stale_runtime_visuals( visuals, *target_index); std::unique_ptr Scene_Datoviz_State::prepare( &dvz_drp2_stream_destroy }; observation.runtime_plan_ns = trace_now_ns() - phase_started; + observation.runtime_plan_cpu_ns = + trace_thread_cpu_ns() - cpu_started; if (!update_stream) { std::string message = "failed to prepare Datoviz retained runtime resources"; @@ -291,6 +307,7 @@ std::optional Scene_Datoviz_State::prepare( throw std::runtime_error(std::move(message)); } phase_started = trace_now_ns(); + const std::uint64_t execute_cpu_started = trace_thread_cpu_ns(); DvzDrp2ValidationResult update_result{}; { DvzDrp2ExecutionTiming timing{}; @@ -300,6 +317,8 @@ std::optional Scene_Datoviz_State::prepare( accumulate_execution_timing(timing, observation); } observation.runtime_execute_ns = trace_now_ns() - phase_started; + observation.runtime_execute_cpu_ns = + trace_thread_cpu_ns() - execute_cpu_started; if (!update_result.ok) throw std::runtime_error( "failed to execute Datoviz retained runtime resources"); @@ -372,8 +391,23 @@ std::optional Scene_Datoviz_State::prepare( artifact{nullptr, &dvz_scene_frame_artifact_destroy}; try { phase_started = trace_now_ns(); - artifact.reset(emit(scene, *target_index)); + const std::uint64_t cpu_started = trace_thread_cpu_ns(); + DvzSceneFrameEmitTiming timing{}; + artifact.reset(emit(scene, *target_index, timing)); observation.frame_plan_ns = trace_now_ns() - phase_started; + observation.frame_plan_cpu_ns = trace_thread_cpu_ns() - cpu_started; + observation.emit_replay_dirty_ns = timing.replay_dirty_ns; + observation.emit_layout_ns = timing.layout_ns; + observation.emit_prepare_ns = timing.prepare_ns; + observation.emit_plan_build_ns = timing.plan_build_ns; + observation.emit_contract_ns = timing.contract_ns; + observation.emit_stream_ns = timing.stream_emit_ns; + observation.emit_stream_freeze_ns = timing.stream_freeze_ns; + observation.emit_commit_ns = timing.commit_ns; + observation.emit_plan_reset_ns = timing.plan_reset_ns; + observation.emit_artifact_create_ns = timing.artifact_create_ns; + observation.emit_artifact_freeze_ns = timing.artifact_freeze_ns; + observation.emit_packet_encode_ns = timing.artifact_encode_ns; } catch (...) { raise_context("preparing Datoviz frame", std::current_exception()); @@ -402,11 +436,14 @@ std::optional Scene_Datoviz_State::prepare( observation.frame_attach_ns = trace_now_ns() - phase_started; if (attached) { phase_started = trace_now_ns(); + const std::uint64_t cpu_started = trace_thread_cpu_ns(); DvzDrp2ExecutionTiming timing{}; result = dvz_drp2_runtime_execute_timed( runtime_slots_[*target_index].runtime, stream, &timing); accumulate_execution_timing(timing, observation); observation.frame_execute_ns = trace_now_ns() - phase_started; + observation.frame_execute_cpu_ns = + trace_thread_cpu_ns() - cpu_started; } observation.validation_performed = true; observation.validation_ok = attached && result.ok; diff --git a/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp b/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp index 13f9c59..8af77a2 100644 --- a/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp +++ b/render_3D/render_3D/scene/detail/Datoviz_Scene_Common.ipp @@ -2,6 +2,17 @@ #include "../Render_Scene_3D.hpp" #include "../../detail/Exception.hpp" +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif #include #include #include @@ -42,6 +53,27 @@ std::uint64_t trace_now_ns() noexcept { std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count()); } +std::uint64_t trace_thread_cpu_ns() noexcept { +#if defined(_WIN32) + FILETIME created{}, exited{}, kernel{}, user{}; + if (!GetThreadTimes(GetCurrentThread(), &created, &exited, &kernel, &user)) + return 0; + const auto ticks = [](FILETIME value) noexcept { + ULARGE_INTEGER result{}; + result.LowPart = value.dwLowDateTime; + result.HighPart = value.dwHighDateTime; + return result.QuadPart; + }; + return (ticks(kernel) + ticks(user)) * 100ULL; +#elif defined(CLOCK_THREAD_CPUTIME_ID) + timespec value{}; + if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &value) != 0) return 0; + return static_cast(value.tv_sec) * 1'000'000'000ULL + + static_cast(value.tv_nsec); +#else + return 0; +#endif +} template owner allocate_wrapper(Allocate allocate, const char* message) { owner resource = allocate(); @@ -164,19 +196,23 @@ private: std::atomic_bool submitted{}; std::atomic_int32_t submission_result{VK_NOT_READY}; std::atomic_int32_t completion_result{VK_NOT_READY}; + std::uint64_t queue_started_ns{}; + std::uint64_t queue_finished_ns{}; }; static std::int32_t submit_immediate( void* user_data, VkQueue queue, - const VkSubmitInfo2* submit_info) noexcept { + const VkSubmitInfo2* submit_info, + DvzCommandSubmitTiming* timing) noexcept { auto* context = static_cast(user_data); if (context == nullptr || queue == VK_NULL_HANDLE || submit_info == nullptr) return VK_ERROR_INITIALIZATION_FAILED; - try { return context->submit_immediate(queue, *submit_info); } + try { return context->submit_immediate(queue, *submit_info, timing); } catch (const std::bad_alloc&) { return VK_ERROR_OUT_OF_HOST_MEMORY; } catch (...) { return VK_ERROR_INITIALIZATION_FAILED; } } std::int32_t submit_immediate( - VkQueue queue, const VkSubmitInfo2& source) { + VkQueue queue, const VkSubmitInfo2& source, + DvzCommandSubmitTiming* timing) { if (source.commandBufferInfoCount == 0 || source.commandBufferInfoCount > DVZ_MAX_SWAPCHAIN_IMAGES || source.waitSemaphoreInfoCount != 0 || @@ -190,13 +226,19 @@ private: submission->submit.pCommandBufferInfos = submission->commands.data(); const VkFenceCreateInfo fence_info{ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + const auto fence_started = trace_now_ns(); const VkResult fence_result = vkCreateFence( submission->device, &fence_info, nullptr, &submission->fence); + if (timing != nullptr) + timing->fence_create_ns = trace_now_ns() - fence_started; if (fence_result != VK_SUCCESS) return fence_result; + const auto enqueue_started = trace_now_ns(); try { enqueue_submission([submission, queue]() noexcept { + submission->queue_started_ns = trace_now_ns(); const VkResult result = vkQueueSubmit2( queue, 1, &submission->submit, submission->fence); + submission->queue_finished_ns = trace_now_ns(); submission->submission_result.store( result, std::memory_order_relaxed); submission->submitted.store(true, std::memory_order_release); @@ -205,6 +247,9 @@ private: catch (...) { return VK_ERROR_OUT_OF_HOST_MEMORY; } + const auto enqueue_finished = trace_now_ns(); + if (timing != nullptr) + timing->enqueue_ns = enqueue_finished - enqueue_started; try { /* * 一个协作等待同时覆盖“排到唯一 VkQueue 提交入口”和“Fence 完成”。 @@ -233,6 +278,21 @@ private: /* 已入队命令的栈外资源不能提前释放;违反 Worker 前置条件时立即终止。 */ std::terminate(); } + if (timing != nullptr) { + const auto queue_started = submission->queue_started_ns; + const auto queue_finished = submission->queue_finished_ns; + timing->queue_wait_ns = queue_started > enqueue_finished + ? queue_started - enqueue_finished + : 0; + timing->queue_submit_ns = queue_finished > queue_started + ? queue_finished - queue_started + : 0; + const auto completion_observed = trace_now_ns(); + timing->fence_wait_ns = + queue_finished != 0 && completion_observed > queue_finished + ? completion_observed - queue_finished + : 0; + } return submission->completion_result.load(std::memory_order_relaxed); } Datoviz_Render_Context(std::uint32_t gpu_index, bool validation_enabled) { 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 2ea5523..ba97926 100644 --- a/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h +++ b/render_3D/third_party/datoviz/include/datoviz/drp2/runtime.h @@ -81,6 +81,17 @@ struct DvzDrp2ExecutionTiming uint64_t state_ensure_ns; uint64_t shader_compile_ns; uint64_t shader_module_create_ns; + uint64_t upload_decode_ns; + uint64_t upload_vulkan_allocate_ns; + uint64_t upload_host_copy_ns; + uint64_t upload_command_allocate_ns; + uint64_t upload_command_record_ns; + uint64_t upload_fence_create_ns; + uint64_t upload_submit_enqueue_ns; + uint64_t upload_submit_queue_wait_ns; + uint64_t upload_queue_submit_ns; + uint64_t upload_fence_wait_ns; + uint64_t upload_retire_ns; uint64_t command_ns[DVZ_DRP2_COMMAND_TYPE_COUNT]; uint32_t command_count[DVZ_DRP2_COMMAND_TYPE_COUNT]; uint64_t state_commit_ns; diff --git a/render_3D/third_party/datoviz/include/datoviz/scene/types.h b/render_3D/third_party/datoviz/include/datoviz/scene/types.h index 5748603..148e0a9 100644 --- a/render_3D/third_party/datoviz/include/datoviz/scene/types.h +++ b/render_3D/third_party/datoviz/include/datoviz/scene/types.h @@ -48,6 +48,7 @@ typedef struct DvzCapabilitySnapshot DvzCapabilitySnapshot; typedef struct DvzDiagnosticReport DvzDiagnosticReport; typedef struct DvzFramePlanEmitter DvzFramePlanEmitter; typedef struct DvzFramePlanEmitConfig DvzFramePlanEmitConfig; +typedef struct DvzSceneFrameEmitTiming DvzSceneFrameEmitTiming; typedef struct DvzFramePlan DvzFramePlan; typedef struct DvzFramePlanNode DvzFramePlanNode; typedef struct DvzSceneFrameArtifact DvzSceneFrameArtifact; @@ -259,6 +260,24 @@ struct DvzDiagnosticReport }; +struct DvzSceneFrameEmitTiming +{ + uint64_t replay_dirty_ns; + uint64_t layout_ns; + uint64_t prepare_ns; + uint64_t plan_build_ns; + uint64_t contract_ns; + uint64_t stream_emit_ns; + uint64_t stream_freeze_ns; + uint64_t commit_ns; + uint64_t plan_reset_ns; + uint64_t artifact_create_ns; + uint64_t artifact_freeze_ns; + uint64_t artifact_encode_ns; +}; + + + struct DvzFramePlanEmitConfig { uint32_t struct_size; @@ -277,6 +296,7 @@ struct DvzFramePlanEmitConfig bool fullscreen_triangle; uint64_t runtime_resource_scope_id; /* Optional scope for mutable runtime intermediates. */ float clear_color[4]; /* RGBA clear color for the render pass [0,1]; default opaque black */ + DvzSceneFrameEmitTiming* timing; /* Nullable borrowed output, written only during emission. */ }; 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 e2b3dd2..40688ee 100644 --- a/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h +++ b/render_3D/third_party/datoviz/include/datoviz/vklite/commands.h @@ -39,13 +39,24 @@ typedef struct DvzCommands DvzCommands; typedef struct DvzDevice DvzDevice; typedef struct DvzQueue DvzQueue; +typedef struct DvzCommandSubmitTiming +{ + uint64_t command_end_ns; + uint64_t fence_create_ns; + uint64_t enqueue_ns; + uint64_t queue_wait_ns; + uint64_t queue_submit_ns; + uint64_t fence_wait_ns; +} DvzCommandSubmitTiming; + /* * Submit one immediate command batch through the host application's queue authority. * user_data is nullable borrowed state and must remain valid until the strategy returns. * The strategy returns only after the submitted command buffers are safe to release. */ typedef int32_t (*DvzCommandSubmitStrategy)( - void* user_data, VkQueue queue, const VkSubmitInfo2* submit_info); + void* user_data, VkQueue queue, const VkSubmitInfo2* submit_info, + DvzCommandSubmitTiming* timing); @@ -252,7 +263,8 @@ DVZ_EXPORT int dvz_cmd_submit_result(DvzCommands* cmds); * @return 0 on success, non-zero on Vulkan or state failure */ DVZ_EXPORT int dvz_cmd_submit_strategy_result( - DvzCommands* cmds, DvzCommandSubmitStrategy strategy, void* user_data); + DvzCommands* cmds, DvzCommandSubmitStrategy strategy, void* user_data, + DvzCommandSubmitTiming* timing); diff --git a/render_3D/third_party/datoviz/src/drp2/_runtime.h b/render_3D/third_party/datoviz/src/drp2/_runtime.h index 3463c00..86d357d 100644 --- a/render_3D/third_party/datoviz/src/drp2/_runtime.h +++ b/render_3D/third_party/datoviz/src/drp2/_runtime.h @@ -329,7 +329,8 @@ DvzCommands* _vklite_borrowed_frame_commands_create( DvzDevice* device, VkCommandBuffer command_buffer); void _vklite_borrowed_frame_commands_free(DvzCommands* cmds); DvzDrp2ValidationResult _vklite_owned_commands_end_submit( - DvzDrp2Runtime* runtime, DvzCommands* cmds, uint32_t command_index); + DvzDrp2Runtime* runtime, DvzCommands* cmds, uint32_t command_index, + DvzCommandSubmitTiming* timing); VkImageLayout _vklite_texture_access_layout(Drp2TextureAccess access); void _vklite_texture_access_scope( Drp2TextureAccess access, VkPipelineStageFlags2* stage, VkAccessFlags2* access_mask); @@ -357,9 +358,11 @@ DvzDrp2ValidationResult _vklite_create_render_pipeline( DvzDrp2ValidationResult _vklite_create_compute_pipeline( Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index); DvzDrp2ValidationResult _vklite_write_buffer( - Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index); + Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index, + DvzDrp2ExecutionTiming* timing); DvzDrp2ValidationResult _vklite_write_texture( - Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index); + Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index, + DvzDrp2ExecutionTiming* timing); DvzDrp2ValidationResult _vklite_copy_buffer_to_buffer( Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index); DvzDrp2ValidationResult _vklite_copy_buffer_to_texture( diff --git a/render_3D/third_party/datoviz/src/drp2/backend.c b/render_3D/third_party/datoviz/src/drp2/backend.c index 4532edb..480fc10 100644 --- a/render_3D/third_party/datoviz/src/drp2/backend.c +++ b/render_3D/third_party/datoviz/src/drp2/backend.c @@ -514,16 +514,22 @@ void _vklite_borrowed_frame_commands_free(DvzCommands* cmds) */ DvzDrp2ValidationResult _vklite_owned_commands_end_submit( - DvzDrp2Runtime* runtime, DvzCommands* cmds, uint32_t command_index) + DvzDrp2Runtime* runtime, DvzCommands* cmds, uint32_t command_index, + DvzCommandSubmitTiming* timing) { ANN(runtime); ANN(cmds); + if (timing != NULL) + *timing = (DvzCommandSubmitTiming){0}; + const uint64_t command_end_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (dvz_cmd_end_result(cmds) != 0) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); + if (timing != NULL) + timing->command_end_ns = dvz_time_monotonic_ns() - command_end_started; const int submit_result = runtime->submit_strategy != NULL ? dvz_cmd_submit_strategy_result( cmds, runtime->submit_strategy, - runtime->submit_user_data) + runtime->submit_user_data, timing) : dvz_cmd_submit_result(cmds); if (submit_result != 0) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); @@ -652,10 +658,10 @@ _vklite_execute( state, command->u.destroy_bind_group.bind_group_id, DRP2_OBJECT_BIND_GROUP, i); break; case DVZ_DRP2_COMMAND_WRITE_BUFFER: - result = _vklite_write_buffer(state, command, i); + result = _vklite_write_buffer(state, command, i, timing); break; case DVZ_DRP2_COMMAND_WRITE_TEXTURE: - result = _vklite_write_texture(state, command, i); + result = _vklite_write_texture(state, command, i, timing); break; case DVZ_DRP2_COMMAND_COPY_BUFFER_TO_BUFFER: result = _vklite_copy_buffer_to_buffer(state, command, i); diff --git a/render_3D/third_party/datoviz/src/drp2/pass.c b/render_3D/third_party/datoviz/src/drp2/pass.c index 5968962..1c9f24b 100644 --- a/render_3D/third_party/datoviz/src/drp2/pass.c +++ b/render_3D/third_party/datoviz/src/drp2/pass.c @@ -1429,7 +1429,7 @@ DvzDrp2ValidationResult _vklite_resource_barrier( dvz_cmd_barriers(cmds, &barriers); DvzDrp2ValidationResult result = - _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index, NULL); _vklite_owned_commands_destroy(cmds); return result; } @@ -1506,7 +1506,7 @@ _vklite_end_render_pass(Drp2VkliteState* state, uint64_t pass_id, uint32_t comma { DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit( - state->runtime, pass->commands, command_index); + state->runtime, pass->commands, command_index, NULL); if (!result.ok) { _vklite_destroy_object_slot(state, pass); @@ -1541,7 +1541,7 @@ _vklite_end_compute_pass(Drp2VkliteState* state, uint64_t pass_id, uint32_t comm return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit( - state->runtime, pass->commands, command_index); + state->runtime, pass->commands, command_index, NULL); if (!result.ok) { _vklite_destroy_object_slot(state, pass); diff --git a/render_3D/third_party/datoviz/src/drp2/transfer.c b/render_3D/third_party/datoviz/src/drp2/transfer.c index ac1fd9f..c5f3612 100644 --- a/render_3D/third_party/datoviz/src/drp2/transfer.c +++ b/render_3D/third_party/datoviz/src/drp2/transfer.c @@ -301,7 +301,8 @@ static bool _vklite_create_staging_buffer( DvzDrp2ValidationResult _vklite_write_buffer( - Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index) + Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index, + DvzDrp2ExecutionTiming* timing) { ANN(state); ANN(command); @@ -318,14 +319,23 @@ DvzDrp2ValidationResult _vklite_write_buffer( if (command->u.write_buffer.data_raw != NULL) { + const uint64_t started = timing != NULL ? dvz_time_monotonic_ns() : 0; dvz_buffer_upload(object->buffer, offset, size, command->u.write_buffer.data_raw); + if (timing != NULL) + timing->upload_host_copy_ns += dvz_time_monotonic_ns() - started; } else if (command->u.write_buffer.data_base64 != NULL) { uint8_t* data = NULL; + const uint64_t decode_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (!_dvz_b64_decode_exact(command->u.write_buffer.data_base64, size, &data)) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_ARGUMENT, command_index); + if (timing != NULL) + timing->upload_decode_ns += dvz_time_monotonic_ns() - decode_started; + const uint64_t copy_started = timing != NULL ? dvz_time_monotonic_ns() : 0; dvz_buffer_upload(object->buffer, offset, size, data); + if (timing != NULL) + timing->upload_host_copy_ns += dvz_time_monotonic_ns() - copy_started; dvz_free(data); } else @@ -337,7 +347,8 @@ DvzDrp2ValidationResult _vklite_write_buffer( DvzDrp2ValidationResult _vklite_write_texture( - Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index) + Drp2VkliteState* state, const DvzDrp2Command* command, uint32_t command_index, + DvzDrp2ExecutionTiming* timing) { ANN(state); ANN(command); @@ -360,21 +371,31 @@ DvzDrp2ValidationResult _vklite_write_texture( uint8_t* decoded = NULL; if (upload_src == NULL) { + const uint64_t decode_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (command->u.write_texture.data_base64 == NULL || !_dvz_b64_decode_exact(command->u.write_texture.data_base64, size, &decoded)) return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_ARGUMENT, command_index); + if (timing != NULL) + timing->upload_decode_ns += dvz_time_monotonic_ns() - decode_started; upload_src = decoded; } DvzBuffer* staging = NULL; + const uint64_t staging_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (!_vklite_create_staging_buffer(state, size, &staging, VK_BUFFER_USAGE_TRANSFER_SRC_BIT)) { dvz_free(decoded); return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); } + if (timing != NULL) + timing->upload_vulkan_allocate_ns += dvz_time_monotonic_ns() - staging_started; + const uint64_t copy_started = timing != NULL ? dvz_time_monotonic_ns() : 0; dvz_buffer_upload(staging, 0, size, upload_src); + if (timing != NULL) + timing->upload_host_copy_ns += dvz_time_monotonic_ns() - copy_started; dvz_free(decoded); + const uint64_t command_allocate_started = timing != NULL ? dvz_time_monotonic_ns() : 0; DvzCommands* cmds = _vklite_owned_commands_create(state->runtime); if (cmds == NULL) { @@ -382,6 +403,9 @@ DvzDrp2ValidationResult _vklite_write_texture( dvz_buffer_free(staging); return _drp2_fail(DVZ_DRP2_VALIDATION_INVALID_STATE, command_index); } + if (timing != NULL) + timing->upload_command_allocate_ns += + dvz_time_monotonic_ns() - command_allocate_started; DvzImageRegion region = {0}; _vklite_region( @@ -392,6 +416,7 @@ DvzDrp2ValidationResult _vklite_write_texture( ®ion, command->u.write_texture.origin_x, command->u.write_texture.origin_y, command->u.write_texture.origin_z); + const uint64_t record_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (dvz_cmd_begin_result(cmds) != 0) { _vklite_owned_commands_destroy(cmds); @@ -403,8 +428,20 @@ DvzDrp2ValidationResult _vklite_write_texture( dvz_cmd_copy_buffer_to_image( cmds, dvz_buffer_handle(staging), 0, dvz_image_handle(texture->images, 0), _vklite_texture_access_layout(DRP2_TEXTURE_ACCESS_TRANSFER_WRITE), ®ion); - DvzDrp2ValidationResult result = - _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); + if (timing != NULL) + timing->upload_command_record_ns += dvz_time_monotonic_ns() - record_started; + DvzCommandSubmitTiming submit_timing = {0}; + DvzDrp2ValidationResult result = _vklite_owned_commands_end_submit( + state->runtime, cmds, command_index, timing != NULL ? &submit_timing : NULL); + if (timing != NULL) + { + timing->upload_command_record_ns += submit_timing.command_end_ns; + timing->upload_fence_create_ns += submit_timing.fence_create_ns; + timing->upload_submit_enqueue_ns += submit_timing.enqueue_ns; + timing->upload_submit_queue_wait_ns += submit_timing.queue_wait_ns; + timing->upload_queue_submit_ns += submit_timing.queue_submit_ns; + timing->upload_fence_wait_ns += submit_timing.fence_wait_ns; + } if (!result.ok) { _vklite_owned_commands_destroy(cmds); @@ -413,9 +450,12 @@ DvzDrp2ValidationResult _vklite_write_texture( return result; } + const uint64_t retire_started = timing != NULL ? dvz_time_monotonic_ns() : 0; _vklite_owned_commands_destroy(cmds); dvz_buffer_destroy(staging); dvz_buffer_free(staging); + if (timing != NULL) + timing->upload_retire_ns += dvz_time_monotonic_ns() - retire_started; return _drp2_ok(); } @@ -448,7 +488,7 @@ DvzDrp2ValidationResult _vklite_copy_buffer_to_buffer( dvz_commands_handle(cmds), dvz_buffer_handle(src->buffer), dvz_buffer_handle(dst->buffer), 1, ®ion); DvzDrp2ValidationResult result = - _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index, NULL); if (!result.ok) { _vklite_owned_commands_destroy(cmds); @@ -498,7 +538,7 @@ DvzDrp2ValidationResult _vklite_copy_buffer_to_texture( dvz_image_handle(dst->images, 0), _vklite_texture_access_layout(DRP2_TEXTURE_ACCESS_TRANSFER_WRITE), ®ion); DvzDrp2ValidationResult result = - _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index, NULL); if (!result.ok) { _vklite_owned_commands_destroy(cmds); @@ -545,7 +585,7 @@ DvzDrp2ValidationResult _vklite_copy_texture_to_buffer( _vklite_texture_access_layout(DRP2_TEXTURE_ACCESS_TRANSFER_READ), ®ion, dvz_buffer_handle(dst->buffer), command->u.copy_texture_to_buffer.dst_offset); DvzDrp2ValidationResult result = - _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index, NULL); if (!result.ok) { _vklite_owned_commands_destroy(cmds); @@ -602,7 +642,7 @@ DvzDrp2ValidationResult _vklite_copy_texture_to_texture( _vklite_transition_image_access(cmds, dst, DRP2_TEXTURE_ACCESS_TRANSFER_WRITE); dvz_cmd_copy_image(cmds, copy); DvzDrp2ValidationResult result = - _vklite_owned_commands_end_submit(state->runtime, cmds, command_index); + _vklite_owned_commands_end_submit(state->runtime, cmds, command_index, NULL); if (!result.ok) { dvz_image_copy_free(copy); 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 987c335..78d97ad 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 @@ -27,6 +27,7 @@ #include "_log.h" #include "scene_emit/scene_emit.h" #include "_technique.h" +#include "datoviz/common/functions.h" #include "datoviz/drp2/runtime.h" #include "../../drp2/_stream.h" #include "render_contract/render_contract.h" @@ -694,8 +695,10 @@ DvzDrp2CommandStream* _scene_figure_emit_stream_with_emitter_ex( DvzDiagnosticReport local_report; DvzFramePlanEmitConfig default_cfg; _scene_emit_defaults(&caps, &default_caps, &report, &local_report, &cfg, &default_cfg); + DvzSceneFrameEmitTiming* timing = cfg->timing; if (!dvz_capability_snapshot_valid(caps)) return NULL; + uint64_t phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; float next_device_scale_x = _scene_scale_or_one(cfg->device_scale_x); float next_device_scale_y = _scene_scale_or_one(cfg->device_scale_y); float next_render_scale = _scene_scale_or_one(cfg->render_scale); @@ -714,31 +717,53 @@ DvzDrp2CommandStream* _scene_figure_emit_stream_with_emitter_ex( if (!_scene_figure_resolve_layouts(figure)) { + if (timing != NULL) + timing->layout_ns += dvz_time_monotonic_ns() - phase_started; (void)dvz_diagnostic_report_add(report, "scene grid layout resolution failed"); return NULL; } + if (timing != NULL) + timing->layout_ns += dvz_time_monotonic_ns() - phase_started; char figure_id[64]; _scene_figure_id(figure, figure_id, sizeof(figure_id)); + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (!_scene_figure_validate_transparency_modes(figure, figure_id, report)) + { + if (timing != NULL) + timing->contract_ns += dvz_time_monotonic_ns() - phase_started; return NULL; + } + if (timing != NULL) + timing->contract_ns += dvz_time_monotonic_ns() - phase_started; + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; for (uint32_t pi = 0; pi < figure->panel_count; pi++) (void)_scene_panel_sync_fly_pivot_marker(&figure->panels[pi]); _scene_prepare_guide_visuals(figure); _scene_prepare_bars_visuals(figure); _scene_prepare_band_visuals(figure); + if (timing != NULL) + timing->prepare_ns += dvz_time_monotonic_ns() - phase_started; - DvzFramePlan* plan = dvz_frame_plan(figure_id, 0); + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; + DvzFramePlan* plan = emitter->scratch_plan; if (plan == NULL) + { + if (timing != NULL) + timing->plan_build_ns += dvz_time_monotonic_ns() - phase_started; return NULL; + } + _frame_plan_reset(plan, figure_id, 0); _scene_emit_visual_uploads(figure, plan, report); if (!_scene_emit_compute_passes(figure, plan, report)) { + if (timing != NULL) + timing->plan_build_ns += dvz_time_monotonic_ns() - phase_started; (void)dvz_diagnostic_report_add(report, "scene compute FramePlan emission failed"); - dvz_frame_plan_destroy(plan); + _frame_plan_reset(plan, "", 0); return NULL; } @@ -749,16 +774,23 @@ DvzDrp2CommandStream* _scene_figure_emit_stream_with_emitter_ex( _scene_emit_panel_render_caps(figure, pi, plan, figure_id, caps, report) && panels_ok; if (!panels_ok) { + if (timing != NULL) + timing->plan_build_ns += dvz_time_monotonic_ns() - phase_started; if (dvz_diagnostic_report_count(report) == graph_report_start) (void)dvz_diagnostic_report_add(report, "scene FramePlan graph emission failed"); - dvz_frame_plan_destroy(plan); + _frame_plan_reset(plan, "", 0); return NULL; } + if (timing != NULL) + timing->plan_build_ns += dvz_time_monotonic_ns() - phase_started; DvzDiagnosticReport contract_report; dvz_diagnostic_report_init(&contract_report); + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; bool contracts_ok = _scene_frame_plan_contracts_validate_ex(figure, plan, caps, &contract_report); + if (timing != NULL) + timing->contract_ns += dvz_time_monotonic_ns() - phase_started; if (!contracts_ok) { for (uint32_t i = 0; i < dvz_diagnostic_report_count(&contract_report); i++) @@ -771,7 +803,7 @@ DvzDrp2CommandStream* _scene_figure_emit_stream_with_emitter_ex( report, dvz_diagnostic_report_get_severity(&contract_report, i), message); } } - dvz_frame_plan_destroy(plan); + _frame_plan_reset(plan, "", 0); return NULL; } @@ -779,18 +811,30 @@ DvzDrp2CommandStream* _scene_figure_emit_stream_with_emitter_ex( _scene_frame_plan_trace(figure, plan); + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; DvzDrp2CommandStream* stream = dvz_frame_plan_emitter_emit_drp2(emitter, plan, caps, report, cfg); + if (timing != NULL) + timing->stream_emit_ns += dvz_time_monotonic_ns() - phase_started; + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (stream != NULL && !_scene_freeze_stream_payloads(stream)) { (void)dvz_diagnostic_report_add(report, "scene DRP2 stream payload freeze failed"); dvz_drp2_stream_destroy(stream); stream = NULL; } + if (timing != NULL) + timing->stream_freeze_ns += dvz_time_monotonic_ns() - phase_started; + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (stream != NULL) _scene_commit_emit_success(figure); + if (timing != NULL) + timing->commit_ns += dvz_time_monotonic_ns() - phase_started; - dvz_frame_plan_destroy(plan); + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; + _frame_plan_reset(plan, "", 0); + if (timing != NULL) + timing->plan_reset_ns += dvz_time_monotonic_ns() - phase_started; return stream; } @@ -855,8 +899,14 @@ DvzSceneFrameArtifact* dvz_figure_emit_frame_with_emitter( ANN(figure); ANN(figure->scene); ANN(emitter); + DvzSceneFrameEmitTiming* timing = cfg != NULL ? cfg->timing : NULL; + if (timing != NULL) + *timing = (DvzSceneFrameEmitTiming){0}; + uint64_t phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (replay_payloads) _scene_mark_runtime_payloads_dirty(figure->scene); + if (timing != NULL) + timing->replay_dirty_ns += dvz_time_monotonic_ns() - phase_started; const uint64_t resource_version = figure->artifact_resource_version + 1; const uint64_t frame_index = figure->artifact_frame_index + 1; @@ -865,7 +915,7 @@ DvzSceneFrameArtifact* dvz_figure_emit_frame_with_emitter( if (stream == NULL) return NULL; DvzSceneFrameArtifact* artifact = - _scene_frame_artifact(stream, resource_version, frame_index); + _scene_frame_artifact(stream, resource_version, frame_index, timing); if (artifact != NULL) { figure->artifact_resource_version = resource_version; @@ -1011,16 +1061,17 @@ DvzDrp2CommandStream* dvz_figure_prepare_runtime_resources( char figure_id[64]; _scene_figure_id(figure, figure_id, sizeof(figure_id)); - DvzFramePlan* plan = dvz_frame_plan(figure_id, 0); + DvzFramePlan* plan = emitter->scratch_plan; if (plan == NULL) return NULL; + _frame_plan_reset(plan, figure_id, 0); _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); + _frame_plan_reset(plan, "", 0); return NULL; } @@ -1065,7 +1116,7 @@ DvzDrp2CommandStream* dvz_figure_prepare_runtime_resources( if (command_recording_valid != NULL) *command_recording_valid = recording_valid; } - dvz_frame_plan_destroy(plan); + _frame_plan_reset(plan, "", 0); if (!command_set_valid) { dvz_drp2_stream_destroy(stream); diff --git a/render_3D/third_party/datoviz/src/scene/core/frame_artifact.c b/render_3D/third_party/datoviz/src/scene/core/frame_artifact.c index 6d9c177..ab8f4d7 100644 --- a/render_3D/third_party/datoviz/src/scene/core/frame_artifact.c +++ b/render_3D/third_party/datoviz/src/scene/core/frame_artifact.c @@ -21,6 +21,7 @@ #include "_assertions.h" #include "_overflow.h" #include "datoviz/drp2.h" +#include "datoviz/common/functions.h" #include "datoviz/scene.h" #include "figure_emit_internal.h" #include "frame_artifact_internal.h" @@ -132,11 +133,13 @@ bool _scene_freeze_stream_payloads(DvzDrp2CommandStream* stream) * Create a frame artifact that owns one DRP2 command stream and encoded split packets. */ DvzSceneFrameArtifact* _scene_frame_artifact( - DvzDrp2CommandStream* stream, uint64_t resource_version, uint64_t frame_index) + DvzDrp2CommandStream* stream, uint64_t resource_version, uint64_t frame_index, + DvzSceneFrameEmitTiming* timing) { if (stream == NULL) return NULL; + uint64_t phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; DvzSceneFrameArtifact* artifact = (DvzSceneFrameArtifact*)dvz_calloc(1, sizeof(DvzSceneFrameArtifact)); if (artifact == NULL) @@ -149,12 +152,17 @@ DvzSceneFrameArtifact* _scene_frame_artifact( artifact->resource_version = resource_version; artifact->frame_index = frame_index; artifact->stream = stream; + if (timing != NULL) + timing->artifact_create_ns += dvz_time_monotonic_ns() - phase_started; + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; if (!_scene_freeze_stream_payloads(stream)) { artifact->status = DVZ_SCENE_FRAME_ARTIFACT_STATUS_ENCODE_ERROR; return artifact; } + if (timing != NULL) + timing->artifact_freeze_ns += dvz_time_monotonic_ns() - phase_started; _dvz_drp2_stream_release_owner(stream); const DvzDrp2PacketKind phases[3] = { @@ -162,6 +170,7 @@ DvzSceneFrameArtifact* _scene_frame_artifact( DVZ_DRP2_PACKET_UPDATE, DVZ_DRP2_PACKET_FRAME, }; + phase_started = timing != NULL ? dvz_time_monotonic_ns() : 0; for (uint32_t i = 0; i < 3; i++) { PacketSpan* span = _span(artifact, phases[i]); @@ -174,6 +183,8 @@ DvzSceneFrameArtifact* _scene_frame_artifact( break; } } + if (timing != NULL) + timing->artifact_encode_ns += dvz_time_monotonic_ns() - phase_started; return artifact; } @@ -190,7 +201,8 @@ DvzSceneFrameArtifact* _scene_emit_frame_artifact( if (stream == NULL) return NULL; - return _scene_frame_artifact(stream, resource_version, frame_index); + DvzSceneFrameEmitTiming* timing = cfg != NULL ? cfg->timing : NULL; + return _scene_frame_artifact(stream, resource_version, frame_index, timing); } diff --git a/render_3D/third_party/datoviz/src/scene/core/frame_artifact_internal.h b/render_3D/third_party/datoviz/src/scene/core/frame_artifact_internal.h index 9900aac..1543f5c 100644 --- a/render_3D/third_party/datoviz/src/scene/core/frame_artifact_internal.h +++ b/render_3D/third_party/datoviz/src/scene/core/frame_artifact_internal.h @@ -40,7 +40,8 @@ * @return the owned frame artifact, or NULL */ DvzSceneFrameArtifact* _scene_frame_artifact( - DvzDrp2CommandStream* stream, uint64_t resource_version, uint64_t frame_index); + DvzDrp2CommandStream* stream, uint64_t resource_version, uint64_t frame_index, + DvzSceneFrameEmitTiming* timing); /** diff --git a/render_3D/third_party/datoviz/src/scene/frame_plan/core.c b/render_3D/third_party/datoviz/src/scene/frame_plan/core.c index 586b06a..90373d6 100644 --- a/render_3D/third_party/datoviz/src/scene/frame_plan/core.c +++ b/render_3D/third_party/datoviz/src/scene/frame_plan/core.c @@ -81,6 +81,36 @@ DvzFramePlan* dvz_frame_plan(const char* figure_id, uint64_t frame_index) +/** + * Clear one FramePlan while retaining its node and graph storage capacities. + * + * Upload-owned payloads belong to the current plan generation and are released here. The retained + * arrays are storage only: every appended node/resource/pass is overwritten before it becomes + * visible in the next generation. + * + * @param plan the FramePlan + * @param figure_id the next figure id + * @param frame_index the next frame index + */ +void _frame_plan_reset(DvzFramePlan* plan, const char* figure_id, uint64_t frame_index) +{ + ANN(plan); + for (uint32_t i = 0; i < plan->count; i++) + { + if (plan->nodes[i].type != DVZ_FRAME_PLAN_NODE_UPLOAD) + continue; + dvz_free(plan->nodes[i].u.upload.owned_data); + plan->nodes[i].u.upload.owned_data = NULL; + } + _frame_plan_copy_label(plan->figure_id, DVZ_SCENE_LABEL_SIZE, figure_id ? figure_id : ""); + plan->frame_index = frame_index; + plan->count = 0; + plan->graph_resource_count = 0; + plan->graph_pass_count = 0; +} + + + /** * Destroy a FramePlan. * @@ -90,14 +120,7 @@ void dvz_frame_plan_destroy(DvzFramePlan* plan) { if (plan == NULL) return; - for (uint32_t i = 0; i < plan->count; i++) - { - if (plan->nodes[i].type == DVZ_FRAME_PLAN_NODE_UPLOAD) - { - dvz_free(plan->nodes[i].u.upload.owned_data); - plan->nodes[i].u.upload.owned_data = NULL; - } - } + _frame_plan_reset(plan, "", 0); dvz_free(plan->nodes); dvz_free(plan->graph_resources); dvz_free(plan->graph_passes); 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 360781e..d25cedb 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 @@ -142,6 +142,7 @@ struct DvzFramePlanEmitter { ConverterState resources; ConverterState objects; + DvzFramePlan* scratch_plan; /* Owned reusable storage; reset before every complete emission. */ uint64_t next_transient_id; bool handshake_sent; uint32_t max_color_sample_count; diff --git a/render_3D/third_party/datoviz/src/scene/frame_plan/frame_plan.h b/render_3D/third_party/datoviz/src/scene/frame_plan/frame_plan.h index 8fcf633..a5fc708 100644 --- a/render_3D/third_party/datoviz/src/scene/frame_plan/frame_plan.h +++ b/render_3D/third_party/datoviz/src/scene/frame_plan/frame_plan.h @@ -541,6 +541,8 @@ struct DvzFramePlan /* Internal helpers */ /*************************************************************************************************/ +void _frame_plan_reset(DvzFramePlan* plan, const char* figure_id, uint64_t frame_index); + bool dvz_frame_plan_render_panel( DvzFramePlan* plan, const char* panel_id, const char* render_target_id, bool picking, DvzPanelDesc desc); diff --git a/render_3D/third_party/datoviz/src/scene/runtime/state.c b/render_3D/third_party/datoviz/src/scene/runtime/state.c index 183362d..f6eec63 100644 --- a/render_3D/third_party/datoviz/src/scene/runtime/state.c +++ b/render_3D/third_party/datoviz/src/scene/runtime/state.c @@ -645,6 +645,14 @@ DvzFramePlanEmitter* dvz_frame_plan_emitter(void) return NULL; _state_init(&emitter->resources); _state_init(&emitter->objects); + emitter->scratch_plan = dvz_frame_plan("", 0); + if (emitter->scratch_plan == NULL) + { + _state_destroy(&emitter->resources); + _state_destroy(&emitter->objects); + dvz_free(emitter); + return NULL; + } emitter->objects.next_id = DRP2_EMITTER_OBJECT_ID_BASE; emitter->next_transient_id = DRP2_RUNTIME_TRANSIENT_ID_BASE; emitter->max_color_sample_count = 16; @@ -663,6 +671,8 @@ void dvz_frame_plan_emitter_destroy(DvzFramePlanEmitter* emitter) { if (emitter == NULL) return; + dvz_frame_plan_destroy(emitter->scratch_plan); + emitter->scratch_plan = NULL; _state_destroy(&emitter->resources); _state_destroy(&emitter->objects); dvz_free(emitter); diff --git a/render_3D/third_party/datoviz/src/vklite/commands.c b/render_3D/third_party/datoviz/src/vklite/commands.c index b40aaed..2205c60 100644 --- a/render_3D/third_party/datoviz/src/vklite/commands.c +++ b/render_3D/third_party/datoviz/src/vklite/commands.c @@ -368,7 +368,8 @@ void dvz_cmd_release(DvzCommands* cmds) * @return 0 on success, non-zero on Vulkan or state failure */ static int _cmd_submit_result( - DvzCommands* cmds, DvzCommandSubmitStrategy strategy, void* user_data) + DvzCommands* cmds, DvzCommandSubmitStrategy strategy, void* user_data, + DvzCommandSubmitTiming* timing) { ANN(cmds); ASSERT(cmds->count > 0); @@ -411,18 +412,31 @@ static int _cmd_submit_result( VkResult res = VK_SUCCESS; if (strategy != NULL) { - res = (VkResult)strategy(user_data, vk_queue, &info); + res = (VkResult)strategy(user_data, vk_queue, &info, timing); } else { /* Standalone vklite has no host scheduler. Keep its fallback local to this batch. */ VkFenceCreateInfo fence_info = {.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; VkFence fence = VK_NULL_HANDLE; + uint64_t started = timing != NULL ? dvz_time_monotonic_ns() : 0; res = vkCreateFence(dvz_device_handle(device), &fence_info, NULL, &fence); + if (timing != NULL) + timing->fence_create_ns = dvz_time_monotonic_ns() - started; if (res == VK_SUCCESS) + { + started = timing != NULL ? dvz_time_monotonic_ns() : 0; res = vkQueueSubmit2(vk_queue, 1, &info, fence); + if (timing != NULL) + timing->queue_submit_ns = dvz_time_monotonic_ns() - started; + } if (res == VK_SUCCESS) + { + started = timing != NULL ? dvz_time_monotonic_ns() : 0; res = vkWaitForFences(dvz_device_handle(device), 1, &fence, VK_TRUE, UINT64_MAX); + if (timing != NULL) + timing->fence_wait_ns = dvz_time_monotonic_ns() - started; + } if (fence != VK_NULL_HANDLE) vkDestroyFence(dvz_device_handle(device), fence, NULL); } @@ -439,20 +453,21 @@ static int _cmd_submit_result( int dvz_cmd_submit_result(DvzCommands* cmds) { - return _cmd_submit_result(cmds, NULL, NULL); + return _cmd_submit_result(cmds, NULL, NULL, NULL); } int dvz_cmd_submit_strategy_result( - DvzCommands* cmds, DvzCommandSubmitStrategy strategy, void* user_data) + DvzCommands* cmds, DvzCommandSubmitStrategy strategy, void* user_data, + DvzCommandSubmitTiming* timing) { if (strategy == NULL) { log_error("command submission strategy is required"); return 1; } - return _cmd_submit_result(cmds, strategy, user_data); + return _cmd_submit_result(cmds, strategy, user_data, timing); } diff --git a/web_server/benchmarks/Drogon_WebSocket_Receiver_Benchmark.cpp b/web_server/benchmarks/Drogon_WebSocket_Receiver_Benchmark.cpp index 8c38405..c2e583d 100644 --- a/web_server/benchmarks/Drogon_WebSocket_Receiver_Benchmark.cpp +++ b/web_server/benchmarks/Drogon_WebSocket_Receiver_Benchmark.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -38,19 +39,39 @@ struct State final { std::vector latency_ms{}; std::uint64_t accept(std::string_view packet) { - if (packet.size() < 56 || packet.substr(0, 4) != "AERP" || - read_le(packet.data() + 4) != 2 || - read_le(packet.data() + 6) != 56) + if (packet.size() < 40 || packet.substr(0, 4) != "AERP" || + read_le(packet.data() + 4) != 4 || + read_le(packet.data() + 6) != 40) throw std::runtime_error("invalid Aethera pixel packet"); - const auto payload = read_le(packet.data() + 24); - if (packet.size() != 56U + payload) + const auto tile_count = read_le(packet.data() + 8); + const auto payload = read_le(packet.data() + 12); + if (tile_count == 0 || packet.size() != 40U + payload) throw std::runtime_error("invalid Aethera pixel payload size"); - const auto sequence = read_le(packet.data() + 28); - const auto packed_ns = read_le(packet.data() + 44); + std::size_t offset{40}; + for (std::uint16_t tile = 0; tile < tile_count; ++tile) { + if (offset + 32U > packet.size()) + throw std::runtime_error("truncated Aethera pixel tile"); + const auto width = read_le(packet.data() + offset + 8U); + const auto height = read_le(packet.data() + offset + 12U); + const auto stride = read_le(packet.data() + offset + 16U); + const auto tile_bytes = read_le(packet.data() + offset + 20U); + const auto expected = static_cast(stride) * height; + if (width == 0 || height == 0 || + width > std::numeric_limits::max() / 4U || + stride != width * 4U || + tile_bytes != expected || + offset + 32U + tile_bytes > packet.size()) + throw std::runtime_error("invalid Aethera pixel tile"); + offset += 32U + tile_bytes; + } + if (offset != packet.size()) + throw std::runtime_error("Aethera pixel batch has trailing bytes"); + const auto sequence = read_le(packet.data() + 16); + const auto packed_ns = read_le(packet.data() + 32); const auto now = Clock::now(); const auto now_ns = static_cast( std::chrono::duration_cast( - now.time_since_epoch()).count()); + std::chrono::system_clock::now().time_since_epoch()).count()); if (packed_ns <= now_ns) latency_ms.push_back(static_cast(now_ns - packed_ns) / 1.0e6); diff --git a/web_server/src/Gallery_Pixel_Stream.cpp b/web_server/src/Gallery_Pixel_Stream.cpp index 0a6f21a..d57fb67 100644 --- a/web_server/src/Gallery_Pixel_Stream.cpp +++ b/web_server/src/Gallery_Pixel_Stream.cpp @@ -14,10 +14,9 @@ #include namespace aethera::web { namespace { -// Scene 的业务视口为 720x420;图集保持同一像素分辨率,禁止缩小画布换取帧率。 +// Scene 与浏览器 Plot Canvas 保持同一 720x420 业务分辨率,禁止缩小画布换取帧率。 constexpr std::uint32_t tile_width{720}; constexpr std::uint32_t tile_height{420}; -constexpr std::uint32_t atlas_columns{4}; constexpr double gallery_frame_rate{30.0}; constexpr auto metric_interval{std::chrono::seconds(1)}; nlohmann::json statistic_json(const Statistic_State& value) { @@ -57,7 +56,7 @@ void Gallery_Pixel_Stream::Private::initialize( sources.push_back(Source{std::move(plot)}); } sampler = std::make_unique( - gallery_frame_rate, tile_width, tile_height, atlas_columns, + gallery_frame_rate, tile_width, tile_height, std::move(source_ids), sources.size() - 1U); metric_completion_starts.resize(sources.size()); metric_rendered_starts.resize(sources.size()); @@ -152,7 +151,8 @@ void Gallery_Pixel_Stream::Private::publish( for (const auto& consumer : *current) { if (!consumer.handler) continue; try { - (*consumer.handler)(Gallery_Pixel_Message{pixels, notification}); + static_cast((*consumer.handler)( + Gallery_Pixel_Message{pixels, notification})); } catch (...) { failed_consumers.push_back(consumer.id); @@ -171,7 +171,7 @@ void Gallery_Pixel_Stream::Private::fail( const auto notification = nlohmann::json{ {"kind", "gallery_error"}, {"protocol", "aethera.gallery.pixels"}, - {"version", 4}, + {"version", 6}, {"message", *terminal_failure} }.dump(); publish({}, notification); @@ -188,8 +188,9 @@ void Gallery_Pixel_Stream::Private::accept_frame( void Gallery_Pixel_Stream::Private::update_metrics( const Plot_Render_Tick& tick, const frame_sampling::Sampled_Gallery_Frame& sample, - std::size_t pixel_bytes) { - const auto& composition = sample.composition; + std::size_t pixel_bytes, + std::size_t transmitted_tiles) { + const auto& batch = sample.batch; const auto sampler_state = sampler->state(); const auto now = std::chrono::steady_clock::now(); if (metric_started.time_since_epoch().count() == 0) { @@ -197,11 +198,11 @@ void Gallery_Pixel_Stream::Private::update_metrics( metric_pixel_start = pixel_frame_count; metric_sample_start = sampler_state.sampled_frames; metric_clock_start = sampling_clock_ticks.load(std::memory_order_relaxed); - for (std::size_t slot = 0; slot < composition.sources.size(); ++slot) { + for (std::size_t slot = 0; slot < batch.sources.size(); ++slot) { metric_completion_starts[slot] = - composition.sources[slot].completion_count; + batch.sources[slot].completion_count; metric_rendered_starts[slot] = - composition.sources[slot].rendered_frame_count; + batch.sources[slot].rendered_frame_count; } return; } @@ -212,8 +213,8 @@ void Gallery_Pixel_Stream::Private::update_metrics( sampling_clock_ticks.load(std::memory_order_relaxed); nlohmann::json source_metrics = nlohmann::json::object(); const auto description = sampler->describe(); - for (std::size_t slot = 0; slot < composition.sources.size(); ++slot) { - const auto& progress = composition.sources[slot]; + for (std::size_t slot = 0; slot < batch.sources.size(); ++slot) { + const auto& progress = batch.sources[slot]; const auto completed = progress.completion_count - metric_completion_starts[slot]; const auto rendered = progress.rendered_frame_count - @@ -247,14 +248,14 @@ void Gallery_Pixel_Stream::Private::update_metrics( metric_completion_starts[slot] = progress.completion_count; metric_rendered_starts[slot] = progress.rendered_frame_count; } - const auto& compose = compose_ms.state(); + const auto& batch_sample = batch_sample_ms.state(); const auto& sample_delay = sample_delay_ms.state(); const auto& pixel_pack = pixel_pack_ms.state(); const auto& publish_time = publish_ms.state(); auto output = nlohmann::json{ {"kind", "gallery_metrics"}, {"protocol", "aethera.gallery.pixels"}, - {"version", 4}, + {"version", 6}, {"clock_sequence", tick.sequence}, {"sampled_frame_count", sampler_state.sampled_frames}, {"pixel_frame_count", pixel_frame_count}, @@ -272,8 +273,8 @@ void Gallery_Pixel_Stream::Private::update_metrics( "pixel_frame_rate_fps", static_cast(pixel_frame_count - metric_pixel_start) / seconds }, - {"compose_average_ms", compose.average}, - {"compose_p95_ms", compose.p95}, + {"batch_sample_average_ms", batch_sample.average}, + {"batch_sample_p95_ms", batch_sample.p95}, {"sample_delay_average_ms", sample_delay.average}, {"sample_delay_p95_ms", sample_delay.p95}, {"pixel_pack_average_ms", pixel_pack.average}, @@ -282,16 +283,16 @@ void Gallery_Pixel_Stream::Private::update_metrics( {"publish_p95_ms", publish_time.p95}, { "statistics", { - {"compose_ms", statistic_json(compose)}, + {"batch_sample_ms", statistic_json(batch_sample)}, {"sample_delay_ms", statistic_json(sample_delay)}, {"pixel_pack_ms", statistic_json(pixel_pack)}, {"publish_ms", statistic_json(publish_time)} } }, {"pixel_bytes", pixel_bytes}, - {"fresh_tiles", composition.fresh_tile_count}, - {"missing_tiles", composition.missing_tile_count}, - {"rejected_frames", composition.rejected_frame_count}, + {"transmitted_tiles", transmitted_tiles}, + {"missing_tiles", batch.missing_tile_count}, + {"rejected_frames", batch.rejected_frame_count}, { "skipped_sample_ticks", skipped_sample_ticks.load(std::memory_order_relaxed) @@ -313,6 +314,9 @@ void Gallery_Pixel_Stream::Private::update_metrics( void Gallery_Pixel_Stream::Private::sample_frame() { active_sample.reset(); active_pixels.reset(); + active_consumers.clear(); + active_pixel_bytes = 0; + active_transmitted_tiles = 0; if (stopping.load(std::memory_order_acquire) || failed.load(std::memory_order_acquire) || !consumer_accepts()) { @@ -325,16 +329,26 @@ void Gallery_Pixel_Stream::Private::sample_frame() { skipped_sample_ticks.fetch_add(1, std::memory_order_relaxed); return; } - static_cast(compose_ms.submit( + static_cast(batch_sample_ms.submit( std::chrono::duration( std::chrono::steady_clock::now() - started).count())); static_cast(sample_delay_ms.submit( active_sample->deadline_delay_ms)); } void Gallery_Pixel_Stream::Private::pack_pixel_frame() { - if (!active_sample || !consumer_accepts()) + if (!active_sample || active_sample->batch.tiles.empty() || + !consumer_accepts()) return; const auto started = std::chrono::steady_clock::now(); + const auto current = consumers.load(std::memory_order_acquire); + for (const auto& consumer : *current) { + if (!consumer.handler || !consumer.readiness) continue; + bool accepts{}; + try { accepts = (*consumer.readiness)(); } + catch (...) { continue; } + if (accepts) active_consumers.push_back(consumer); + } + if (active_consumers.empty()) return; active_pixels = std::make_shared( frame_sampling::websocket_pixel::pack_websocket_pixel_frame( @@ -344,20 +358,33 @@ void Gallery_Pixel_Stream::Private::pack_pixel_frame() { std::chrono::steady_clock::now() - started).count())); } void Gallery_Pixel_Stream::Private::publish_pixel_frame() { - if (!active_pixels) return; - ++pixel_frame_count; + if (!active_pixels || active_consumers.empty()) return; const auto started = std::chrono::steady_clock::now(); - publish(active_pixels, {}); + for (const auto& consumer : active_consumers) { + bool accepted{}; + try { + accepted = consumer.handler && + (*consumer.handler)(Gallery_Pixel_Message{active_pixels, {}}); + } + catch (...) {} + if (!accepted) continue; + ++pixel_frame_count; + active_pixel_bytes += active_pixels->bytes.size(); + active_transmitted_tiles += active_sample->batch.tiles.size(); + } static_cast(publish_ms.submit( std::chrono::duration( std::chrono::steady_clock::now() - started).count())); } void Gallery_Pixel_Stream::Private::complete_frame() { if (!active_sample) return; - const auto pixel_bytes = active_pixels ? active_pixels->bytes.size() : 0U; - update_metrics(active_sample->tick, *active_sample, pixel_bytes); + update_metrics(active_sample->tick, *active_sample, active_pixel_bytes, + active_transmitted_tiles); active_sample.reset(); active_pixels.reset(); + active_consumers.clear(); + active_pixel_bytes = 0; + active_transmitted_tiles = 0; } std::shared_ptr Gallery_Pixel_Stream::create( std::vector plots) { @@ -381,7 +408,7 @@ void Gallery_Pixel_Stream::bind_plots() { throw std::invalid_argument("gallery pixel stream requires a Plot"); /* - * 图集像素流使用独立固定采样时钟,不依赖任意 Plot 的完成回调推进。 + * tile 像素批使用独立固定采样时钟,不依赖任意 Plot 的完成回调推进。 * 每个来源只保留 latest;采样图仍在执行或没有新完成帧时直接合并 tick, * 不排队,也不让 Taskflow worker 等待。 */ @@ -441,7 +468,6 @@ void Gallery_Pixel_Stream::bind_plots() { return; owner_data.sampling_clock_ticks.fetch_add( 1, std::memory_order_relaxed); - if (!owner_data.sampler->has_pending_frame()) return; bool idle{}; if (!owner_data.pixel_busy.compare_exchange_strong( idle, true, std::memory_order_acq_rel, @@ -585,24 +611,24 @@ std::string Gallery_Pixel_Stream::layout_description() const { const auto& data = static_cast(*d); const auto description = data.sampler->describe(); nlohmann::json plots = nlohmann::json::object(); - for (const auto& source : description.sources) plots[source.id] = {{"column", source.column}, {"row", source.row}}; + for (const auto& source : description.sources) + plots[source.id] = {{"slot", source.slot}}; return nlohmann::json{ {"kind", "gallery_layout"}, {"protocol", "aethera.gallery.pixels"}, - {"version", 4}, + {"version", 6}, {"transport", "drogon_pixels"}, {"frame_rate_fps", gallery_frame_rate}, - {"columns", description.columns}, - {"rows", description.rows}, {"tile_width", description.tile_width}, {"tile_height", description.tile_height}, - {"width", description.width}, - {"height", description.height}, {"pixels", { {"transport", "Drogon WebSocket binary"}, - {"header_bytes", 56}, - {"payload", "native"}, - {"format_source", "binary_frame_header"} + {"header_bytes", 40}, + {"tile_header_bytes", 32}, + {"protocol_version", 4}, + {"packed_time", "unix_nanoseconds"}, + {"payload", "current_native_tiles"}, + {"format_source", "tile_header"} }}, {"plots", std::move(plots)} }.dump(); @@ -611,7 +637,7 @@ nlohmann::json Gallery_Pixel_Stream::diagnostics() const { nlohmann::json output{ {"kind", "gallery_metrics"}, {"protocol", "aethera.gallery.pixels"}, - {"version", 4}, + {"version", 6}, {"sources", nlohmann::json::object()} }; this->template access_state([&output](const State& state) { diff --git a/web_server/src/Gallery_Pixel_Stream.hpp b/web_server/src/Gallery_Pixel_Stream.hpp index 726d89e..dfd3e95 100644 --- a/web_server/src/Gallery_Pixel_Stream.hpp +++ b/web_server/src/Gallery_Pixel_Stream.hpp @@ -13,14 +13,14 @@ namespace aethera::web { struct Gallery_Pixel_Message { std::shared_ptr - pixels{}; /* 不可变的原始图集协议帧。 */ + pixels{}; /* 不可变的当前 tile 原始像素批。 */ std::string notification{}; /* 终止错误等低频控制通知。 */ }; struct Gallery_Pixel_Stream : Def, std::enable_shared_from_this { struct Plot_Entry { - std::string id; /* 图集布局中的业务 Plot 标识。 */ + std::string id; /* 像素批槽位对应的业务 Plot 标识。 */ std::shared_ptr plot; /* 被采样的 Plot,共享所有权。 */ }; struct Prop : Prev_Prop {}; @@ -31,7 +31,7 @@ struct Gallery_Pixel_Stream : Def, struct Private; using Stream_Id = std::uint64_t; - using Stream_Handler = std::function; + using Stream_Handler = std::function; using Consumer_Readiness = std::function; using Consumer_Diagnostics = std::function; @@ -58,4 +58,3 @@ private: } #include "Gallery_Pixel_Stream.ipp" - diff --git a/web_server/src/Gallery_Pixel_Stream.ipp b/web_server/src/Gallery_Pixel_Stream.ipp index febf809..433afbb 100644 --- a/web_server/src/Gallery_Pixel_Stream.ipp +++ b/web_server/src/Gallery_Pixel_Stream.ipp @@ -26,7 +26,7 @@ struct Gallery_Pixel_Stream::Private : Prev_Private { using Consumers = std::vector; Object* object{}; /* 绑定后有效,可空、非拥有借用。 */ - Sliding_Statistics compose_ms{600}; + Sliding_Statistics batch_sample_ms{600}; Sliding_Statistics sample_delay_ms{600}; Sliding_Statistics pixel_pack_ms{600}; Sliding_Statistics publish_ms{600}; @@ -36,8 +36,11 @@ struct Gallery_Pixel_Stream::Private : Prev_Private { Frame_Scheduler::Timer sample_timer{}; std::atomic_bool pixel_busy{}; std::optional active_sample{}; - std::shared_ptr - active_pixels{}; + std::shared_ptr active_pixels{}; + Consumers active_consumers{}; + std::size_t active_pixel_bytes{}; + std::size_t active_transmitted_tiles{}; std::atomic_uint64_t next_consumer_id{1}; std::atomic> consumers{ std::make_shared()}; @@ -78,7 +81,8 @@ struct Gallery_Pixel_Stream::Private : Prev_Private { std::shared_ptr frame); void update_metrics(const Plot_Render_Tick& tick, const frame_sampling::Sampled_Gallery_Frame& sample, - std::size_t pixel_bytes); + std::size_t pixel_bytes, + std::size_t transmitted_tiles); void sample_frame(); void pack_pixel_frame(); void publish_pixel_frame(); @@ -90,4 +94,3 @@ struct Gallery_Pixel_Stream::Private : Prev_Private { }; } - diff --git a/web_server/src/Gallery_WebSocket.cpp b/web_server/src/Gallery_WebSocket.cpp index 8420882..02073eb 100644 --- a/web_server/src/Gallery_WebSocket.cpp +++ b/web_server/src/Gallery_WebSocket.cpp @@ -14,6 +14,7 @@ struct Gallery_WebSocket::Private final { Gallery_Pixel_Stream::Stream_Id subscription{}; std::string connection_id{}; std::atomic_bool attached{}; + std::atomic_bool presentation_ready{}; std::atomic_bool frame_in_flight{}; std::atomic_uint64_t in_flight_sequence{}; std::atomic_uint64_t sent_count{}; @@ -42,7 +43,8 @@ void Gallery_WebSocket::start() { d->connection_id, [weak](Gallery_Pixel_Message frame) { if (const auto socket = weak.lock()) - socket->deliver(std::move(frame)); + return socket->deliver(std::move(frame)); + return false; }, [weak] { const auto socket = weak.lock(); @@ -50,6 +52,8 @@ void Gallery_WebSocket::start() { return false; const auto connection = socket->d->connection.lock(); return connection && connection->connected() && + socket->d->presentation_ready.load( + std::memory_order_acquire) && !socket->d->frame_in_flight.load(std::memory_order_acquire); }, [weak] { @@ -74,39 +78,46 @@ void Gallery_WebSocket::start() { drogon::WebSocketMessageType::Text); } -void Gallery_WebSocket::deliver(Gallery_Pixel_Message frame) { - if (!d->attached.load(std::memory_order_acquire)) return; - if (frame.pixels) queue_pixel_frame(std::move(frame.pixels)); +bool Gallery_WebSocket::deliver(Gallery_Pixel_Message frame) { + if (!d->attached.load(std::memory_order_acquire)) return false; + bool accepted = !frame.pixels; + if (frame.pixels) + accepted = queue_pixel_frame(std::move(frame.pixels)); if (!frame.notification.empty()) if (const auto connection = d->connection.lock(); connection && connection->connected()) connection->send(std::move(frame.notification), drogon::WebSocketMessageType::Text); + return accepted; } -void Gallery_WebSocket::queue_pixel_frame( +bool Gallery_WebSocket::queue_pixel_frame( std::shared_ptr frame) { - if (!frame || !d->attached.load(std::memory_order_acquire)) return; + if (!frame || !d->attached.load(std::memory_order_acquire) || + !d->presentation_ready.load(std::memory_order_acquire)) + return false; bool idle{}; if (!d->frame_in_flight.compare_exchange_strong( idle, true, std::memory_order_acq_rel, std::memory_order_acquire)) - return; + return false; const auto connection = d->connection.lock(); if (!connection || !connection->connected() || !d->attached.load(std::memory_order_acquire)) { d->frame_in_flight.store(false, std::memory_order_release); - return; + return false; } d->in_flight_sequence.store(frame->sequence, std::memory_order_release); try { connection->send(frame->bytes, drogon::WebSocketMessageType::Binary); d->sent_count.fetch_add(1, std::memory_order_relaxed); + return true; } catch (...) { d->frame_in_flight.store(false, std::memory_order_release); close(); + return false; } } @@ -124,10 +135,20 @@ void Gallery_WebSocket::acknowledge_pixel_frame(std::uint64_t sequence) { void Gallery_WebSocket::receive(std::string_view message) { const auto json = nlohmann::json::parse(message, nullptr, false); - if (!json.is_object() || - json.value("kind", std::string{}) != "pixel_frame_consumed") + if (!json.is_object()) return; + const auto kind = json.value("kind", std::string{}); + if (kind == "pixel_stream_ready") { + d->presentation_ready.store(true, std::memory_order_release); return; - acknowledge_pixel_frame(json.value("sequence", 0ULL)); + } + if (kind == "pixel_stream_not_ready") { + d->presentation_ready.store(false, std::memory_order_release); + d->frame_in_flight.store(false, std::memory_order_release); + d->in_flight_sequence.store(0, std::memory_order_release); + return; + } + if (kind == "pixel_frame_consumed") + acknowledge_pixel_frame(json.value("sequence", 0ULL)); } void Gallery_WebSocket::close() noexcept { @@ -138,6 +159,7 @@ void Gallery_WebSocket::close() noexcept { catch (...) {} d->subscription = 0; d->frame_in_flight.store(false, std::memory_order_release); + d->presentation_ready.store(false, std::memory_order_release); } Gallery_WebSocket_Controller::Gallery_WebSocket_Controller( diff --git a/web_server/src/Gallery_WebSocket.hpp b/web_server/src/Gallery_WebSocket.hpp index 37b8ff5..0f2ac9b 100644 --- a/web_server/src/Gallery_WebSocket.hpp +++ b/web_server/src/Gallery_WebSocket.hpp @@ -22,9 +22,10 @@ struct Gallery_WebSocket final void close() noexcept; private: - void deliver(Gallery_Pixel_Message frame); - void queue_pixel_frame(std::shared_ptr frame); + [[nodiscard]] bool deliver(Gallery_Pixel_Message frame); + [[nodiscard]] bool queue_pixel_frame( + std::shared_ptr frame); void acknowledge_pixel_frame(std::uint64_t sequence); struct Private; std::unique_ptr d; diff --git a/web_server/src/Web_Server.cpp b/web_server/src/Web_Server.cpp index 6345c4c..22b61b2 100644 --- a/web_server/src/Web_Server.cpp +++ b/web_server/src/Web_Server.cpp @@ -416,7 +416,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) .setIdleConnectionTimeout(90) .run(); /* 先解除 Plot 完成帧订阅并停止页面时钟;Taskflow 任务只捕获弱所有权, - * 因而销毁期不会访问已经释放的图集,也不需要额外业务线程池排空。 */ + * 因而销毁期不会访问已经释放的像素批状态,也不需要额外业务线程池排空。 */ for (const auto& [id, stream] : *gallery_streams) { static_cast(id); stream->shutdown(); diff --git a/web_server/src/detail/Gallery_Frame_Atlas.cpp b/web_server/src/detail/Gallery_Frame_Atlas.cpp deleted file mode 100644 index 6165d3d..0000000 --- a/web_server/src/detail/Gallery_Frame_Atlas.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include "Gallery_Frame_Atlas.hpp" -#include -#include -#include -#include -#include -#include - -namespace aethera::web::detail { -struct Gallery_Frame_Atlas::Private { - struct Source { - struct Published { - std::shared_ptr latest_completion; - std::shared_ptr latest_pixels; - std::uint64_t completion_count{}; - std::uint64_t rendered_frame_count{}; - }; - /* Plot 完成线程发布不可变版本,图集线程只读取同一个版本;不再把四个 - * 相关字段分别复制出锁区。 */ - std::atomic> published{ - std::make_shared()}; - std::atomic_uint64_t composited_rendered_sequence{}; - }; - - Gallery_Atlas_Description description{}; /* 尺寸与槽位布局的唯一权威描述。 */ - std::vector> sources{}; /* 各 Plot 独立锁定的最近完成快照。 */ - std::vector pixels{}; /* 编码器直接读取的持久原生布局图集。 */ - std::atomic_int layout{-1}; /* 第一张有效画面原子确定,后续来源必须一致。 */ - std::atomic_uint64_t rejected_frame_count{}; /* 两次 compose 之间拒绝的帧数。 */ - - Private(std::uint32_t tile_width, std::uint32_t tile_height, - std::uint32_t columns, std::vector source_ids) - : description{tile_width, tile_height, columns} { - if (tile_width == 0 || tile_height == 0 || columns == 0 || - source_ids.empty()) - throw std::invalid_argument( - "gallery atlas dimensions and source list must be non-empty"); - if ((tile_width & 1U) != 0 || (tile_height & 1U) != 0) - throw std::invalid_argument( - "gallery atlas tile dimensions must be even for H.264"); - description.rows = static_cast( - (source_ids.size() + columns - 1U) / columns); - if (tile_width > std::numeric_limits::max() / columns || - tile_height > std::numeric_limits::max() / - description.rows) - throw std::overflow_error("gallery atlas dimensions overflow"); - description.width = tile_width * columns; - description.height = tile_height * description.rows; - description.sources.reserve(source_ids.size()); - sources.reserve(source_ids.size()); - for (std::size_t slot = 0; slot < source_ids.size(); ++slot) { - Gallery_Atlas_Source_Description source{ - std::move(source_ids[slot]), slot, - static_cast(slot % columns), - static_cast(slot / columns)}; - description.sources.push_back(std::move(source)); - sources.push_back(std::make_unique()); - } - const auto pixel_count = static_cast(description.width) * - description.height; - if (pixel_count > std::numeric_limits::max() / 4U) - throw std::overflow_error("gallery atlas byte size overflow"); - pixels.resize(pixel_count * 4U); - for (std::size_t offset = 3; offset < pixels.size(); offset += 4U) - pixels[offset] = std::byte{255}; - } -}; - -Gallery_Frame_Atlas::Gallery_Frame_Atlas( - std::uint32_t tile_width, std::uint32_t tile_height, - std::uint32_t columns, std::vector source_ids) - : d(std::make_unique(tile_width, tile_height, columns, - std::move(source_ids))) {} - -Gallery_Frame_Atlas::~Gallery_Frame_Atlas() = default; - -Gallery_Frame_Atlas::Accept_Frame_Result Gallery_Frame_Atlas::accept_frame( - std::size_t slot, std::shared_ptr frame) { - if (slot >= d->sources.size()) - throw std::out_of_range("gallery atlas source slot is invalid"); - const auto expected = static_cast(d->description.tile_width) * - d->description.tile_height * 4U; - if (!frame || frame->width != d->description.tile_width || - frame->height != d->description.tile_height || - (frame->pixels && frame->pixels->size() != expected)) { - d->rejected_frame_count.fetch_add(1, std::memory_order_relaxed); - return Accept_Frame_Result::invalid_frame; - } - auto& source = *d->sources[slot]; - if (frame->pixels) { - const auto candidate = static_cast(frame->layout); - int unknown{-1}; - static_cast(d->layout.compare_exchange_strong( - unknown, candidate, std::memory_order_acq_rel, - std::memory_order_acquire)); - if (d->layout.load(std::memory_order_acquire) != candidate) { - d->rejected_frame_count.fetch_add(1, std::memory_order_relaxed); - return Accept_Frame_Result::invalid_frame; - } - } - auto current = source.published.load(std::memory_order_acquire); - for (;;) { - if (current->latest_completion && - frame->sequence <= current->latest_completion->sequence) { - d->rejected_frame_count.fetch_add(1, std::memory_order_relaxed); - return Accept_Frame_Result::stale_frame; - } - auto next = std::make_shared(*current); - if (frame->rendered_sequence != 0 && - (!current->latest_completion || - frame->rendered_sequence != - current->latest_completion->rendered_sequence)) - ++next->rendered_frame_count; - next->latest_completion = frame; - if (frame->pixels && frame->rendered_sequence != 0) - next->latest_pixels = frame; - ++next->completion_count; - std::shared_ptr desired = next; - if (source.published.compare_exchange_weak( - current, std::move(desired), std::memory_order_release, - std::memory_order_acquire)) - return Accept_Frame_Result::accepted; - } -} - -Gallery_Atlas_Composition Gallery_Frame_Atlas::compose() { - Gallery_Atlas_Composition result; - result.width = d->description.width; - result.height = d->description.height; - const auto layout = d->layout.load(std::memory_order_acquire); - result.layout = layout < 0 ? Plot_Pixel_Layout::rgba8 - : static_cast(layout); - result.rejected_frame_count = - d->rejected_frame_count.exchange(0, std::memory_order_acq_rel); - result.sources.reserve(d->sources.size()); - const auto source_row_bytes = - static_cast(d->description.tile_width) * 4U; - for (std::size_t slot = 0; slot < d->sources.size(); ++slot) { - auto& source = *d->sources[slot]; - const auto published = source.published.load(std::memory_order_acquire); - const auto& latest_completion = published->latest_completion; - const auto& latest_pixels = published->latest_pixels; - if (!latest_completion) { - ++result.missing_tile_count; - result.sources.push_back({}); - continue; - } - if (!latest_pixels) { - ++result.missing_tile_count; - } - else if (latest_pixels->rendered_sequence != - source.composited_rendered_sequence.load( - std::memory_order_acquire)) { - const auto& layout = d->description.sources[slot]; - for (std::uint32_t y = 0; y < d->description.tile_height; ++y) { - const auto source_offset = - static_cast(y) * source_row_bytes; - const auto destination_offset = - (static_cast( - layout.row * d->description.tile_height + y) * - d->description.width + - layout.column * d->description.tile_width) * 4U; - std::memcpy(d->pixels.data() + destination_offset, - latest_pixels->pixels->data() + source_offset, - source_row_bytes); - } - source.composited_rendered_sequence.store( - latest_pixels->rendered_sequence, std::memory_order_release); - ++result.fresh_tile_count; - } - result.sources.push_back({ - latest_completion->sequence, - latest_completion->correlation_id, - latest_completion->presentation_time, - latest_completion->rendered_sequence, - latest_completion->rendered_correlation_id, - published->completion_count, - published->rendered_frame_count}); - } - result.pixels = d->pixels; - return result; -} - -Gallery_Atlas_Description Gallery_Frame_Atlas::describe() const { - return d->description; -} -} diff --git a/web_server/src/detail/Gallery_Frame_Atlas.hpp b/web_server/src/detail/Gallery_Frame_Atlas.hpp deleted file mode 100644 index 0677341..0000000 --- a/web_server/src/detail/Gallery_Frame_Atlas.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -namespace aethera::web::detail { -struct Gallery_Atlas_Source_Description { - std::string id; /* 图集槽位对应的 Plot 业务标识。 */ - std::size_t slot{}; /* 从左到右、从上到下的稳定槽位序号。 */ - std::uint32_t column{}; /* 浏览器裁剪使用的零起始列号。 */ - std::uint32_t row{}; /* 浏览器裁剪使用的零起始行号。 */ -}; -struct Gallery_Atlas_Description { - std::uint32_t tile_width{}; /* 单个 Plot 完成帧的固定像素宽度。 */ - std::uint32_t tile_height{}; /* 单个 Plot 完成帧的固定像素高度。 */ - std::uint32_t columns{}; /* 图集固定列数。 */ - std::uint32_t rows{}; /* 容纳全部 Plot 后计算得到的行数。 */ - std::uint32_t width{}; /* H.264 输入图集的像素宽度。 */ - std::uint32_t height{}; /* H.264 输入图集的像素高度。 */ - std::vector sources; /* 每个 Plot 在图集中的唯一布局描述。 */ -}; -struct Gallery_Atlas_Source_Progress { - std::uint64_t completion_sequence{}; /* 当前槽位最近一次逻辑完成回调的局部序号。 */ - std::uint64_t completion_correlation_id{}; /* 最近逻辑回调对应的页面时钟序号。 */ - std::chrono::microseconds completion_presentation_time{}; /* 最近逻辑回调的页面媒体时间。 */ - std::uint64_t rendered_sequence{}; /* 当前像素对应的真实 Scene/GPU 画面序号。 */ - std::uint64_t rendered_correlation_id{}; /* 当前真实画面对应的页面时钟序号。 */ - std::uint64_t completion_count{}; /* 本图集生命周期内接受的逻辑完成回调数。 */ - std::uint64_t rendered_frame_count{}; /* 本图集生命周期内接受的不同真实画面数。 */ -}; -struct Gallery_Atlas_Composition { - std::span pixels{}; /* 在下一次 compose 前有效的连续原生布局图集。 */ - Plot_Pixel_Layout layout{Plot_Pixel_Layout::rgba8}; /* 本图集全部来源共同的像素布局。 */ - std::uint32_t width{}; /* 本次图集像素宽度。 */ - std::uint32_t height{}; /* 本次图集像素高度。 */ - std::uint32_t fresh_tile_count{}; /* 本次实际复制了新完成帧的槽位数。 */ - std::uint32_t missing_tile_count{}; /* 尚未收到首帧的槽位数。 */ - std::uint64_t rejected_frame_count{}; /* 上次 compose 后拒绝的无效或过期帧数。 */ - std::vector sources; /* 合成时刻各槽位的完成进度快照。 */ -}; -struct Gallery_Frame_Atlas final { -public: - enum struct Accept_Frame_Result : std::uint8_t { - accepted, - invalid_frame, - stale_frame - }; - Gallery_Frame_Atlas(std::uint32_t tile_width, - std::uint32_t tile_height, - std::uint32_t columns, - std::vector source_ids); - ~Gallery_Frame_Atlas(); - Gallery_Frame_Atlas(const Gallery_Frame_Atlas&) = delete; - Gallery_Frame_Atlas& operator=(const Gallery_Frame_Atlas&) = delete; - [[nodiscard]] Accept_Frame_Result accept_frame( - std::size_t slot, std::shared_ptr frame); - [[nodiscard]] Gallery_Atlas_Composition compose(); - [[nodiscard]] Gallery_Atlas_Description describe() const; -private: - struct Private; - std::unique_ptr d; -}; -} diff --git a/web_server/src/detail/Gallery_Frame_Batch.cpp b/web_server/src/detail/Gallery_Frame_Batch.cpp new file mode 100644 index 0000000..a6cadec --- /dev/null +++ b/web_server/src/detail/Gallery_Frame_Batch.cpp @@ -0,0 +1,128 @@ +#include "Gallery_Frame_Batch.hpp" +#include +#include +#include +#include + +namespace aethera::web::detail { + +struct Gallery_Frame_Batch::Private { + struct Published { + std::shared_ptr latest_completion; /* 最新逻辑完成帧共享所有权。 */ + std::shared_ptr latest_pixels; /* 最新真实像素帧共享所有权。 */ + std::uint64_t completion_count{}; /* 接受的逻辑完成数量。 */ + std::uint64_t rendered_frame_count{}; /* 接受的不同真实画面数量。 */ + }; + struct Source { + std::atomic> published{ + std::make_shared()}; /* Plot 发布的唯一不可变版本。 */ + }; + + Gallery_Batch_Description description{}; /* 尺寸及槽位映射的唯一权威来源。 */ + std::vector> sources{}; /* 各 Plot 独立发布 latest。 */ + std::atomic_uint64_t rejected_frame_count{}; /* 两次采样之间的拒绝计数。 */ + + Private(std::uint32_t tile_width, std::uint32_t tile_height, + std::vector source_ids) + : description{tile_width, tile_height} { + if (tile_width == 0 || tile_height == 0 || source_ids.empty()) + throw std::invalid_argument( + "gallery batch dimensions and source list must be non-empty"); + const auto pixel_count = static_cast(tile_width) * + tile_height; + if (pixel_count > std::numeric_limits::max() / 4U) + throw std::overflow_error("gallery tile dimensions overflow"); + description.sources.reserve(source_ids.size()); + sources.reserve(source_ids.size()); + for (std::size_t slot = 0; slot < source_ids.size(); ++slot) { + description.sources.push_back({std::move(source_ids[slot]), slot}); + sources.push_back(std::make_unique()); + } + } +}; + +Gallery_Frame_Batch::Gallery_Frame_Batch( + std::uint32_t tile_width, std::uint32_t tile_height, + std::vector source_ids) + : d(std::make_unique(tile_width, tile_height, + std::move(source_ids))) {} + +Gallery_Frame_Batch::~Gallery_Frame_Batch() = default; + +Gallery_Frame_Batch::Accept_Frame_Result Gallery_Frame_Batch::accept_frame( + std::size_t slot, std::shared_ptr frame) { + if (slot >= d->sources.size()) + throw std::out_of_range("gallery batch source slot is invalid"); + const auto expected = static_cast(d->description.tile_width) * + d->description.tile_height * 4U; + if (!frame || frame->width != d->description.tile_width || + frame->height != d->description.tile_height || + (frame->pixels && frame->pixels->size() != expected)) { + d->rejected_frame_count.fetch_add(1, std::memory_order_relaxed); + return Accept_Frame_Result::invalid_frame; + } + auto& source = *d->sources[slot]; + auto current = source.published.load(std::memory_order_acquire); + for (;;) { + if (current->latest_completion && + frame->sequence <= current->latest_completion->sequence) { + d->rejected_frame_count.fetch_add(1, std::memory_order_relaxed); + return Accept_Frame_Result::stale_frame; + } + auto next = std::make_shared(*current); + if (frame->rendered_sequence != 0 && + (!current->latest_completion || + frame->rendered_sequence != + current->latest_completion->rendered_sequence)) + ++next->rendered_frame_count; + next->latest_completion = frame; + if (frame->pixels && frame->rendered_sequence != 0) + next->latest_pixels = frame; + ++next->completion_count; + std::shared_ptr desired = next; + if (source.published.compare_exchange_weak( + current, std::move(desired), std::memory_order_release, + std::memory_order_acquire)) + return Accept_Frame_Result::accepted; + } +} + +Gallery_Frame_Sample Gallery_Frame_Batch::sample() { + Gallery_Frame_Sample result; + result.rejected_frame_count = + d->rejected_frame_count.exchange(0, std::memory_order_acq_rel); + result.tiles.reserve(d->sources.size()); + result.sources.reserve(d->sources.size()); + for (std::size_t slot = 0; slot < d->sources.size(); ++slot) { + auto& source = *d->sources[slot]; + const auto published = source.published.load(std::memory_order_acquire); + const auto& completion = published->latest_completion; + const auto& pixels = published->latest_pixels; + if (!completion) { + ++result.missing_tile_count; + result.sources.push_back({}); + continue; + } + if (!pixels) { + ++result.missing_tile_count; + } + else { + result.tiles.push_back({slot, pixels}); + } + result.sources.push_back({ + completion->sequence, + completion->correlation_id, + completion->presentation_time, + completion->rendered_sequence, + completion->rendered_correlation_id, + published->completion_count, + published->rendered_frame_count}); + } + return result; +} + +Gallery_Batch_Description Gallery_Frame_Batch::describe() const { + return d->description; +} + +} diff --git a/web_server/src/detail/Gallery_Frame_Batch.hpp b/web_server/src/detail/Gallery_Frame_Batch.hpp new file mode 100644 index 0000000..20d7d6e --- /dev/null +++ b/web_server/src/detail/Gallery_Frame_Batch.hpp @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace aethera::web::detail { + +struct Gallery_Source_Description { + std::string id; /* Plot 业务标识。 */ + std::size_t slot{}; /* 像素批协议中的稳定槽位。 */ +}; + +struct Gallery_Batch_Description { + std::uint32_t tile_width{}; /* 每个 Plot 原始像素帧宽度。 */ + std::uint32_t tile_height{}; /* 每个 Plot 原始像素帧高度。 */ + std::vector sources; /* 槽位到 Plot 的唯一映射。 */ +}; + +struct Gallery_Source_Progress { + std::uint64_t completion_sequence{}; /* 最新逻辑完成序号。 */ + std::uint64_t completion_correlation_id{}; /* 最新逻辑完成对应的页面时钟序号。 */ + std::chrono::microseconds completion_presentation_time{}; /* 最新逻辑完成的页面媒体时间。 */ + std::uint64_t rendered_sequence{}; /* 最新真实像素对应的渲染序号。 */ + std::uint64_t rendered_correlation_id{}; /* 最新真实像素对应的页面时钟序号。 */ + std::uint64_t completion_count{}; /* 生命周期内逻辑完成数量。 */ + std::uint64_t rendered_frame_count{}; /* 生命周期内不同真实画面数量。 */ +}; + +struct Gallery_Sampled_Tile { + std::size_t slot{}; /* 本 tile 对应的稳定源槽位。 */ + std::shared_ptr frame; /* 不可变像素帧共享所有权,保持到封包结束。 */ +}; + +struct Gallery_Frame_Sample { + std::vector tiles; /* 采样时刻各源的最新真实 tile。 */ + std::uint32_t missing_tile_count{}; /* 尚未收到首张真实画面的源数量。 */ + std::uint64_t rejected_frame_count{}; /* 上次采样后拒绝的无效或过期帧数。 */ + std::vector sources; /* 采样时刻各源的权威完成进度。 */ +}; + +struct Gallery_Frame_Batch final { +public: + enum struct Accept_Frame_Result : std::uint8_t { + accepted, + invalid_frame, + stale_frame + }; + + Gallery_Frame_Batch(std::uint32_t tile_width, + std::uint32_t tile_height, + std::vector source_ids); + ~Gallery_Frame_Batch(); + Gallery_Frame_Batch(const Gallery_Frame_Batch&) = delete; + Gallery_Frame_Batch& operator=(const Gallery_Frame_Batch&) = delete; + + [[nodiscard]] Accept_Frame_Result accept_frame( + std::size_t slot, std::shared_ptr frame); + [[nodiscard]] Gallery_Frame_Sample sample(); + [[nodiscard]] Gallery_Batch_Description describe() const; + +private: + struct Private; + std::unique_ptr d; +}; + +} diff --git a/web_server/src/frame_sampling/Frame_Sampler.cpp b/web_server/src/frame_sampling/Frame_Sampler.cpp index 243e8f1..e1d80a4 100644 --- a/web_server/src/frame_sampling/Frame_Sampler.cpp +++ b/web_server/src/frame_sampling/Frame_Sampler.cpp @@ -16,13 +16,12 @@ std::int64_t clock_nanoseconds(Clock::time_point value) noexcept { } struct Frame_Sampler::Private { - detail::Gallery_Frame_Atlas atlas; /* 各 Plot 最近原生帧与持久图集的唯一状态源。 */ + detail::Gallery_Frame_Batch batch; /* 各 Plot 最新不可变帧与采样进度的唯一状态源。 */ std::chrono::nanoseconds period{}; /* 固定采样周期;构造后不变。 */ double frame_rate_fps{}; /* 对外诊断使用的精确目标频率。 */ std::size_t sampling_source_slot{}; /* 为样本提供页面媒体时间的固定业务来源。 */ std::atomic_int64_t next_deadline_ns{}; /* 下一次允许生成样本的单调 deadline。 */ std::atomic_uint64_t accepted_source_frames{}; /* 成功发布到 latest 的源帧计数。 */ - std::atomic_uint64_t sampled_source_generation{}; /* 最近样本覆盖到的 accepted_source_frames。 */ std::atomic_uint64_t rejected_source_frames{}; /* 无效或过期源帧计数。 */ std::atomic_uint64_t sampled_frames{}; /* 实际采样计数。 */ std::atomic_uint64_t early_ticks{}; /* deadline 前被合并的触发计数。 */ @@ -31,10 +30,9 @@ struct Frame_Sampler::Private { Private(double target_frame_rate_fps, std::uint32_t tile_width, std::uint32_t tile_height, - std::uint32_t columns, std::vector source_ids, std::size_t source_slot) - : atlas(tile_width, tile_height, columns, std::move(source_ids)), + : batch(tile_width, tile_height, std::move(source_ids)), period(static_cast(std::llround( 1'000'000'000.0 / target_frame_rate_fps))), frame_rate_fps(target_frame_rate_fps), @@ -43,7 +41,7 @@ struct Frame_Sampler::Private { Frame_Sampler::Frame_Sampler( double frame_rate_fps, std::uint32_t tile_width, - std::uint32_t tile_height, std::uint32_t columns, + std::uint32_t tile_height, std::vector source_ids, std::size_t sampling_source_slot) { if (!std::isfinite(frame_rate_fps) || frame_rate_fps <= 0.0) @@ -52,7 +50,7 @@ Frame_Sampler::Frame_Sampler( if (sampling_source_slot >= source_ids.size()) throw std::out_of_range("frame sampler source slot is invalid"); d = std::make_unique( - frame_rate_fps, tile_width, tile_height, columns, + frame_rate_fps, tile_width, tile_height, std::move(source_ids), sampling_source_slot); if (d->period <= std::chrono::nanoseconds::zero()) throw std::invalid_argument("frame sampler period is too small"); @@ -62,47 +60,35 @@ Frame_Sampler::~Frame_Sampler() = default; Frame_Sampler::Accept_Frame_Result Frame_Sampler::accept_frame( std::size_t slot, std::shared_ptr frame) { - const auto result = d->atlas.accept_frame(slot, std::move(frame)); - if (result == detail::Gallery_Frame_Atlas::Accept_Frame_Result::accepted) { + const auto result = d->batch.accept_frame(slot, std::move(frame)); + if (result == detail::Gallery_Frame_Batch::Accept_Frame_Result::accepted) { d->accepted_source_frames.fetch_add(1, std::memory_order_release); return Accept_Frame_Result::accepted; } d->rejected_source_frames.fetch_add(1, std::memory_order_relaxed); - return result == detail::Gallery_Frame_Atlas::Accept_Frame_Result::stale_frame + return result == detail::Gallery_Frame_Batch::Accept_Frame_Result::stale_frame ? Accept_Frame_Result::stale_frame : Accept_Frame_Result::invalid_frame; } -bool Frame_Sampler::has_pending_frame() const noexcept { - return d->accepted_source_frames.load(std::memory_order_acquire) != - d->sampled_source_generation.load(std::memory_order_acquire); -} - std::optional Frame_Sampler::sample() { const auto now = Clock::now(); const auto now_ns = clock_nanoseconds(now); const auto compose_sample = [this, now](double deadline_delay_ms, - std::uint64_t generation, std::uint64_t sample_sequence) { - auto composition = d->atlas.compose(); - const auto& source = composition.sources[d->sampling_source_slot]; + auto batch = d->batch.sample(); + const auto& source = batch.sources[d->sampling_source_slot]; + const auto description = d->batch.describe(); Plot_Render_Tick tick{ now, source.completion_correlation_id, static_cast(source.completion_presentation_time.count()) / 1'000.0, - composition.width, - composition.height}; - d->sampled_source_generation.store(generation, - std::memory_order_release); + description.tile_width, + description.tile_height}; return Sampled_Gallery_Frame{ - std::move(composition), tick, sample_sequence, deadline_delay_ms}; + std::move(batch), tick, sample_sequence, deadline_delay_ms}; }; - const auto generation = d->accepted_source_frames.load( - std::memory_order_acquire); - if (generation == d->sampled_source_generation.load( - std::memory_order_acquire)) - return std::nullopt; auto deadline_ns = d->next_deadline_ns.load(std::memory_order_acquire); for (;;) { if (deadline_ns == 0) { @@ -113,7 +99,7 @@ std::optional Frame_Sampler::sample() { continue; const auto sample_sequence = d->sampled_frames.fetch_add( 1, std::memory_order_relaxed) + 1; - return compose_sample(0.0, generation, sample_sequence); + return compose_sample(0.0, sample_sequence); } if (now_ns < deadline_ns) { d->early_ticks.fetch_add(1, std::memory_order_relaxed); @@ -132,12 +118,12 @@ std::optional Frame_Sampler::sample() { 1, std::memory_order_relaxed) + 1; d->missed_periods.fetch_add(missed, std::memory_order_relaxed); return compose_sample(static_cast(late_ns) / 1'000'000.0, - generation, sample_sequence); + sample_sequence); } } -detail::Gallery_Atlas_Description Frame_Sampler::describe() const { - return d->atlas.describe(); +detail::Gallery_Batch_Description Frame_Sampler::describe() const { + return d->batch.describe(); } Frame_Sampler_State Frame_Sampler::state() const noexcept { diff --git a/web_server/src/frame_sampling/Frame_Sampler.hpp b/web_server/src/frame_sampling/Frame_Sampler.hpp index 25d8140..f0c25fc 100644 --- a/web_server/src/frame_sampling/Frame_Sampler.hpp +++ b/web_server/src/frame_sampling/Frame_Sampler.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include "../detail/Gallery_Frame_Atlas.hpp" +#include "../detail/Gallery_Frame_Batch.hpp" #include #include #include @@ -12,7 +12,7 @@ namespace aethera::web::frame_sampling { struct Sampled_Gallery_Frame { - detail::Gallery_Atlas_Composition composition{}; /* 本次采样得到的原生像素图集;下一次成功采样前有效。 */ + detail::Gallery_Frame_Sample batch{}; /* 本次采样得到的全部当前 tile 及权威源进度。 */ Plot_Render_Tick tick{}; /* 触发采样的服务端帧时钟。 */ std::uint64_t sample_sequence{}; /* 该图库流自身连续递增的采样序号。 */ double deadline_delay_ms{}; /* 实际采样相对固定周期 deadline 的延迟。 */ @@ -22,7 +22,7 @@ struct Frame_Sampler_State { double target_frame_rate_fps{}; /* 采样器固定目标频率。 */ std::uint64_t accepted_source_frames{}; /* 已进入最近帧快照的 Plot 完成帧数。 */ std::uint64_t rejected_source_frames{}; /* 因尺寸、布局或顺序不合法而拒绝的完成帧数。 */ - std::uint64_t sampled_frames{}; /* 到达 deadline 并实际生成的图集样本数。 */ + std::uint64_t sampled_frames{}; /* 到达 deadline 并实际生成的当前 tile 批次数。 */ std::uint64_t early_ticks{}; /* deadline 前到达、未触发采样的外接图次数。 */ std::uint64_t missed_periods{}; /* 调度延迟跨过的完整采样周期数。 */ }; @@ -42,7 +42,6 @@ public: Frame_Sampler(double frame_rate_fps, std::uint32_t tile_width, std::uint32_t tile_height, - std::uint32_t columns, std::vector source_ids, std::size_t sampling_source_slot); ~Frame_Sampler(); @@ -51,9 +50,8 @@ public: [[nodiscard]] Accept_Frame_Result accept_frame( std::size_t slot, std::shared_ptr frame); - [[nodiscard]] bool has_pending_frame() const noexcept; [[nodiscard]] std::optional sample(); - [[nodiscard]] detail::Gallery_Atlas_Description describe() const; + [[nodiscard]] detail::Gallery_Batch_Description describe() const; [[nodiscard]] Frame_Sampler_State state() const noexcept; private: diff --git a/web_server/src/frame_sampling/websocket_pixel/WebSocket_Pixel_Frame.cpp b/web_server/src/frame_sampling/websocket_pixel/WebSocket_Pixel_Frame.cpp index b27668e..e81800c 100644 --- a/web_server/src/frame_sampling/websocket_pixel/WebSocket_Pixel_Frame.cpp +++ b/web_server/src/frame_sampling/websocket_pixel/WebSocket_Pixel_Frame.cpp @@ -6,7 +6,8 @@ namespace aethera::web::frame_sampling::websocket_pixel { namespace { -constexpr std::size_t header_size{56}; +constexpr std::size_t batch_header_size{40}; +constexpr std::size_t tile_header_size{32}; template void append_little_endian(std::string& output, Integer value) { @@ -21,37 +22,43 @@ void append_little_endian(std::string& output, Integer value) { WebSocket_Pixel_Frame pack_websocket_pixel_frame( const Sampled_Gallery_Frame& frame) { - const auto& composition = frame.composition; - const auto expected = static_cast(composition.width) * - composition.height * 4U; - if (composition.width == 0 || composition.height == 0 || - composition.pixels.size() != expected) + if (frame.batch.tiles.empty()) throw std::invalid_argument( - "WebSocket pixel frame requires a complete four-byte atlas"); - if (expected > std::numeric_limits::max()) - throw std::overflow_error("WebSocket pixel payload exceeds protocol size"); - - const auto format = composition.layout == Plot_Pixel_Layout::bgra8 - ? Wire_Pixel_Format::bgra8_premultiplied - : Wire_Pixel_Format::rgba8; + "WebSocket pixel batch requires at least one changed tile"); + if (frame.batch.tiles.size() > std::numeric_limits::max()) + throw std::overflow_error("WebSocket pixel batch has too many tiles"); + std::size_t payload_size{}; + for (const auto& tile : frame.batch.tiles) { + if (tile.slot > std::numeric_limits::max() || + !tile.frame || !tile.frame->pixels || tile.frame->width == 0 || + tile.frame->height == 0 || + tile.frame->width > std::numeric_limits::max() / 4U) + throw std::invalid_argument("WebSocket pixel tile is incomplete"); + const auto expected = static_cast(tile.frame->width) * + tile.frame->height * 4U; + if (tile.frame->pixels->size() != expected || + expected > std::numeric_limits::max()) + throw std::invalid_argument("WebSocket pixel tile size is invalid"); + const auto maximum_payload = static_cast( + std::numeric_limits::max()); + if (expected > maximum_payload - tile_header_size || + payload_size > maximum_payload - tile_header_size - expected) + throw std::overflow_error( + "WebSocket pixel batch exceeds protocol size"); + payload_size += tile_header_size + expected; + } WebSocket_Pixel_Frame result; result.sequence = frame.sample_sequence; - result.format = format; - result.bytes.reserve(header_size + expected); + result.bytes.reserve(batch_header_size + payload_size); result.bytes.append("AERP", 4); - append_little_endian(result.bytes, 2U); + append_little_endian(result.bytes, 4U); append_little_endian( - result.bytes, static_cast(header_size)); - append_little_endian( - result.bytes, static_cast(format)); - append_little_endian(result.bytes, 0U); + result.bytes, static_cast(batch_header_size)); + append_little_endian( + result.bytes, static_cast(frame.batch.tiles.size())); append_little_endian(result.bytes, 0U); - append_little_endian(result.bytes, composition.width); - append_little_endian(result.bytes, composition.height); append_little_endian( - result.bytes, composition.width * 4U); - append_little_endian( - result.bytes, static_cast(expected)); + result.bytes, static_cast(payload_size)); append_little_endian(result.bytes, frame.sample_sequence); append_little_endian(result.bytes, static_cast(std::max(0, @@ -60,13 +67,32 @@ WebSocket_Pixel_Frame pack_websocket_pixel_frame( frame.tick.time_milliseconds)).count()))); append_little_endian(result.bytes, static_cast(std::chrono::duration_cast< - std::chrono::nanoseconds>(std::chrono::steady_clock::now() + std::chrono::nanoseconds>(std::chrono::system_clock::now() .time_since_epoch()).count())); - append_little_endian(result.bytes, 0U); - if (result.bytes.size() != header_size) - throw std::logic_error("WebSocket pixel protocol header size mismatch"); - result.bytes.append( - reinterpret_cast(composition.pixels.data()), expected); + if (result.bytes.size() != batch_header_size) + throw std::logic_error("WebSocket pixel batch header size mismatch"); + for (const auto& tile : frame.batch.tiles) { + const auto& pixels = *tile.frame->pixels; + const auto format = tile.frame->layout == Plot_Pixel_Layout::bgra8 + ? Wire_Pixel_Format::bgra8_premultiplied + : Wire_Pixel_Format::rgba8; + append_little_endian( + result.bytes, static_cast(tile.slot)); + append_little_endian( + result.bytes, static_cast(format)); + append_little_endian(result.bytes, 0U); + append_little_endian(result.bytes, 0U); + append_little_endian(result.bytes, tile.frame->width); + append_little_endian(result.bytes, tile.frame->height); + append_little_endian( + result.bytes, tile.frame->width * 4U); + append_little_endian( + result.bytes, static_cast(pixels.size())); + append_little_endian( + result.bytes, tile.frame->rendered_sequence); + result.bytes.append(reinterpret_cast(pixels.data()), + pixels.size()); + } return result; } diff --git a/web_server/src/frame_sampling/websocket_pixel/WebSocket_Pixel_Frame.hpp b/web_server/src/frame_sampling/websocket_pixel/WebSocket_Pixel_Frame.hpp index e0406a9..49a92fc 100644 --- a/web_server/src/frame_sampling/websocket_pixel/WebSocket_Pixel_Frame.hpp +++ b/web_server/src/frame_sampling/websocket_pixel/WebSocket_Pixel_Frame.hpp @@ -12,12 +12,11 @@ enum struct Wire_Pixel_Format : std::uint8_t { }; struct WebSocket_Pixel_Frame { - std::string bytes{}; /* 56 字节小端头部及未转换的原生像素负载。 */ - std::uint64_t sequence{}; /* 本二进制包对应的采样时钟序号。 */ - Wire_Pixel_Format format{Wire_Pixel_Format::rgba8}; /* 浏览器解释负载通道与 alpha 的依据。 */ + std::string bytes{}; /* v4 当前 tile 批次;40 字节批头后跟 tile 头和原生像素。 */ + std::uint64_t sequence{}; /* 本二进制批次对应的采样时钟序号。 */ }; -/* 只封装协议头并复制所有权,不做缩放、通道交换或 alpha 转换。 */ +/* 封装本次采样的全部当前 tile,不做缩放、图集合成、通道交换或 alpha 转换。 */ [[nodiscard]] WebSocket_Pixel_Frame pack_websocket_pixel_frame( const Sampled_Gallery_Frame& frame); diff --git a/web_server/tests/Frame_Sampler_Tests.cpp b/web_server/tests/Frame_Sampler_Tests.cpp index e4bf0eb..720f62f 100644 --- a/web_server/tests/Frame_Sampler_Tests.cpp +++ b/web_server/tests/Frame_Sampler_Tests.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace aethera::web::frame_sampling { @@ -18,21 +19,30 @@ std::shared_ptr native_bgra_frame() { std::move(pixels), Plot_Pixel_Layout::bgra8, std::chrono::microseconds{12'345}, 1, 9, 1, 9, 2, 2}); } + +std::uint64_t read_little_endian_u64(std::string_view bytes, + std::size_t offset) { + std::uint64_t value{}; + for (std::size_t index = 0; index < sizeof(value); ++index) + value |= static_cast( + static_cast(bytes[offset + index])) << + (index * 8U); + return value; +} } TEST(Frame_Sampler, First_Deadline_Samples_Latest_Without_Format_Conversion) { - Frame_Sampler sampler(30.0, 2, 2, 1, {"plot"}, 0); + Frame_Sampler sampler(30.0, 2, 2, {"plot"}, 0); ASSERT_EQ(sampler.accept_frame(0, native_bgra_frame()), Frame_Sampler::Accept_Frame_Result::accepted); - EXPECT_TRUE(sampler.has_pending_frame()); const auto sample = sampler.sample(); ASSERT_TRUE(sample); - EXPECT_FALSE(sampler.has_pending_frame()); - EXPECT_EQ(sample->composition.layout, Plot_Pixel_Layout::bgra8); - EXPECT_EQ(sample->composition.pixels[0], std::byte{11}); - EXPECT_EQ(sample->composition.pixels[1], std::byte{22}); - EXPECT_EQ(sample->composition.pixels[2], std::byte{33}); - EXPECT_EQ(sample->composition.pixels[3], std::byte{44}); + ASSERT_EQ(sample->batch.tiles.size(), 1U); + EXPECT_EQ(sample->batch.tiles[0].frame->layout, Plot_Pixel_Layout::bgra8); + EXPECT_EQ((*sample->batch.tiles[0].frame->pixels)[0], std::byte{11}); + EXPECT_EQ((*sample->batch.tiles[0].frame->pixels)[1], std::byte{22}); + EXPECT_EQ((*sample->batch.tiles[0].frame->pixels)[2], std::byte{33}); + EXPECT_EQ((*sample->batch.tiles[0].frame->pixels)[3], std::byte{44}); EXPECT_EQ(sample->tick.sequence, 9U); EXPECT_EQ(sample->sample_sequence, 1U); EXPECT_DOUBLE_EQ(sample->tick.time_milliseconds, 12.345); @@ -40,25 +50,62 @@ TEST(Frame_Sampler, First_Deadline_Samples_Latest_Without_Format_Conversion) { const auto state = sampler.state(); EXPECT_DOUBLE_EQ(state.target_frame_rate_fps, 30.0); EXPECT_EQ(state.sampled_frames, 1U); - EXPECT_EQ(state.early_ticks, 0U); + EXPECT_EQ(state.early_ticks, 1U); } -TEST(WebSocket_Pixel_Frame, Header_Describes_Unchanged_Native_Payload) { - Frame_Sampler sampler(30.0, 2, 2, 1, {"plot"}, 0); +TEST(WebSocket_Pixel_Frame, Header_Describes_Native_Payload_Without_Conversion) { + Frame_Sampler sampler(30.0, 2, 2, {"plot"}, 0); ASSERT_EQ(sampler.accept_frame(0, native_bgra_frame()), Frame_Sampler::Accept_Frame_Result::accepted); const auto sample = sampler.sample(); ASSERT_TRUE(sample); + const auto earliest_unix_ns = std::chrono::duration_cast< + std::chrono::nanoseconds>(std::chrono::system_clock::now() + .time_since_epoch()).count(); const auto packet = websocket_pixel::pack_websocket_pixel_frame(*sample); - ASSERT_EQ(packet.bytes.size(), 56U + 16U); + const auto latest_unix_ns = std::chrono::duration_cast< + std::chrono::nanoseconds>(std::chrono::system_clock::now() + .time_since_epoch()).count(); + ASSERT_EQ(packet.bytes.size(), 40U + 32U + 16U); EXPECT_EQ(packet.bytes.substr(0, 4), "AERP"); - EXPECT_EQ(static_cast(packet.bytes[4]), 2U); - EXPECT_EQ(static_cast(packet.bytes[6]), 56U); + EXPECT_EQ(static_cast(packet.bytes[4]), 4U); + EXPECT_EQ(static_cast(packet.bytes[6]), 40U); EXPECT_EQ(static_cast(packet.bytes[8]), 1U); - EXPECT_EQ(static_cast(packet.bytes[56]), 11U); - EXPECT_EQ(static_cast(packet.bytes[57]), 22U); - EXPECT_EQ(static_cast(packet.bytes[58]), 33U); - EXPECT_EQ(static_cast(packet.bytes[59]), 44U); + const auto packed_unix_ns = read_little_endian_u64(packet.bytes, 32U); + EXPECT_GE(packed_unix_ns, static_cast(earliest_unix_ns)); + EXPECT_LE(packed_unix_ns, static_cast(latest_unix_ns)); + EXPECT_EQ(static_cast(packet.bytes[44]), 1U); + EXPECT_EQ(static_cast(packet.bytes[48]), 2U); + EXPECT_EQ(static_cast(packet.bytes[72]), 11U); + EXPECT_EQ(static_cast(packet.bytes[73]), 22U); + EXPECT_EQ(static_cast(packet.bytes[74]), 33U); + EXPECT_EQ(static_cast(packet.bytes[75]), 44U); +} + +TEST(WebSocket_Pixel_Frame, Batch_Contains_All_Current_Source_Tiles) { + Frame_Sampler sampler(30.0, 2, 2, {"alpha", "beta"}, 0); + ASSERT_EQ(sampler.accept_frame(0, native_bgra_frame()), + Frame_Sampler::Accept_Frame_Result::accepted); + auto rgba_pixels = std::make_shared>(16U, + std::byte{91}); + auto rgba = std::make_shared(Plot_Pixel_Frame{ + std::move(rgba_pixels), Plot_Pixel_Layout::rgba8, + std::chrono::microseconds{12'345}, 2, 10, 2, 10, 2, 2}); + ASSERT_EQ(sampler.accept_frame(1, std::move(rgba)), + Frame_Sampler::Accept_Frame_Result::accepted); + const auto sample = sampler.sample(); + ASSERT_TRUE(sample); + ASSERT_EQ(sample->batch.tiles.size(), 2U); + const auto packet = websocket_pixel::pack_websocket_pixel_frame(*sample); + ASSERT_EQ(packet.bytes.size(), 40U + 2U * (32U + 16U)); + EXPECT_EQ(static_cast(packet.bytes[8]), 2U); + EXPECT_EQ(static_cast(packet.bytes[44]), 1U); + EXPECT_EQ(static_cast(packet.bytes[72]), 11U); + const auto second_header = 40U + 32U + 16U; + EXPECT_EQ(static_cast(packet.bytes[second_header]), 1U); + EXPECT_EQ(static_cast(packet.bytes[second_header + 4U]), 0U); + EXPECT_EQ(static_cast(packet.bytes[second_header + 32U]), + 91U); } } diff --git a/web_server/tests/Gallery_Frame_Atlas_Tests.cpp b/web_server/tests/Gallery_Frame_Atlas_Tests.cpp deleted file mode 100644 index 8557d1d..0000000 --- a/web_server/tests/Gallery_Frame_Atlas_Tests.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace aethera::web::detail { -namespace { -std::shared_ptr solid_frame( - std::uint64_t sequence, std::uint64_t correlation_id, - std::uint32_t width, std::uint32_t height, - std::byte red, std::byte green, std::byte blue, - std::uint64_t rendered_sequence = 0, - std::uint64_t rendered_correlation_id = 0) { - auto pixels = std::make_shared>( - static_cast(width) * height * 4U); - for (std::size_t offset = 0; offset < pixels->size(); offset += 4U) { - (*pixels)[offset] = red; - (*pixels)[offset + 1U] = green; - (*pixels)[offset + 2U] = blue; - (*pixels)[offset + 3U] = std::byte{255}; - } - return std::make_shared(Plot_Pixel_Frame{ - std::move(pixels), Plot_Pixel_Layout::rgba8, {}, sequence, correlation_id, - rendered_sequence == 0 ? sequence : rendered_sequence, - rendered_correlation_id == 0 ? correlation_id - : rendered_correlation_id, - width, height}); -} - -std::size_t pixel_offset(const Gallery_Atlas_Description& description, - std::uint32_t x, std::uint32_t y) { - return (static_cast(y) * description.width + x) * 4U; -} -} - -TEST(Gallery_Frame_Atlas, Layout_And_Changed_Tiles_Have_One_Authority) { - Gallery_Frame_Atlas atlas(2, 2, 2, {"alpha", "beta", "gamma"}); - const auto description = atlas.describe(); - ASSERT_EQ(description.columns, 2U); - ASSERT_EQ(description.rows, 2U); - ASSERT_EQ(description.width, 4U); - ASSERT_EQ(description.height, 4U); - ASSERT_EQ(description.sources.size(), 3U); - EXPECT_EQ(description.sources[2].column, 0U); - EXPECT_EQ(description.sources[2].row, 1U); - - EXPECT_EQ(atlas.accept_frame( - 0, solid_frame(1, 11, 2, 2, std::byte{10}, - std::byte{20}, std::byte{30})), - Gallery_Frame_Atlas::Accept_Frame_Result::accepted); - EXPECT_EQ(atlas.accept_frame( - 2, solid_frame(7, 17, 2, 2, std::byte{70}, - std::byte{80}, std::byte{90})), - Gallery_Frame_Atlas::Accept_Frame_Result::accepted); - - const auto first = atlas.compose(); - ASSERT_EQ(first.pixels.size(), 4U * 4U * 4U); - EXPECT_EQ(first.fresh_tile_count, 2U); - EXPECT_EQ(first.missing_tile_count, 1U); - EXPECT_EQ(first.sources[0].rendered_correlation_id, 11U); - EXPECT_EQ(first.sources[2].completion_count, 1U); - EXPECT_EQ(first.sources[2].rendered_frame_count, 1U); - const auto alpha = pixel_offset(description, 0, 0); - const auto gamma = pixel_offset(description, 0, 2); - EXPECT_EQ(first.pixels[alpha], std::byte{10}); - EXPECT_EQ(first.pixels[alpha + 3U], std::byte{255}); - EXPECT_EQ(first.pixels[gamma], std::byte{70}); - EXPECT_EQ(first.pixels[gamma + 2U], std::byte{90}); - - const auto unchanged = atlas.compose(); - EXPECT_EQ(unchanged.fresh_tile_count, 0U); - EXPECT_EQ(unchanged.missing_tile_count, 1U); -} - -TEST(Gallery_Frame_Atlas, Invalid_And_Stale_Frames_Do_Not_Replace_Latest) { - Gallery_Frame_Atlas atlas(2, 2, 1, {"only"}); - EXPECT_EQ(atlas.accept_frame( - 0, solid_frame(4, 14, 2, 2, std::byte{4}, - std::byte{5}, std::byte{6})), - Gallery_Frame_Atlas::Accept_Frame_Result::accepted); - EXPECT_EQ(atlas.accept_frame( - 0, solid_frame(3, 13, 2, 2, std::byte{30}, - std::byte{31}, std::byte{32})), - Gallery_Frame_Atlas::Accept_Frame_Result::stale_frame); - EXPECT_EQ(atlas.accept_frame( - 0, solid_frame(5, 15, 4, 2, std::byte{50}, - std::byte{51}, std::byte{52})), - Gallery_Frame_Atlas::Accept_Frame_Result::invalid_frame); - const auto composition = atlas.compose(); - EXPECT_EQ(composition.rejected_frame_count, 2U); - EXPECT_EQ(composition.sources[0].completion_sequence, 4U); - EXPECT_EQ(composition.pixels[0], std::byte{4}); - EXPECT_EQ(atlas.compose().rejected_frame_count, 0U); -} - -TEST(Gallery_Frame_Atlas, Logical_History_Callback_Does_Not_Fake_A_New_Image) { - Gallery_Frame_Atlas atlas(2, 2, 1, {"only"}); - ASSERT_EQ(atlas.accept_frame( - 0, solid_frame(4, 14, 2, 2, std::byte{4}, - std::byte{5}, std::byte{6})), - Gallery_Frame_Atlas::Accept_Frame_Result::accepted); - ASSERT_EQ(atlas.compose().fresh_tile_count, 1U); - ASSERT_EQ(atlas.accept_frame( - 0, solid_frame(5, 15, 2, 2, std::byte{50}, - std::byte{51}, std::byte{52}, 4, 14)), - Gallery_Frame_Atlas::Accept_Frame_Result::accepted); - const auto composition = atlas.compose(); - EXPECT_EQ(composition.fresh_tile_count, 0U); - EXPECT_EQ(composition.sources[0].completion_count, 2U); - EXPECT_EQ(composition.sources[0].rendered_frame_count, 1U); - EXPECT_EQ(composition.sources[0].completion_sequence, 5U); - EXPECT_EQ(composition.sources[0].rendered_sequence, 4U); - EXPECT_EQ(composition.pixels[0], std::byte{4}); -} - -TEST(Gallery_Frame_Atlas, Diagnostics_Only_Completion_Preserves_Progress_And_Image) { - Gallery_Frame_Atlas atlas(2, 2, 1, {"only"}); - ASSERT_EQ(atlas.accept_frame( - 0, solid_frame(1, 11, 2, 2, std::byte{7}, - std::byte{8}, std::byte{9})), - Gallery_Frame_Atlas::Accept_Frame_Result::accepted); - ASSERT_EQ(atlas.compose().fresh_tile_count, 1U); - - auto diagnostics = std::make_shared( - Plot_Pixel_Frame{{}, Plot_Pixel_Layout::rgba8, {}, 2, 12, 2, 12, 2, 2}); - ASSERT_EQ(atlas.accept_frame(0, std::move(diagnostics)), - Gallery_Frame_Atlas::Accept_Frame_Result::accepted); - const auto composition = atlas.compose(); - EXPECT_EQ(composition.fresh_tile_count, 0U); - EXPECT_EQ(composition.missing_tile_count, 0U); - EXPECT_EQ(composition.sources[0].completion_sequence, 2U); - EXPECT_EQ(composition.sources[0].rendered_sequence, 2U); - EXPECT_EQ(composition.sources[0].completion_count, 2U); - EXPECT_EQ(composition.sources[0].rendered_frame_count, 2U); - EXPECT_EQ(composition.pixels[0], std::byte{7}); -} - -TEST(Gallery_Frame_Atlas, Plot_Producers_Accept_In_Parallel_While_Encoding_Composes) { - constexpr std::size_t source_count{8}; - constexpr std::uint64_t frame_count{500}; - std::vector ids; - ids.reserve(source_count); - for (std::size_t slot = 0; slot < source_count; ++slot) - ids.push_back("source-" + std::to_string(slot)); - Gallery_Frame_Atlas atlas(2, 2, 4, std::move(ids)); - - std::atomic_size_t completed_producers{}; - std::vector producers; - producers.reserve(source_count); - for (std::size_t slot = 0; slot < source_count; ++slot) { - producers.emplace_back([&, slot] { - for (std::uint64_t sequence = 1; sequence <= frame_count; - ++sequence) { - const auto color = std::byte{ - static_cast((slot + sequence) % 251U)}; - EXPECT_EQ(atlas.accept_frame( - slot, solid_frame( - sequence, sequence, 2, 2, color, - std::byte{static_cast(slot)}, - std::byte{0})), - Gallery_Frame_Atlas::Accept_Frame_Result::accepted); - } - completed_producers.fetch_add(1, std::memory_order_release); - }); - } - - while (completed_producers.load(std::memory_order_acquire) < source_count) - static_cast(atlas.compose()); - for (auto& producer : producers) producer.join(); - - const auto final = atlas.compose(); - ASSERT_EQ(final.sources.size(), source_count); - EXPECT_EQ(final.missing_tile_count, 0U); - EXPECT_EQ(final.rejected_frame_count, 0U); - for (const auto& source : final.sources) { - EXPECT_EQ(source.completion_sequence, frame_count); - EXPECT_EQ(source.rendered_sequence, frame_count); - EXPECT_EQ(source.completion_count, frame_count); - EXPECT_EQ(source.rendered_frame_count, frame_count); - } -} -} diff --git a/web_server/tests/Gallery_Frame_Batch_Tests.cpp b/web_server/tests/Gallery_Frame_Batch_Tests.cpp new file mode 100644 index 0000000..fd3bb61 --- /dev/null +++ b/web_server/tests/Gallery_Frame_Batch_Tests.cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include + +namespace aethera::web::detail { +namespace { +std::shared_ptr solid_frame( + std::uint64_t sequence, std::uint64_t correlation_id, + std::byte color, std::uint64_t rendered_sequence = 0, + bool include_pixels = true) { + auto pixels = include_pixels + ? std::make_shared>(16U, color) + : std::shared_ptr>{}; + return std::make_shared(Plot_Pixel_Frame{ + std::move(pixels), Plot_Pixel_Layout::rgba8, {}, sequence, + correlation_id, rendered_sequence == 0 ? sequence : rendered_sequence, + correlation_id, 2, 2}); +} +} + +TEST(Gallery_Frame_Batch, Samples_Current_Tiles_With_Stable_Slots) { + Gallery_Frame_Batch batch(2, 2, {"alpha", "beta", "gamma"}); + const auto description = batch.describe(); + ASSERT_EQ(description.sources.size(), 3U); + EXPECT_EQ(description.sources[2].id, "gamma"); + EXPECT_EQ(description.sources[2].slot, 2U); + + ASSERT_EQ(batch.accept_frame(0, solid_frame(1, 11, std::byte{10})), + Gallery_Frame_Batch::Accept_Frame_Result::accepted); + ASSERT_EQ(batch.accept_frame(2, solid_frame(7, 17, std::byte{70})), + Gallery_Frame_Batch::Accept_Frame_Result::accepted); + const auto first = batch.sample(); + ASSERT_EQ(first.tiles.size(), 2U); + EXPECT_EQ(first.missing_tile_count, 1U); + EXPECT_EQ(first.tiles[0].slot, 0U); + EXPECT_EQ(first.tiles[1].slot, 2U); + EXPECT_EQ((*first.tiles[0].frame->pixels)[0], std::byte{10}); + EXPECT_EQ((*first.tiles[1].frame->pixels)[0], std::byte{70}); + EXPECT_EQ(batch.sample().tiles.size(), 2U); + +} + +TEST(Gallery_Frame_Batch, Invalid_And_Stale_Frames_Do_Not_Replace_Latest) { + Gallery_Frame_Batch batch(2, 2, {"only"}); + ASSERT_EQ(batch.accept_frame(0, solid_frame(4, 14, std::byte{4})), + Gallery_Frame_Batch::Accept_Frame_Result::accepted); + EXPECT_EQ(batch.accept_frame(0, solid_frame(3, 13, std::byte{30})), + Gallery_Frame_Batch::Accept_Frame_Result::stale_frame); + auto invalid = solid_frame(5, 15, std::byte{50}); + auto mutable_invalid = std::make_shared(*invalid); + mutable_invalid->width = 4; + EXPECT_EQ(batch.accept_frame(0, std::move(mutable_invalid)), + Gallery_Frame_Batch::Accept_Frame_Result::invalid_frame); + const auto sample = batch.sample(); + ASSERT_EQ(sample.tiles.size(), 1U); + EXPECT_EQ(sample.rejected_frame_count, 2U); + EXPECT_EQ(sample.sources[0].completion_sequence, 4U); + EXPECT_EQ((*sample.tiles[0].frame->pixels)[0], std::byte{4}); +} + +TEST(Gallery_Frame_Batch, Logical_Completion_References_Current_Pixels) { + Gallery_Frame_Batch batch(2, 2, {"only"}); + ASSERT_EQ(batch.accept_frame(0, solid_frame(4, 14, std::byte{4})), + Gallery_Frame_Batch::Accept_Frame_Result::accepted); + ASSERT_EQ(batch.sample().tiles.size(), 1U); + ASSERT_EQ(batch.accept_frame( + 0, solid_frame(5, 15, std::byte{50}, 4)), + Gallery_Frame_Batch::Accept_Frame_Result::accepted); + const auto sample = batch.sample(); + ASSERT_EQ(sample.tiles.size(), 1U); + EXPECT_EQ(sample.tiles[0].frame->rendered_sequence, 4U); + EXPECT_EQ(sample.sources[0].completion_count, 2U); + EXPECT_EQ(sample.sources[0].rendered_frame_count, 1U); + EXPECT_EQ(sample.sources[0].completion_sequence, 5U); + EXPECT_EQ(sample.sources[0].rendered_sequence, 4U); +} + +TEST(Gallery_Frame_Batch, Diagnostics_Completion_Preserves_Latest_Image) { + Gallery_Frame_Batch batch(2, 2, {"only"}); + ASSERT_EQ(batch.accept_frame(0, solid_frame(1, 11, std::byte{7})), + Gallery_Frame_Batch::Accept_Frame_Result::accepted); + ASSERT_EQ(batch.sample().tiles.size(), 1U); + ASSERT_EQ(batch.accept_frame( + 0, solid_frame(2, 12, std::byte{}, 2, false)), + Gallery_Frame_Batch::Accept_Frame_Result::accepted); + const auto sample = batch.sample(); + ASSERT_EQ(sample.tiles.size(), 1U); + EXPECT_EQ((*sample.tiles[0].frame->pixels)[0], std::byte{7}); + EXPECT_EQ(sample.missing_tile_count, 0U); + EXPECT_EQ(sample.sources[0].completion_sequence, 2U); + EXPECT_EQ(sample.sources[0].completion_count, 2U); +} + +} diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index f2133c4..8164a4a 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -48,23 +48,24 @@ type Plot_Diagnostics = {protocol: "aethera.plot.diagnostics"; version: 4; dimen type Frame_Sample = {sequence: number; generated_at_ms: number; received_at_ms: number; values: Frame_Stage_Values}; type Pixel_Presentation_Metrics = {frame_rate_fps: number; presented_frames: number; dropped_frames: number; jitter_buffer_ms: number; decode_processing_ms: number; estimated_playout_delay_ms: number; - freeze_count: number; current_time_seconds: number; ready_state: number}; -type Gallery_Tile = {column: number; row: number}; -type Gallery_Layout = {kind: "gallery_layout"; protocol: "aethera.gallery.pixels"; version: 4; - transport: "drogon_pixels"; frame_rate_fps?: number; columns: number; rows: number; - tile_width: number; tile_height: number; width: number; height: number; plots: Record}; + websocket_arrival_ms: number; browser_queue_ms: number; browser_render_ms: number; + frame_latency_ms: number; freeze_count: number; current_time_seconds: number; ready_state: number}; +type Gallery_Source = {slot: number}; +type Gallery_Layout = {kind: "gallery_layout"; protocol: "aethera.gallery.pixels"; version: 6; + transport: "drogon_pixels"; frame_rate_fps?: number; + tile_width: number; tile_height: number; plots: Record}; type Gallery_Source_Metrics = {has_rendered_frame: boolean; logical_completion_rate_fps: number; rendered_frame_rate_fps: number; logical_completion_count: number; rendered_frame_count: number; latest_completion_sequence: number; latest_rendered_sequence: number; latest_rendered_clock_sequence: number; frame_lag: number}; -type Gallery_Pixel_Metrics = {kind: "gallery_metrics"; protocol: "aethera.gallery.pixels"; version: 4; clock_sequence: number; +type Gallery_Pixel_Metrics = {kind: "gallery_metrics"; protocol: "aethera.gallery.pixels"; version: 6; clock_sequence: number; sampled_frame_count: number; pixel_frame_count: number; target_frame_rate_fps: number; clock_delivery_rate_fps: number; sampled_frame_rate_fps: number; - pixel_frame_rate_fps: number; compose_average_ms: number; compose_p95_ms: number; + pixel_frame_rate_fps: number; batch_sample_average_ms: number; batch_sample_p95_ms: number; pixel_pack_average_ms: number; pixel_pack_p95_ms: number; sample_delay_average_ms: number; sample_delay_p95_ms: number; - publish_average_ms: number; publish_p95_ms: number; pixel_bytes: number; fresh_tiles: number; missing_tiles: number; + publish_average_ms: number; publish_p95_ms: number; pixel_bytes: number; transmitted_tiles: number; missing_tiles: number; rejected_frames: number; skipped_sample_ticks: number; sources: Record}; type Gallery_Pixel_State = {status: Stream_Status; layout: Gallery_Layout | null; - pixel_surface: Gallery_Pixel_Surface | null; presentation: Pixel_Presentation_Metrics; + pixel_group: Gallery_Pixel_Group | null; presentation: Pixel_Presentation_Metrics; transport: Gallery_Pixel_Metrics | null; error: string | null}; type Gallery_Pixel_States = Record; type Frame_Diagnostics = {server: Plot_Diagnostics; samples: Frame_Sample[]; pixel_presentation: Pixel_Presentation_Metrics; @@ -160,10 +161,9 @@ function valid_gallery_layout(value: unknown): value is Gallery_Layout { if (!value || typeof value !== "object") return false; const layout = value as Partial; return layout.kind === "gallery_layout" && - layout.protocol === "aethera.gallery.pixels" && layout.version === 4 && + layout.protocol === "aethera.gallery.pixels" && layout.version === 6 && layout.transport === "drogon_pixels" && typeof layout.tile_width === "number" && typeof layout.tile_height === "number" && - typeof layout.width === "number" && typeof layout.height === "number" && Boolean(layout.plots); } @@ -171,7 +171,7 @@ function valid_gallery_metrics(value: unknown): value is Gallery_Pixel_Metrics { if (!value || typeof value !== "object") return false; const metrics = value as Partial; return metrics.kind === "gallery_metrics" && - metrics.protocol === "aethera.gallery.pixels" && metrics.version === 4 && + metrics.protocol === "aethera.gallery.pixels" && metrics.version === 6 && typeof metrics.pixel_frame_rate_fps === "number" && typeof metrics.clock_delivery_rate_fps === "number" && Boolean(metrics.sources); @@ -226,35 +226,62 @@ const empty_pixel_presentation = (): Pixel_Presentation_Metrics => ({ frame_rate_fps: 0, presented_frames: 0, dropped_frames: 0, jitter_buffer_ms: 0, decode_processing_ms: 0, estimated_playout_delay_ms: 0, freeze_count: 0, + websocket_arrival_ms: 0, browser_queue_ms: 0, browser_render_ms: 0, + frame_latency_ms: 0, current_time_seconds: 0, ready_state: 0 }); -type Gallery_Pixel_Packet = {format: number; width: number; height: number; stride: number; - payload_bytes: number; sequence: number; presentation_microseconds: number; - packed_steady_nanoseconds: number; pixels: Uint8Array}; +type Gallery_Pixel_Tile = {slot: number; format: number; width: number; height: number; + stride: number; rendered_sequence: number; pixels: Uint8Array}; +type Gallery_Pixel_Batch = {sequence: number; presentation_microseconds: number; + packed_unix_milliseconds: number; tiles: Gallery_Pixel_Tile[]}; -function parse_gallery_pixel_packet(buffer: ArrayBuffer): Gallery_Pixel_Packet { - if (buffer.byteLength < 56) throw new Error("原始像素帧头部不完整"); +type Pending_Gallery_Pixels = {buffer: ArrayBuffer; received_unix_ms: number; + received_performance_ms: number}; + +function parse_gallery_pixel_batch(buffer: ArrayBuffer): Gallery_Pixel_Batch { + if (buffer.byteLength < 40) throw new Error("原始像素批头部不完整"); const bytes = new Uint8Array(buffer); if (bytes[0] !== 0x41 || bytes[1] !== 0x45 || bytes[2] !== 0x52 || bytes[3] !== 0x50) throw new Error("原始像素帧 magic 不匹配"); const view = new DataView(buffer); const version = view.getUint16(4, true); const header_bytes = view.getUint16(6, true); - const format = view.getUint8(8); - const width = view.getUint32(12, true); - const height = view.getUint32(16, true); - const stride = view.getUint32(20, true); - const payload_bytes = view.getUint32(24, true); - if (version !== 2 || header_bytes !== 56 || (format !== 0 && format !== 1) || - width === 0 || height === 0 || stride !== width * 4 || - payload_bytes !== stride * height || buffer.byteLength !== header_bytes + payload_bytes) - throw new Error("原始像素帧协议字段无效"); - return {format, width, height, stride, payload_bytes, - sequence: Number(view.getBigUint64(28, true)), - presentation_microseconds: Number(view.getBigUint64(36, true)), - packed_steady_nanoseconds: Number(view.getBigUint64(44, true)), - pixels: new Uint8Array(buffer, header_bytes, payload_bytes)}; + const tile_count = view.getUint16(8, true); + const payload_bytes = view.getUint32(12, true); + if (version !== 4 || header_bytes !== 40 || tile_count === 0 || + buffer.byteLength !== header_bytes + payload_bytes) + throw new Error("原始像素批协议字段无效"); + const packed_unix_nanoseconds = view.getBigUint64(32, true); + const tiles: Gallery_Pixel_Tile[] = []; + let offset = header_bytes; + for (let index = 0; index < tile_count; ++index) { + if (offset + 32 > buffer.byteLength) + throw new Error("原始像素 tile 头部不完整"); + const slot = view.getUint32(offset, true); + const format = view.getUint8(offset + 4); + const width = view.getUint32(offset + 8, true); + const height = view.getUint32(offset + 12, true); + const stride = view.getUint32(offset + 16, true); + const tile_bytes = view.getUint32(offset + 20, true); + const rendered_sequence = Number(view.getBigUint64(offset + 24, true)); + const pixel_offset = offset + 32; + if ((format !== 0 && format !== 1) || width === 0 || height === 0 || + stride !== width * 4 || tile_bytes !== stride * height || + pixel_offset + tile_bytes > buffer.byteLength) + throw new Error("原始像素 tile 字段无效"); + tiles.push({slot, format, width, height, stride, rendered_sequence, + pixels: new Uint8Array(buffer, pixel_offset, tile_bytes)}); + offset = pixel_offset + tile_bytes; + } + if (offset !== buffer.byteLength) + throw new Error("原始像素批存在尾随字节"); + return {tiles, + sequence: Number(view.getBigUint64(16, true)), + presentation_microseconds: Number(view.getBigUint64(24, true)), + packed_unix_milliseconds: + Number(packed_unix_nanoseconds / 1_000_000n) + + Number(packed_unix_nanoseconds % 1_000_000n) / 1_000_000}; } function compile_gallery_shader(gl: WebGL2RenderingContext, type: number, source: string) { @@ -270,26 +297,27 @@ function compile_gallery_shader(gl: WebGL2RenderingContext, type: number, source return shader; } -/* 每个像素组只上传一次图集纹理;各 Plot Canvas 只裁剪自己的 tile。 */ -class Gallery_Pixel_Surface { - readonly canvas = document.createElement("canvas"); +/* 每个像素组只占用一个 WebGL context。Plot Canvas 只接收 ImageBitmap + * 所有权,不经过 Canvas2D drawImage,也不会触发浏览器的 WebGL context 上限。 */ +class Gallery_Pixel_Uploader { private readonly gl: WebGL2RenderingContext; private readonly texture: WebGLTexture; + private readonly program: WebGLProgram; + private readonly vertex_buffer: WebGLBuffer; private readonly format_location: WebGLUniformLocation; - private texture_width = 0; - private texture_height = 0; - private presented_frames = 0; - private previous_presented_frames = 0; - private previous_sample_time = performance.now(); - private latest_presentation_microseconds = 0; + private texture_initialized = false; - constructor() { - const gl = this.canvas.getContext("webgl2", { - alpha: true, premultipliedAlpha: true, preserveDrawingBuffer: true, + constructor(private readonly width: number, private readonly height: number) { + if (typeof OffscreenCanvas === "undefined") + throw new Error("原始像素模式需要 OffscreenCanvas"); + const canvas = new OffscreenCanvas(width, height); + const gl = canvas.getContext("webgl2", { + alpha: true, premultipliedAlpha: true, antialias: false, depth: false, stencil: false - }); + }) as WebGL2RenderingContext | null; if (!gl) throw new Error("WebSocket 原始像素模式需要 WebGL2"); this.gl = gl; + this.canvas = canvas; const vertex = compile_gallery_shader(gl, gl.VERTEX_SHADER, `#version 300 es in vec2 position; in vec2 texture_coordinate; out vec2 uv; void main() { gl_Position = vec4(position, 0.0, 1.0); uv = texture_coordinate; }`); @@ -304,11 +332,13 @@ class Gallery_Pixel_Surface { gl.deleteShader(vertex); gl.deleteShader(fragment); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(`原始像素 program 链接失败: ${gl.getProgramInfoLog(program) ?? "unknown"}`); + this.program = program; gl.useProgram(program); const position = gl.getAttribLocation(program, "position"); const texture_coordinate = gl.getAttribLocation(program, "texture_coordinate"); const buffer = gl.createBuffer(); if (!buffer) throw new Error("无法创建原始像素顶点缓冲"); + this.vertex_buffer = buffer; gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0, 1, 1, 1, 0 @@ -320,6 +350,7 @@ class Gallery_Pixel_Surface { if (!texture) throw new Error("无法创建原始像素纹理"); this.texture = texture; gl.bindTexture(gl.TEXTURE_2D, texture); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); @@ -328,37 +359,121 @@ class Gallery_Pixel_Surface { const format_location = gl.getUniformLocation(program, "pixel_format"); if (!format_location) throw new Error("原始像素 shader 缺少 pixel_format"); this.format_location = format_location; + gl.viewport(0, 0, width, height); } - render(buffer: ArrayBuffer) { - const packet = parse_gallery_pixel_packet(buffer); + private readonly canvas: OffscreenCanvas; + + render(tile: Gallery_Pixel_Tile): ImageBitmap { + if (tile.width !== this.width || tile.height !== this.height) + throw new Error("同一像素组的 tile 尺寸发生变化"); const gl = this.gl; - if (this.texture_width !== packet.width || this.texture_height !== packet.height) { - this.canvas.width = packet.width; this.canvas.height = packet.height; - gl.viewport(0, 0, packet.width, packet.height); - this.texture_width = packet.width; this.texture_height = packet.height; - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, packet.width, packet.height, - 0, gl.RGBA, gl.UNSIGNED_BYTE, packet.pixels); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + if (!this.texture_initialized) { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, tile.width, tile.height, + 0, gl.RGBA, gl.UNSIGNED_BYTE, tile.pixels); + this.texture_initialized = true; } else { - gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, packet.width, packet.height, - gl.RGBA, gl.UNSIGNED_BYTE, packet.pixels); + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, tile.width, tile.height, + gl.RGBA, gl.UNSIGNED_BYTE, tile.pixels); } - gl.uniform1i(this.format_location, packet.format); + gl.uniform1i(this.format_location, tile.format); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); - this.presented_frames += 1; - this.latest_presentation_microseconds = packet.presentation_microseconds; - return packet.sequence; + return this.canvas.transferToImageBitmap(); } - draw_tile(target: HTMLCanvasElement, tile: Gallery_Tile, width: number, height: number) { - if (target.width !== width) target.width = width; - if (target.height !== height) target.height = height; - const context = target.getContext("2d", {alpha: true}); - if (!context) throw new Error("无法创建 Plot Canvas 2D context"); - context.clearRect(0, 0, width, height); - context.drawImage(this.canvas, tile.column * width, tile.row * height, - width, height, 0, 0, width, height); + dispose() { + this.gl.deleteTexture(this.texture); + this.gl.deleteBuffer(this.vertex_buffer); + this.gl.deleteProgram(this.program); } +} + +class Gallery_Plot_Surface { + private readonly context: ImageBitmapRenderingContext; + + constructor(private readonly canvas: HTMLCanvasElement) { + const context = canvas.getContext("bitmaprenderer"); + if (!context) + throw new Error("原始像素模式需要 ImageBitmapRenderingContext"); + this.context = context; + } + + render(bitmap: ImageBitmap, width: number, height: number) { + if (this.canvas.width !== width || this.canvas.height !== height) { + this.canvas.width = width; + this.canvas.height = height; + } + this.context.transferFromImageBitmap(bitmap); + } +} + +class Gallery_Pixel_Group { + private readonly surfaces = new Map(); + private slots = new Map(); + private uploader: Gallery_Pixel_Uploader | null = null; + private ready = false; + private presented_frames = 0; + private previous_presented_frames = 0; + private previous_sample_time = performance.now(); + private latest_presentation_microseconds = 0; + private dropped_frames = 0; + private websocket_arrival_ms = 0; + private browser_queue_ms = 0; + private browser_render_ms = 0; + private frame_latency_ms = 0; + constructor(private readonly readiness: (ready: boolean) => void) {} + + private update_readiness() { + const ready = this.slots.size > 0 && + [...this.slots.values()].every(plot_id => this.surfaces.has(plot_id)); + if (ready === this.ready) return; + this.ready = ready; + this.readiness(ready); + } + + set_layout(layout: Gallery_Layout) { + this.slots = new Map(Object.entries(layout.plots) + .map(([plot_id, source]) => [source.slot, plot_id])); + this.update_readiness(); + } + + register(plot_id: string, canvas: HTMLCanvasElement) { + this.surfaces.delete(plot_id); + const surface = new Gallery_Plot_Surface(canvas); + this.surfaces.set(plot_id, surface); + this.update_readiness(); + return () => { + if (this.surfaces.get(plot_id) !== surface) return; + this.surfaces.delete(plot_id); + this.update_readiness(); + }; + } + + render(pending: Pending_Gallery_Pixels) { + const batch = parse_gallery_pixel_batch(pending.buffer); + const rendering_started = performance.now(); + for (const tile of batch.tiles) { + const plot_id = this.slots.get(tile.slot); + const surface = plot_id ? this.surfaces.get(plot_id) : undefined; + if (!surface) continue; + this.uploader ??= new Gallery_Pixel_Uploader(tile.width, tile.height); + surface.render(this.uploader.render(tile), tile.width, tile.height); + } + this.latest_presentation_microseconds = batch.presentation_microseconds; + this.websocket_arrival_ms = Math.max(0, + pending.received_unix_ms - batch.packed_unix_milliseconds); + this.browser_queue_ms = Math.max(0, + rendering_started - pending.received_performance_ms); + this.browser_render_ms = Math.max(0, + performance.now() - rendering_started); + this.frame_latency_ms = Math.max(0, + Date.now() - batch.packed_unix_milliseconds); + this.presented_frames += 1; + return batch.sequence; + } + + drop_pending_frame() { this.dropped_frames += 1; } presentation(): Pixel_Presentation_Metrics { const now = performance.now(); @@ -369,15 +484,30 @@ class Gallery_Pixel_Surface { this.previous_presented_frames = this.presented_frames; return {...empty_pixel_presentation(), frame_rate_fps, presented_frames: this.presented_frames, + dropped_frames: this.dropped_frames, + jitter_buffer_ms: this.browser_queue_ms, + decode_processing_ms: this.browser_render_ms, + estimated_playout_delay_ms: this.frame_latency_ms, + websocket_arrival_ms: this.websocket_arrival_ms, + browser_queue_ms: this.browser_queue_ms, + browser_render_ms: this.browser_render_ms, + frame_latency_ms: this.frame_latency_ms, current_time_seconds: this.latest_presentation_microseconds / 1_000_000, ready_state: this.presented_frames > 0 ? 4 : 0}; } - dispose() { this.gl.deleteTexture(this.texture); } + dispose() { + if (this.ready) this.readiness(false); + this.ready = false; + this.uploader?.dispose(); + this.uploader = null; + this.surfaces.clear(); + this.slots.clear(); + } } const connecting_gallery_pixels = (): Gallery_Pixel_State => ({ - status: "CONNECTING", layout: null, pixel_surface: null, + status: "CONNECTING", layout: null, pixel_group: null, presentation: empty_pixel_presentation(), transport: null, error: null }); @@ -395,9 +525,9 @@ function use_gallery_pixels(plots: Plot[]): Gallery_Pixel_States { ]))); const cleanups = pixels.map(endpoint => { let stopped = false; - let pixel_surface: Gallery_Pixel_Surface | null = null; + let pixel_group: Gallery_Pixel_Group | null = null; let pixel_live = false; - let pending_pixels: ArrayBuffer | null = null; + let pending_pixels: Pending_Gallery_Pixels | null = null; let animation_frame = 0; const update = (change: (current: Gallery_Pixel_State) => Gallery_Pixel_State) => set_states(current => ({...current, @@ -421,11 +551,15 @@ function use_gallery_pixels(plots: Plot[]): Gallery_Pixel_States { }); socket.binaryType = "arraybuffer"; socket.onopen = () => { - pixel_surface?.dispose(); - pixel_surface = new Gallery_Pixel_Surface(); + pixel_group?.dispose(); + pixel_group = new Gallery_Pixel_Group(ready => { + if (socket.readyState !== WebSocket.OPEN) return; + socket.send(JSON.stringify({kind: ready + ? "pixel_stream_ready" : "pixel_stream_not_ready"})); + }); pixel_live = false; update(current => ({...current, status: "CONNECTING", error: null, - pixel_surface})); + pixel_group})); }; socket.onclose = () => { if (!stopped) update(current => ({...current, @@ -433,21 +567,19 @@ function use_gallery_pixels(plots: Plot[]): Gallery_Pixel_States { }; const present_latest_pixels = () => { animation_frame = 0; - const buffer = pending_pixels; + const pending = pending_pixels; pending_pixels = null; - if (!buffer || stopped || !pixel_surface) return; + if (!pending || stopped || !pixel_group) return; try { - const sequence = pixel_surface.render(buffer); - if (socket.readyState === WebSocket.OPEN) - socket.send(JSON.stringify({ - kind: "pixel_frame_consumed", sequence})); + const sequence = pixel_group.render(pending); if (!pixel_live) { pixel_live = true; update(current => ({...current, status: "LIVE", - pixel_surface, error: null})); + pixel_group, error: null})); } - window.dispatchEvent(new CustomEvent("aethera:gallery-pixel-frame", - {detail: {pixels: endpoint, surface: pixel_surface, sequence}})); + if (socket.readyState === WebSocket.OPEN) + socket.send(JSON.stringify({ + kind: "pixel_frame_consumed", sequence})); } catch (error) { update(current => ({...current, status: "OFFLINE", error: error instanceof Error ? error.message : @@ -457,20 +589,24 @@ function use_gallery_pixels(plots: Plot[]): Gallery_Pixel_States { animation_frame = window.requestAnimationFrame( present_latest_pixels); }; - const queue_pixels = (buffer: ArrayBuffer) => { + const queue_pixels = (pending: Pending_Gallery_Pixels) => { if (stopped) return; - pending_pixels = buffer; + if (pending_pixels) pixel_group?.drop_pending_frame(); + pending_pixels = pending; if (animation_frame === 0) animation_frame = window.requestAnimationFrame( present_latest_pixels); }; socket.onmessage = event => { if (typeof event.data !== "string") { - if (!pixel_surface) return; + if (!pixel_group) return; + const received = {received_unix_ms: Date.now(), + received_performance_ms: performance.now()}; if (event.data instanceof ArrayBuffer) - queue_pixels(event.data); + queue_pixels({buffer: event.data, ...received}); else if (event.data instanceof Blob) - void event.data.arrayBuffer().then(queue_pixels); + void event.data.arrayBuffer().then(buffer => + queue_pixels({buffer, ...received})); return; } let decoded: unknown; @@ -478,6 +614,7 @@ function use_gallery_pixels(plots: Plot[]): Gallery_Pixel_States { if (!decoded || typeof decoded !== "object") return; const message = decoded as Record; if (valid_gallery_layout(decoded)) { + pixel_group?.set_layout(decoded); update(current => ({...current, layout: decoded})); window.dispatchEvent(new CustomEvent("aethera:gallery-layout", {detail: {...decoded, pixels: endpoint}})); @@ -490,9 +627,9 @@ function use_gallery_pixels(plots: Plot[]): Gallery_Pixel_States { } }; const stats_timer = window.setInterval(() => { - if (pixel_surface) + if (pixel_group) update(current => ({...current, - presentation: pixel_surface!.presentation()})); + presentation: pixel_group!.presentation()})); }, 1000); return () => { stopped = true; @@ -500,7 +637,7 @@ function use_gallery_pixels(plots: Plot[]): Gallery_Pixel_States { window.removeEventListener("aethera:gallery-diagnostics", receive_server_diagnostics); window.cancelAnimationFrame(animation_frame); - pixel_surface?.dispose(); + pixel_group?.dispose(); socket.close(); }; }); @@ -2529,7 +2666,7 @@ function Taskflow_Runtime_Pane() { return
全局执行域

Taskflow 总体观测

每秒读取一次累计 Observer 状态;逐帧拓扑捕获在各图的“Taskflow 帧分析”页按需开启。

{error ?

{error}

: null}{state ? <>
-
Worker
{state.worker_count}
任务体墙钟占用
{state.worker_utilization.toFixed(1)}%
+
Worker
{state.worker_count}
任务驻留墙钟
{state.worker_utilization.toFixed(1)}%
线程 CPU 占用
{state.worker_cpu_utilization.toFixed(1)}%
活跃 Worker
{state.active_workers}/{state.worker_count}
活跃任务
{state.active_tasks}
活跃 Topology
{state.active_topologies}
活跃 Taskflow
{state.active_taskflows}
@@ -2537,13 +2674,13 @@ function Taskflow_Runtime_Pane() {
队列峰值
{state.peak_worker_queue_size}
最长任务
{nanoseconds(state.longest_task.time_ns)}
协作让出
{state.cooperative_wait_count.toLocaleString("zh-CN")} 次 · {nanoseconds(state.cooperative_wait_time_ns)}
-

任务体墙钟只累计 Worker 实际执行节点的独占片段,显式协作让出已经扣除。线程 CPU 是 Worker 原生线程从首次任务起的总 CPU,包含 Executor 调度开销,因此不再用“任务墙钟减线程 CPU”伪造非 CPU 等待。队列只报告任务进入时的本地队列样本和累计峰值。

-
Worker 占用、协作让出与本地队列累计值,不在 GET 时重新计算任务样本。
{state.workers.map(worker =>
-
Worker {worker.id}墙 {worker.utilization.toFixed(1)}% · CPU {worker.cpu_utilization.toFixed(1)}%
-
+

任务驻留墙钟统计任务函数的生命周期,显式协作让出已经扣除,但 Windows 抢占、等待 CPU 和任务内部未声明的阻塞仍在其中。线程 CPU 才是 Worker 真正获得的处理器时间。两者差值不能直接叫浪费:要结合逐帧节点、协作等待和队列判断。当前 32 个 Worker 同时争用处理器时,高驻留、低 CPU 主要表示调度排队。

+
Worker 任务驻留、实际 CPU、协作让出与本地队列累计值,不在 GET 时重新计算任务样本。
{state.workers.map(worker =>
+
Worker {worker.id}驻留 {worker.utilization.toFixed(1)}% · CPU {worker.cpu_utilization.toFixed(1)}%
+
任务
{worker.task_count}
进入队列/峰值
{worker.active_task.time_ns ? worker.entry_queue_size : "--"}/{worker.peak_queue_size}
最长
{nanoseconds(worker.max_task_time_ns)}
活跃持续
{worker.active_task.time_ns ? nanoseconds(worker.active_task.time_ns) : "空闲"}
-
线程总 CPU
{nanoseconds(worker.cpu_time_ns)}
独占任务墙钟
{nanoseconds(worker.busy_time_ns)}
+
线程总 CPU
{nanoseconds(worker.cpu_time_ns)}
任务驻留墙钟
{nanoseconds(worker.busy_time_ns)}
协作让出
{worker.cooperative_wait_count.toLocaleString("zh-CN")} 次
让出墙钟
{nanoseconds(worker.cooperative_wait_time_ns)}
{worker.active_task.time_ns ? {worker.active_task.type} · {worker.active_task.native_id} : null}
)}
@@ -2561,27 +2698,20 @@ const Plot_Card = memo(function Plot_Card({plot, selected, policy, gallery, on_p const pixel_canvas_ref = useRef(null); const surface_ref = useRef(null); const card_ref = useRef(null); - const tile = gallery.layout?.plots[plot.id] ?? null; + const source = gallery.layout?.plots[plot.id] ?? null; const tile_width = gallery.layout?.tile_width ?? 720; const tile_height = gallery.layout?.tile_height ?? 420; const graph = use_plot_stream(plot, surface_ref, gallery.presentation, tile_width, tile_height); const status: Stream_Status = graph.status === "OFFLINE" || gallery.status === "OFFLINE" ? "OFFLINE" : graph.status === "LIVE" && gallery.status === "LIVE" ? "LIVE" : "CONNECTING"; const metrics = graph.metrics; + const backend_policy = graph.server_diagnostics?.frame_policy ?? null; const source_transport = gallery.transport?.sources[plot.id] ?? null; useEffect(() => { - if (!gallery.pixel_surface || !tile) return; - const draw = (event: Event) => { - const detail = (event as CustomEvent<{pixels: string; - surface: Gallery_Pixel_Surface}>).detail; - const canvas = pixel_canvas_ref.current; - if (detail?.pixels !== plot.pixels || detail.surface !== gallery.pixel_surface || !canvas) - return; - gallery.pixel_surface.draw_tile(canvas, tile, tile_width, tile_height); - }; - window.addEventListener("aethera:gallery-pixel-frame", draw); - return () => window.removeEventListener("aethera:gallery-pixel-frame", draw); - }, [gallery.pixel_surface, plot.pixels, tile?.column, tile?.row, tile_width, tile_height]); + const canvas = pixel_canvas_ref.current; + if (!gallery.pixel_group || !source || !canvas) return; + return gallery.pixel_group.register(plot.id, canvas); + }, [gallery.pixel_group, plot.id, source?.slot]); 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
{{IDLE: "已停止", CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}{metrics?.delivery === "diagnostics" ? "无像素传输" : metrics ? enum_label(metrics.pacing_mode) : "等待策略"}
- 真实画面 {source_transport ? source_transport.rendered_frame_rate_fps.toFixed(1) : metrics ? metrics.frame_rate_fps.toFixed(1) : "--.-"} FPS - 逻辑闭环 {source_transport ? source_transport.logical_completion_rate_fps.toFixed(1) : "--.-"} FPS + 后台完成 {backend_policy ? backend_policy.throughput.completion_rate_fps.toFixed(1) : source_transport ? source_transport.logical_completion_rate_fps.toFixed(1) : "--.-"} FPS + 后台延迟 {backend_policy ? backend_policy.latency.latest_completion_ms.toFixed(1) : "--.-"} ms + 画面产出 {source_transport ? source_transport.rendered_frame_rate_fps.toFixed(1) : metrics ? metrics.frame_rate_fps.toFixed(1) : "--.-"} FPS 采样 {gallery.transport ? gallery.transport.sampled_frame_rate_fps.toFixed(1) : "--.-"} FPS Canvas {gallery.presentation.frame_rate_fps.toFixed(1)} FPS - 完成 {metrics ? metrics.server_completion_ms.toFixed(1) : "--.-"} ms - Drogon WebSocket {gallery.transport ? gallery.transport.pixel_pack_average_ms.toFixed(1) : "--.-"} ms + 到达帧龄 {gallery.presentation.websocket_arrival_ms.toFixed(1)} ms + Canvas提交帧龄 {gallery.presentation.frame_latency_ms.toFixed(1)} ms + 像素封包 {gallery.transport ? gallery.transport.pixel_pack_average_ms.toFixed(1) : "--.-"} ms 画面落后 {source_transport?.has_rendered_frame ? source_transport.frame_lag : "--"} 帧
@@ -2770,7 +2902,7 @@ export function App() { return plots; }, [category, plots]); use_selected_plot_diagnostics(selected); - /* 页面筛选是像素资源的权威需求源,只订阅当前面板消费的 atlas。 */ + /* 页面筛选是像素资源的权威需求源,只订阅当前面板消费的 tile 批。 */ const gallery_pixels = use_gallery_pixels(visible); const [execution_policies, set_execution_policies] = useState({}); const [schema, set_schema] = useState(null);