diff --git a/.gitignore b/.gitignore index e55cf91..2c7a9bb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /webapp_gallery/node_modules/ /webapp_gallery/dist/ /third_party/datoviz/ +/output/ +/webapp_gallery/.playwright-cli/ diff --git a/Kernel/src/renderive/base/Concepts.hpp b/Kernel/src/renderive/base/Concepts.hpp index 7a5bf38..f4d8918 100644 --- a/Kernel/src/renderive/base/Concepts.hpp +++ b/Kernel/src/renderive/base/Concepts.hpp @@ -10,3 +10,23 @@ concept Mutex_Type = std::default_initializable && requires(That& mutex) { { mutex.lock() } -> std::same_as; { mutex.unlock() } -> std::same_as; }; +class Non_Copyable { +public: + Non_Copyable(const Non_Copyable&) = delete; + Non_Copyable& operator=(const Non_Copyable&) = delete; + Non_Copyable(Non_Copyable&&) = default; + Non_Copyable& operator=(Non_Copyable&&) = default; +protected: + Non_Copyable() = default; + ~Non_Copyable() = default; +}; +class Non_Movable { +public: + Non_Movable(const Non_Movable&) = default; + Non_Movable& operator=(const Non_Movable&) = default; + Non_Movable(Non_Movable&&) = delete; + Non_Movable& operator=(Non_Movable&&) = delete; +protected: + Non_Movable() = default; + ~Non_Movable() = default; +}; diff --git a/Kernel/src/renderive/state/Double_Buffer_Strategy.hpp b/Kernel/src/renderive/state/Double_Buffer_Strategy.hpp new file mode 100644 index 0000000..11fbd18 --- /dev/null +++ b/Kernel/src/renderive/state/Double_Buffer_Strategy.hpp @@ -0,0 +1,222 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "renderive/base/Atomic_Mutex.hpp" +#include "renderive/base/Concepts.hpp" +#include "renderive/base/observer/Observer.hpp" +#include "base/State_Strategy_Base.hpp" +template +struct Buffered_Data { + using Tag = Tag_Type; + using Data = Data_Type; +}; +template +concept Buffered_Data_Entry = requires { + typename Entry::Tag; + typename Entry::Data; +}; +template +struct Double_Buffer_Layout {}; +namespace double_buffer_detail { +template +struct Slot { + Data buffers[2]{}; + std::uint8_t render_index{}; + std::uint8_t cache_index{1}; + std::uint64_t cache_update_count{}; + std::uint64_t publish_count{}; + bool dirty{}; +}; +template +struct Unique_Tags; +template <> +struct Unique_Tags<> : std::true_type {}; +template +struct Unique_Tags : std::bool_constant<((!std::same_as) && ...) && Unique_Tags::value> {}; +template +struct Entry_Index { + static constexpr std::size_t match_count = (static_cast(std::same_as) + ...); + static_assert(match_count == 1); + static constexpr std::size_t value = []() consteval { + constexpr bool matches[] = {std::same_as...}; + for(std::size_t i = 0; i < sizeof...(Entries); ++i) { + if(matches[i]) { + return i; + } + } + return sizeof...(Entries); + }(); +}; +template +using Entry_Of = std::tuple_element_t::value, std::tuple>; +} +template > +struct Double_Buffer_Strategy : That, State_Strategy_Base { + using Self = Double_Buffer_Strategy; + using Data = Data_Type; + enum class Observation_Event { + cache_updated, + published + }; + struct Observation { + Observation_Event event{}; + std::uint64_t time_ns{}; + std::uint64_t cache_update_count{}; + std::uint64_t publish_count{}; + }; + static_assert(Timed_Struct_Observer); + Double_Buffer_Strategy() requires std::default_initializable && std::default_initializable : That() {} + explicit Double_Buffer_Strategy(With_Observer option) requires std::default_initializable && std::default_initializable : That(), observer(std::move(option.observer)) {} + template + explicit Double_Buffer_Strategy(std::in_place_t, Args&&... args) requires std::default_initializable : That(std::forward(args)...) {} + template + Double_Buffer_Strategy(std::in_place_t, With_Observer option, Args&&... args) requires std::default_initializable : That(std::forward(args)...), observer(std::move(option.observer)) {} + Self& write(Data value) { + std::optional observation; + { + std::lock_guard lock(mtx); + slot.buffers[slot.cache_index] = std::move(value); + slot.dirty = true; + ++slot.cache_update_count; + observation.emplace(Observation_Event::cache_updated, observer.now_ns(), slot.cache_update_count, slot.publish_count); + } + observer.observe(*observation); + return *this; + } + void publish() override { + std::optional observation; + { + std::lock_guard lock(mtx); + if(!slot.dirty) { + return; + } + std::swap(slot.render_index, slot.cache_index); + slot.dirty = false; + ++slot.publish_count; + observation.emplace(Observation_Event::published, observer.now_ns(), slot.cache_update_count, slot.publish_count); + } + observer.observe(*observation); + } + std::uint64_t state_revision() const override { + std::lock_guard lock(mtx); + return slot.publish_count; + } +protected: + const Data& render_buffer_value() const noexcept { + return slot.buffers[slot.render_index]; + } +private: + Observer observer; + double_buffer_detail::Slot slot; + mutable Mutex mtx; +}; +template > +struct Multi_Double_Buffer_Strategy; +template +struct Multi_Double_Buffer_Strategy, Mutex, Observer> : That, State_Strategy_Base { + using Self = Multi_Double_Buffer_Strategy; + enum class Observation_Event { + cache_updated, + published + }; + struct Observation { + Observation_Event event{}; + std::size_t buffer_index{}; + std::uint64_t time_ns{}; + std::uint64_t cache_update_count{}; + std::uint64_t publish_count{}; + }; + static_assert(sizeof...(Entries) > 0); + static_assert(double_buffer_detail::Unique_Tags::value); + static_assert((std::default_initializable && ...)); + static_assert(Timed_Struct_Observer); + Multi_Double_Buffer_Strategy() requires std::default_initializable : That() {} + explicit Multi_Double_Buffer_Strategy(With_Observer option) requires std::default_initializable : That(), observer(std::move(option.observer)) {} + template + explicit Multi_Double_Buffer_Strategy(std::in_place_t, Args&&... args) : That(std::forward(args)...) {} + template + Multi_Double_Buffer_Strategy(std::in_place_t, With_Observer option, Args&&... args) : That(std::forward(args)...), observer(std::move(option.observer)) {} + template + using Data = typename double_buffer_detail::Entry_Of::Data; + template + static consteval std::size_t buffer_index() { + return double_buffer_detail::Entry_Index::value; + } + template + Self& write(Data value) { + std::optional observation; + { + std::lock_guard lock(mtx); + auto& current = slot(); + current.buffers[current.cache_index] = std::move(value); + current.dirty = true; + ++current.cache_update_count; + observation.emplace(Observation_Event::cache_updated, buffer_index(), observer.now_ns(), current.cache_update_count, current.publish_count); + } + observer.observe(*observation); + return *this; + } + void publish() override { + std::array observations{}; + std::size_t observation_count{}; + { + std::lock_guard lock(mtx); + bool published{}; + (publish_entry(observations, observation_count, published), ...); + if(published) { + ++revision; + } + } + for(std::size_t i = 0; i < observation_count; ++i) { + observer.observe(observations[i]); + } + } + std::uint64_t state_revision() const override { + std::lock_guard lock(mtx); + return revision; + } + template + std::uint64_t buffer_revision() const { + std::lock_guard lock(mtx); + return slot().publish_count; + } +protected: + template + const Data& render_buffer_value() const noexcept { + const auto& current = slot(); + return current.buffers[current.render_index]; + } +private: + template + auto& slot() noexcept { + return std::get()>(slots); + } + template + const auto& slot() const noexcept { + return std::get()>(slots); + } + template + void publish_entry(std::array& observations, std::size_t& observation_count, bool& published) { + using Tag = typename Entry::Tag; + auto& current = slot(); + if(!current.dirty) { + return; + } + std::swap(current.render_index, current.cache_index); + current.dirty = false; + ++current.publish_count; + observations[observation_count++] = Observation{Observation_Event::published, buffer_index(), observer.now_ns(), current.cache_update_count, current.publish_count}; + published = true; + } + Observer observer; + std::tuple...> slots; + std::uint64_t revision{}; + mutable Mutex mtx; +}; diff --git a/render_3D/CMakeLists.txt b/render_3D/CMakeLists.txt index d71d456..7b8db82 100644 --- a/render_3D/CMakeLists.txt +++ b/render_3D/CMakeLists.txt @@ -40,6 +40,45 @@ find_package(tinyxml2 CONFIG REQUIRED) find_package(tinyobjloader CONFIG REQUIRED) find_package(ZLIB REQUIRED) message(STATUS "${B}Vulkan_VERSION=${Vulkan_VERSION}${E}") + +# Datoviz loads shaderc lazily when the first GLSL frame plan is executed. A process can +# therefore start successfully and fail only on its first 3D frame when the Vulkan SDK's Bin +# directory was not inherited by the launcher (for example, a fresh CLion run configuration). +# Record that runtime dependency once and stage it beside every executable that embeds render_3D. +if (WIN32) + get_filename_component(Renderive_render_3D_vulkan_bin + "${Vulkan_GLSLC_EXECUTABLE}" DIRECTORY) + find_file(Renderive_render_3D_shaderc_runtime + NAMES shaderc_shared.dll + HINTS + "${Renderive_render_3D_vulkan_bin}" + "$ENV{VULKAN_SDK}/Bin" + NO_DEFAULT_PATH + ) + if (NOT Renderive_render_3D_shaderc_runtime) + message(FATAL_ERROR + "render_3D requires shaderc_shared.dll beside the Vulkan SDK tools") + endif () + set_property(GLOBAL PROPERTY RENDERIVE_RENDER_3D_RUNTIME_FILES + "${Renderive_render_3D_shaderc_runtime}") +endif () + +function(renderive_stage_render_3D_runtime target) + if (NOT TARGET "${target}") + message(FATAL_ERROR "Cannot stage render_3D runtime for missing target: ${target}") + endif () + if (WIN32) + get_property(Renderive_render_3D_runtime_files GLOBAL PROPERTY + RENDERIVE_RENDER_3D_RUNTIME_FILES) + add_custom_command(TARGET "${target}" POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + ${Renderive_render_3D_runtime_files} + "$" + COMMENT "Staging Datoviz runtime dependencies for ${target}" + VERBATIM) + endif () +endfunction() + if (RENDERIVE_BUILD_TESTS) find_package(GTest CONFIG REQUIRED) endif () @@ -57,8 +96,14 @@ target_include_directories(Renderive_render_3D PUBLIC "$" ) target_compile_features(Renderive_render_3D PUBLIC cxx_std_20) -target_link_libraries(Renderive_render_3D PRIVATE ${Renderive_render_3D_datoviz_targets} tinyobjloader::tinyobjloader) +target_link_libraries(Renderive_render_3D + PUBLIC Renderive_Kernel + PRIVATE ${Renderive_render_3D_datoviz_targets} tinyobjloader::tinyobjloader +) if (MSVC) + # Datoviz is embedded from OBJECT libraries in this static archive. Its Windows headers + # otherwise declare every symbol as dllimport for this C++ translation unit. + target_compile_definitions(Renderive_render_3D PRIVATE DVZ_SHARED) target_compile_options(Renderive_render_3D PRIVATE /utf-8) endif () if (RENDERIVE_BUILD_TESTS) @@ -76,6 +121,7 @@ if (RENDERIVE_BUILD_TESTS) set(Renderive_render_3D_test_target "Renderive_render_3D_${Renderive_render_3D_test_name}_${Renderive_render_3D_test_hash}") add_executable("${Renderive_render_3D_test_target}" "${Renderive_render_3D_test_source}") target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE Renderive_render_3D GTest::gtest_main) + renderive_stage_render_3D_runtime("${Renderive_render_3D_test_target}") add_test(NAME "${Renderive_render_3D_test_target}" COMMAND "${Renderive_render_3D_test_target}") endforeach () endif () diff --git a/render_3D/DATOVIZ_MIGRATION_AUDIT.md b/render_3D/DATOVIZ_MIGRATION_AUDIT.md new file mode 100644 index 0000000..78f6106 --- /dev/null +++ b/render_3D/DATOVIZ_MIGRATION_AUDIT.md @@ -0,0 +1,78 @@ +# Datoviz 迁移与封装审计 + +## 目标边界 + +Renderive 只负责逻辑状态收集、批量数据收集、Kernel 帧策略调度、输入事件汇聚和像素帧发布。Datoviz 的 scene/visual API 只允许在 `Render_Domain` 所属线程创建、修改、渲染和销毁,对外接口不暴露 `Dvz*`、Vulkan 对象、缓冲区角色或工作线程状态。 + +当前垂直切片的数据流为: + +```text +Point_State --Kernel Double_State_Strategy--+ + +--帧快照--> Kernel Frame Strategy +Point_Data --Kernel Latest_Real_Time_Data----+ | +Kernel Event --Input_Collector---------------+ v + Render_Domain 单线程 + | + Datoviz Point Visual + | + Vulkan 外部目标 + RGBA8 回读 + | + immutable Pixel_Frame + | + WebSocket RVP1 像素帧 +``` + +## 已迁移的 Datoviz 生产模块 + +`render_3D/datoviz/CMakeLists.txt` 已直接编译以下模块,现有 Point 后端不再需要复制 Datoviz 实现: + +- 基础层:`common`、`fileio`、`geom`、`math`、`thread`。 +- 输入与控制器:`input`、`controller`。 +- GPU:`vk`、`vklite`、`drp2`。 +- 场景层:`scene` 以及 `scene/visuals` 下全部生产源、GLSL/WGSL 注册表和 SPIR-V 构建产物。 +- Visual family:point、pixel、marker、segment、path、image、mesh、volume、primitive、sphere、glyph、text、labels、splat、vector。 + +`registry` 和 `stroke` 是 visual 内部支撑目录,不是独立业务组件,不应再增加一层公共包装。 + +明确不迁移 `app`、`gui`、原生 window/canvas、stream、video、Qt bridge 和 wasm。无窗口像素服务由 Renderive 自己的帧策略、事件入口和 Web 传输承担,这些上游模块没有消费者,迁入会形成第二套调度和事件来源。 + +## 已完成的 Point 封装 + +- `Point_Visual`:PImpl;`Point_State` 只保存样式、变换、可见性和深度测试。 +- `Point_Data`:直接使用 Kernel `Latest_Real_Time_Data>`,不在 visual 中复制大数据。 +- `Point_Scene`:直接组合 Kernel Manual、Low Latency、Playback 三种策略;统一发布 `Frame_Status` 查询快照。 +- `Datoviz_Point_Backend`:所有 Datoviz/Vulkan 资源均限制在单一 `Render_Domain`;批量属性使用 `dvz_visual_set_data_many()` 原子提交。 +- 输入:Kernel pointer/wheel/key 事件映射到 Datoviz router/arcball;Web 保留事件的真实派生类型,不经过 `Event` 切片。 +- 输出:外部 RGBA8 target 渲染、同步回读并发布不可变 `Pixel_Frame`。 +- Demo/Web:`Point_Demo`、图库案例 `point_3d`、RVP1 RGBA 编码、三种帧模式动作和前端目录展示。 + +## 后续 visual 包装批次 + +Datoviz 生产源码和上游测试已经迁入;以下是尚未实现的 Renderive 业务包装,不应在没有业务消费者时一次性生成空类: + +1. 点/线批量族:Pixel、Marker、Segment、Path、Vector、Sphere、Splat。复用 Point 的状态/实时数据/单线程后端结构,但每个 family 保留自己的业务状态和 Datoviz 属性映射。 +2. 几何族:Primitive、Mesh。分别建模顶点/索引/实例数据;索引和实例不可复制成 visual 成员的第二份权威数据。 +3. 图像与文字族:Image、Glyph、Labels、Text。纹理/字形数据使用 real-time data 或具有明确所有权的资源对象,不能把 Datoviz field 指针暴露到公共接口。 +4. 体数据:Volume。体素 payload、传输函数和采样状态分离;大体数据必须走实时数据接口,不能进入双缓冲小状态对象。 + +每新增一个 family 都必须同时完成:状态边界测试、并发批量数据测试、Datoviz 属性映射测试、真实 GPU 像素测试、resize 测试、三种帧策略测试、事件测试,以及 Web 展示测试。Point 的实现是这些测试的基准,不是供公共 API 继承的万能基类。 + +## 测试覆盖 + +- Datoviz scene runner:558/558,通过;其中已包含 Point 的 typed upload、属性校验、item range、resize、external buffer、large count、GLSL/WGSL emit 和 GPU 执行测试。 +- Renderive Point 状态/Kernel 策略测试:4/4,通过。 +- Renderive Point Vulkan/事件集成测试:2/2,通过。 +- Web Point 专项测试:8/8,通过,覆盖 RGBA 行步长、目录动作、resize、自动低延迟、Web 事件透传、Manual 快照、Playback 顺序和增删恢复。 +- `webapp_gallery`:TypeScript 与 Vite production build 通过。 + +## 状态来源检查 + +| 状态 | 唯一权威来源 | 读取方式 | +| --- | --- | --- | +| Point 样式/变换/可见性 | `Point_Visual` 的 Kernel 双状态策略 | 帧边界发布快照 | +| Point 大批量数据 | `Point_Data` | revision + snapshot | +| viewport/clear color | `Scene_State_Buffer` | 帧边界发布快照 | +| 输入事件 | `Input_Collector` 队列 | 每帧 drain | +| 帧生命周期/计数 | Kernel frame strategy | `Frame_Status` 即时查询 | +| Datoviz/Vulkan 资源 | `Datoviz_Point_Backend` | 仅 Render_Domain 内访问 | +| 对外像素 | `Point_Scene::latest_frame()` | 不可变共享快照 | diff --git a/render_3D/cmake/computer_graphics.cmake b/render_3D/cmake/computer_graphics.cmake index 0bd222e..57df644 100644 --- a/render_3D/cmake/computer_graphics.cmake +++ b/render_3D/cmake/computer_graphics.cmake @@ -201,6 +201,7 @@ block() ) rcl_get_effective_install_dir(render_3D::Vulkan-Hpp VULKAN_HPP_ROOT) rcl_cmake_library_set_cmake_options(render_3D::Vulkan-Hpp ${Vulkan-Hpp_option}) + set(ENV{VULKAN_SDK} "C:/VulkanSDK/1.4.357.0") find_package(Vulkan REQUIRED) get_filename_component(VULKAN_SDK_ROOT "${Vulkan_INCLUDE_DIR}" DIRECTORY) set(volk_option ${base_options}) diff --git a/render_3D/render_3D/Point_Demo.cpp b/render_3D/render_3D/Point_Demo.cpp new file mode 100644 index 0000000..bdac3dc --- /dev/null +++ b/render_3D/render_3D/Point_Demo.cpp @@ -0,0 +1,31 @@ +#include "Point_Demo.h" + +#include +#include +#include + +namespace renderive::render_3d { + +std::vector point_demo_data(float phase_radians) { + const float orbit_x = 0.72F * std::cos(phase_radians); + const float orbit_y = 0.44F * std::sin(phase_radians); + return { + {{-0.72F, -0.38F, 0.15F}, {245, 70, 78, 255}, 42.0F}, + {{0.00F, 0.48F, -0.05F}, {65, 220, 132, 255}, 48.0F}, + {{orbit_x, orbit_y, 0.30F}, {75, 135, 255, 255}, 54.0F}, + {{-0.35F, 0.10F, -0.45F}, {250, 205, 75, 255}, 30.0F}, + {{0.38F, -0.12F, -0.25F}, {205, 95, 245, 255}, 34.0F}, + }; +} + +Point_Demo make_point_demo(Scene_Options options) { + auto data = std::make_shared(); + data->update(point_demo_data()); + Point_State state; + state.style = {{12, 16, 24, 255}, 2.0F, Point_Aspect::Outline}; + auto visual = std::make_shared(data, state); + auto scene = std::make_unique(options, std::move(visual)); + return {std::move(data), std::move(scene)}; +} + +} // namespace renderive::render_3d diff --git a/render_3D/render_3D/Point_Demo.h b/render_3D/render_3D/Point_Demo.h new file mode 100644 index 0000000..a16a495 --- /dev/null +++ b/render_3D/render_3D/Point_Demo.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Point_Scene.h" + +#include +#include + +namespace renderive::render_3d { + +struct Point_Demo { + std::shared_ptr points; + std::unique_ptr scene; +}; + +[[nodiscard]] std::vector point_demo_data(float phase_radians = 0.0F); +[[nodiscard]] Point_Demo make_point_demo(Scene_Options options = {}); + +} // namespace renderive::render_3d diff --git a/render_3D/render_3D/Point_Scene.cpp b/render_3D/render_3D/Point_Scene.cpp new file mode 100644 index 0000000..e325b46 --- /dev/null +++ b/render_3D/render_3D/Point_Scene.cpp @@ -0,0 +1,210 @@ +#include "Point_Scene.h" + +#include "detail/Datoviz_Point_Backend.h" +#include "detail/Point_Core.h" +#include "detail/Render_Domain.h" + +#include +#include +#include +#include +#include +#include + +namespace renderive::render_3d { +namespace { + +void validate(Extent extent) { + if (extent.empty()) + throw std::invalid_argument("Point_Scene viewport must be nonempty"); +} + +void validate(Clear_Color color) { + const auto component = [](float value) { + return std::isfinite(value) && value >= 0.0F && value <= 1.0F; + }; + if (!component(color.red) || !component(color.green) || + !component(color.blue) || !component(color.alpha)) + throw std::invalid_argument("Point_Scene clear color must be in [0, 1]"); +} + +detail::Input_Command_Type pointer_type(::renderive::Event_Type type) { + switch (type) { + case ::renderive::Event_Type::Pointer_Move: + return detail::Input_Command_Type::Pointer_Move; + case ::renderive::Event_Type::Pointer_Press: + return detail::Input_Command_Type::Pointer_Press; + case ::renderive::Event_Type::Pointer_Release: + return detail::Input_Command_Type::Pointer_Release; + default: + throw std::invalid_argument("Point_Scene expected a pointer event"); + } +} + +float datoviz_wheel_step(float pixel_delta, float angle_delta) { + if (angle_delta != 0.0F) + return angle_delta / 120.0F; + return pixel_delta / 100.0F; +} + +} // namespace + +struct Point_Scene::Impl { + Impl(const Scene_Options& options, std::shared_ptr point_visual) + : visual(std::move(point_visual)), + states(detail::Scene_State{options.viewport, options.clear_color}), + scheduler(options.frame_mode, options.maximum_frames_per_second) { + if (!visual) + throw std::invalid_argument("Point_Scene requires a Point_Visual"); + render_domain.invoke([&] { + backend = std::make_unique( + options.gpu_index, options.validation_enabled, + detail::Scene_State{options.viewport, options.clear_color}); + }); + } + + ~Impl() { + accepting.store(false, std::memory_order_release); + render_domain.invoke([&] { backend.reset(); }); + } + + [[nodiscard]] bool render() { + if (!accepting.load(std::memory_order_acquire)) + return false; + return render_domain.invoke([&] { + return scheduler.render([&](const detail::Point_Frame_Data& frame) { + auto rendered = backend->render(frame); + if (!rendered) + return false; + std::lock_guard lock(frame_mutex); + latest = std::move(rendered); + return true; + }); + }); + } + + std::shared_ptr visual; + detail::Scene_State_Buffer states; + detail::Input_Collector input; + detail::Frame_Scheduler scheduler; + detail::Render_Domain render_domain; + std::unique_ptr backend; + mutable std::mutex frame_mutex; + std::shared_ptr latest; + std::atomic accepting{true}; +}; + +Point_Scene::Point_Scene(Scene_Options options, + std::shared_ptr visual) { + validate(options.viewport); + validate(options.clear_color); + if (!std::isfinite(options.maximum_frames_per_second) || + options.maximum_frames_per_second <= 0.0) + throw std::invalid_argument("Point_Scene frame frequency must be positive"); + impl_ = std::make_unique(options, std::move(visual)); +} + +Point_Scene::~Point_Scene() = default; + +void Point_Scene::resize(Extent extent) { + validate(extent); + impl_->states.update([extent](detail::Scene_State& state) { + state.viewport = extent; + }); +} + +void Point_Scene::set_clear_color(Clear_Color color) { + validate(color); + impl_->states.update([color](detail::Scene_State& state) { + state.clear_color = color; + }); +} + +void Point_Scene::dispatch(const ::renderive::Event&) { + // Non-positional Show/Hide/Leave events carry no Datoviz controller payload. +} + +void Point_Scene::dispatch_pointer( + ::renderive::Event_Type type, float x, float y, + ::renderive::Mouse_Button button, ::renderive::Mouse_Button_Mask buttons, + ::renderive::Keyboard_Modifier modifiers) { + if (!std::isfinite(x) || !std::isfinite(y)) + return; + detail::Input_Command command; + command.type = pointer_type(type); + command.x = x; + command.y = y; + command.button = button; + command.buttons = buttons; + command.modifiers = modifiers; + impl_->input.push(command); +} + +void Point_Scene::dispatch_wheel( + float x, float y, float pixel_delta_x, float pixel_delta_y, + float angle_delta_x, float angle_delta_y, + ::renderive::Keyboard_Modifier modifiers) { + if (!std::isfinite(x) || !std::isfinite(y) || + !std::isfinite(pixel_delta_x) || !std::isfinite(pixel_delta_y) || + !std::isfinite(angle_delta_x) || !std::isfinite(angle_delta_y)) + return; + detail::Input_Command command; + command.type = detail::Input_Command_Type::Wheel; + command.x = x; + command.y = y; + command.delta_x = datoviz_wheel_step(pixel_delta_x, angle_delta_x); + command.delta_y = datoviz_wheel_step(pixel_delta_y, angle_delta_y); + command.modifiers = modifiers; + impl_->input.push(command); +} + +void Point_Scene::dispatch(const ::renderive::Key_Event& event) { + detail::Input_Command command; + command.type = event.type == ::renderive::Event_Type::Key_Release + ? detail::Input_Command_Type::Key_Release + : event.auto_repeat + ? detail::Input_Command_Type::Key_Repeat + : detail::Input_Command_Type::Key_Press; + command.key = event.key; + command.native_key = event.native_key; + command.modifiers = event.modifiers; + impl_->input.push(command); +} + +bool Point_Scene::prepare_frame() { + return impl_->accepting.load(std::memory_order_acquire) && + impl_->scheduler.prepare(impl_->states, *impl_->visual, impl_->input); +} + +bool Point_Scene::refresh_manual_frame() { + return impl_->accepting.load(std::memory_order_acquire) && + impl_->scheduler.refresh(); +} + +bool Point_Scene::discard_pending_frame() { + return impl_->accepting.load(std::memory_order_acquire) && + impl_->scheduler.discard_pending(); +} + +bool Point_Scene::render_prepared_frame() { return impl_->render(); } + +bool Point_Scene::request_frame() { + return prepare_frame() && impl_->scheduler.activate_for_request() && + render_prepared_frame(); +} + +std::shared_ptr Point_Scene::latest_frame() const { + std::lock_guard lock(impl_->frame_mutex); + return impl_->latest; +} + +Frame_Status Point_Scene::frame_status() const { + auto result = impl_->scheduler.status(); + std::lock_guard lock(impl_->frame_mutex); + if (impl_->latest) + result.latest_sequence = std::max(result.latest_sequence, + impl_->latest->sequence); + return result; +} + +} // namespace renderive::render_3d diff --git a/render_3D/render_3D/Point_Scene.h b/render_3D/render_3D/Point_Scene.h new file mode 100644 index 0000000..8c3b904 --- /dev/null +++ b/render_3D/render_3D/Point_Scene.h @@ -0,0 +1,120 @@ +#pragma once + +#include "Point_Visual.h" + +#include + +#include +#include +#include +#include + +namespace renderive::render_3d { + +struct Extent { + std::uint32_t width{}; + std::uint32_t height{}; + [[nodiscard]] constexpr bool empty() const noexcept { return width == 0 || height == 0; } + bool operator==(const Extent&) const = default; +}; + +struct Clear_Color { + float red{0.035F}; + float green{0.045F}; + float blue{0.07F}; + float alpha{1.0F}; + bool operator==(const Clear_Color&) const = default; +}; + +enum class Frame_Mode : std::uint8_t { + Manual, + Low_Latency, + Playback, +}; + +struct Scene_Options { + Extent viewport{560, 320}; + Clear_Color clear_color; + Frame_Mode frame_mode{Frame_Mode::Low_Latency}; + double maximum_frames_per_second{60.0}; + std::uint32_t gpu_index{}; + bool validation_enabled{}; +}; + +struct Pixel_Frame { + Extent extent; + std::uint64_t sequence{}; + std::vector rgba8; +}; + +struct Frame_Status { + Frame_Mode mode{Frame_Mode::Low_Latency}; + double frequency_hz{}; + std::uint64_t produced_frame_count{}; + std::uint64_t consumed_frame_count{}; + std::uint64_t dropped_frame_count{}; + std::uint64_t failed_operation_count{}; + std::uint64_t pending_frame_count{}; + std::uint64_t latest_sequence{}; + std::uint64_t next_refresh_interval_ns{}; +}; + +// Owns frame scheduling and the single Datoviz mutation/render domain. +class Point_Scene final { +public: + Point_Scene(Scene_Options options, std::shared_ptr visual); + ~Point_Scene(); + + Point_Scene(const Point_Scene&) = delete; + Point_Scene& operator=(const Point_Scene&) = delete; + Point_Scene(Point_Scene&&) = delete; + Point_Scene& operator=(Point_Scene&&) = delete; + + void resize(Extent extent); + void set_clear_color(Clear_Color color); + void dispatch(const ::renderive::Event& event); + + template <::renderive::Event_Point Point_Type> + void dispatch(const ::renderive::Basic_Pointer_Event& event) { + dispatch_pointer(event.type, static_cast(event.position.x), + static_cast(event.position.y), event.button, + event.buttons, event.modifiers); + } + + template <::renderive::Event_Point Point_Type> + void dispatch(const ::renderive::Basic_Wheel_Event& event) { + dispatch_wheel(static_cast(event.position.x), + static_cast(event.position.y), + static_cast(event.pixel_delta_x), + static_cast(event.pixel_delta_y), + static_cast(event.angle_delta_x), + static_cast(event.angle_delta_y), event.modifiers); + } + + void dispatch(const ::renderive::Key_Event& event); + + // Frame strategy operations. request_frame() is the complete low-latency path. + [[nodiscard]] bool prepare_frame(); + [[nodiscard]] bool refresh_manual_frame(); + [[nodiscard]] bool discard_pending_frame(); + [[nodiscard]] bool render_prepared_frame(); + [[nodiscard]] bool request_frame(); + + [[nodiscard]] std::shared_ptr latest_frame() const; + [[nodiscard]] Frame_Status frame_status() const; + +private: + void dispatch_pointer(::renderive::Event_Type type, float x, float y, + ::renderive::Mouse_Button button, + ::renderive::Mouse_Button_Mask buttons, + ::renderive::Keyboard_Modifier modifiers); + void dispatch_wheel(float x, float y, float pixel_delta_x, + float pixel_delta_y, float angle_delta_x, + float angle_delta_y, + ::renderive::Keyboard_Modifier modifiers); + + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace renderive::render_3d diff --git a/render_3D/render_3D/Point_Visual.cpp b/render_3D/render_3D/Point_Visual.cpp new file mode 100644 index 0000000..6b0f551 --- /dev/null +++ b/render_3D/render_3D/Point_Visual.cpp @@ -0,0 +1,79 @@ +#include "Point_Visual.h" + +#include "detail/Point_Core.h" + +#include + +#include +#include +#include +#include + +namespace renderive::render_3d { +namespace { + +void validate(const Point_State& state) { + if (!std::isfinite(state.style.stroke_width_px) || + state.style.stroke_width_px < 0.0F) + throw std::invalid_argument("point stroke width must be finite and nonnegative"); + for (float value : state.transform.values) { + if (!std::isfinite(value)) + throw std::invalid_argument("point transform must contain finite values"); + } +} + +void validate(const std::vector& points) { + for (const auto& point : points) { + if (!std::isfinite(point.position.x) || !std::isfinite(point.position.y) || + !std::isfinite(point.position.z) || !std::isfinite(point.diameter_px) || + point.diameter_px <= 0.0F) + throw std::invalid_argument("point payload contains invalid coordinates or diameter"); + } +} + +struct Point_State_Base {}; + +} // namespace + +struct Point_Visual::Impl final + : ::Double_State_Strategy { + using Base = ::Double_State_Strategy; + + Impl(std::shared_ptr source, const Point_State& initial) + : Base(initial), data(std::move(source)) {} + + [[nodiscard]] detail::Published_Point publish_snapshot() { + std::lock_guard lock(publication_mutex); + Base::publish(); + const std::uint64_t data_revision = data->revision(); + auto payload = data->snapshot().value_or(std::vector{}); + validate(payload); + return {Base::render_state_value(), std::move(payload), + Base::state_revision(), data_revision}; + } + + std::shared_ptr data; + std::mutex publication_mutex; +}; + +Point_Visual::Point_Visual(std::shared_ptr data, Point_State initial) { + if (!data) + throw std::invalid_argument("Point_Visual requires a Point_Data source"); + validate(initial); + impl_ = std::make_unique(std::move(data), initial); +} + +Point_Visual::~Point_Visual() = default; + +void Point_Visual::configure(Point_State state) { + validate(state); + impl_->update([state = std::move(state)](Point_State& target) mutable { + target = std::move(state); + }); +} + +detail::Published_Point detail::Point_State_Access::publish(Point_Visual& visual) { + return visual.impl_->publish_snapshot(); +} + +} // namespace renderive::render_3d diff --git a/render_3D/render_3D/Point_Visual.h b/render_3D/render_3D/Point_Visual.h new file mode 100644 index 0000000..75d2f48 --- /dev/null +++ b/render_3D/render_3D/Point_Visual.h @@ -0,0 +1,71 @@ +#pragma once +#include +#include +#include +#include +#include +namespace renderive::render_3d { +struct Vec3 { + float x{}; + float y{}; + float z{}; + bool operator==(const Vec3&) const = default; +}; +struct Rgba8 { + std::uint8_t red{255}; + std::uint8_t green{255}; + std::uint8_t blue{255}; + std::uint8_t alpha{255}; + bool operator==(const Rgba8&) const = default; +}; +struct Matrix4 { + std::array values{ + 1.0F, 0.0F, 0.0F, 0.0F, + 0.0F, 1.0F, 0.0F, 0.0F, + 0.0F, 0.0F, 1.0F, 0.0F, + 0.0F, 0.0F, 0.0F, 1.0F + }; + bool operator==(const Matrix4&) const = default; +}; +struct Point { + Vec3 position; + Rgba8 color; + float diameter_px{8.0F}; + bool operator==(const Point&) const = default; +}; +enum class Point_Aspect : std::uint8_t { + Filled, + Stroke, + Outline, +}; +struct Point_Style { + Rgba8 edge_color{0, 0, 0, 255}; + float stroke_width_px{}; + Point_Aspect aspect{Point_Aspect::Filled}; + bool operator==(const Point_Style&) const = default; +}; +struct Point_State { + Point_Style style; + Matrix4 transform; + bool visible{true}; + bool depth_test{true}; + bool operator==(const Point_State&) const = default; +}; +// Bulk point payloads use Kernel's latest-value real-time data channel directly. +using Point_Data = ::Latest_Real_Time_Data>; +namespace detail { +class Point_State_Access; +} +// Thread-safe logical point visual. Datoviz resources deliberately do not live here. +class Point_Visual final : Non_Copyable { +public: + explicit Point_Visual(std::shared_ptr data, Point_State initial = {}); + ~Point_Visual(); + // Atomically replace the visual properties. Point payloads are updated on Point_Data. + void configure(Point_State state); +private: + friend class detail::Point_State_Access; + struct Impl; + std::unique_ptr impl_; +}; +} // namespace renderive::render_3d diff --git a/render_3D/render_3D/detail/Datoviz_Point_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Point_Backend.cpp new file mode 100644 index 0000000..e0b779e --- /dev/null +++ b/render_3D/render_3D/detail/Datoviz_Point_Backend.cpp @@ -0,0 +1,620 @@ +#include "Datoviz_Point_Backend.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::render_3d::detail { +namespace { + +constexpr std::uint64_t color_target_id = 0x5256504f494e54ULL; + +template +Resource* allocate_wrapper(Allocate allocate, const char* message) { + Resource* resource = allocate(); + if (resource == nullptr) + throw std::runtime_error(message); + return resource; +} + +int modifiers(::renderive::Keyboard_Modifier value) { + const auto bits = static_cast(value); + int result = DVZ_KEY_MODIFIER_NONE; + if ((bits & static_cast(::renderive::Keyboard_Modifier::Shift)) != 0) + result |= DVZ_KEY_MODIFIER_SHIFT; + if ((bits & static_cast(::renderive::Keyboard_Modifier::Ctrl)) != 0) + result |= DVZ_KEY_MODIFIER_CONTROL; + if ((bits & static_cast(::renderive::Keyboard_Modifier::Alt)) != 0) + result |= DVZ_KEY_MODIFIER_ALT; + if ((bits & static_cast(::renderive::Keyboard_Modifier::Meta)) != 0) + result |= DVZ_KEY_MODIFIER_SUPER; + return result; +} + +DvzPointerButton button(::renderive::Mouse_Button value) { + switch (value) { + case ::renderive::Mouse_Button::Left: + return DVZ_POINTER_BUTTON_LEFT; + case ::renderive::Mouse_Button::Middle: + return DVZ_POINTER_BUTTON_MIDDLE; + case ::renderive::Mouse_Button::Right: + return DVZ_POINTER_BUTTON_RIGHT; + case ::renderive::Mouse_Button::None: + return DVZ_POINTER_BUTTON_NONE; + } + return DVZ_POINTER_BUTTON_NONE; +} + +DvzKeyCode key_code(::renderive::Key key, std::uint32_t native_key) { + switch (key) { + case ::renderive::Key::Escape: + return DVZ_KEY_ESCAPE; + case ::renderive::Key::Enter: + return DVZ_KEY_ENTER; + case ::renderive::Key::Space: + return DVZ_KEY_SPACE; + case ::renderive::Key::Delete: + return DVZ_KEY_DELETE; + case ::renderive::Key::Backspace: + return DVZ_KEY_BACKSPACE; + case ::renderive::Key::Left: + return DVZ_KEY_LEFT; + case ::renderive::Key::Right: + return DVZ_KEY_RIGHT; + case ::renderive::Key::Up: + return DVZ_KEY_UP; + case ::renderive::Key::Down: + return DVZ_KEY_DOWN; + case ::renderive::Key::Unknown: + break; + } + return native_key <= static_cast(DVZ_KEY_LAST) + ? static_cast(native_key) + : DVZ_KEY_UNKNOWN; +} + +DvzShapeAspect aspect(Point_Aspect value) { + switch (value) { + case Point_Aspect::Filled: + return DVZ_SHAPE_ASPECT_FILLED; + case Point_Aspect::Stroke: + return DVZ_SHAPE_ASPECT_STROKE; + case Point_Aspect::Outline: + return DVZ_SHAPE_ASPECT_OUTLINE; + } + return DVZ_SHAPE_ASPECT_FILLED; +} + +} // namespace + +class Datoviz_Point_Backend::Frame_Target final { +public: + Frame_Target(DvzGpuCtx* gpu_context, Extent extent, std::uint64_t generation) + : gpu_context_(gpu_context), extent_(extent), generation_(generation) { + if (gpu_context == nullptr || extent.empty()) + throw std::invalid_argument("invalid Datoviz point frame target"); + const std::uint64_t byte_size = static_cast(extent.width) * + extent.height * 4ULL; + if (byte_size > std::numeric_limits::max()) + throw std::length_error("Datoviz point frame target is too large"); + byte_size_ = static_cast(byte_size); + + DvzDevice* device = dvz_gpu_ctx_device(gpu_context_); + DvzVma* allocator = dvz_gpu_ctx_alloc(gpu_context_); + DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN); + if (device == nullptr || allocator == nullptr || queue == nullptr) + throw std::runtime_error("Datoviz GPU context is incomplete"); + try { + image_ = allocate_wrapper(dvz_images_create_wrapper, + "failed to allocate Datoviz image"); + dvz_images(device, allocator, VK_IMAGE_TYPE_2D, 1, image_); + dvz_images_format(image_, VK_FORMAT_R8G8B8A8_UNORM); + dvz_images_size(image_, extent.width, extent.height, 1); + dvz_images_tiling(image_, VK_IMAGE_TILING_OPTIMAL); + dvz_images_usage(image_, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + dvz_images_alloc_flags(image_, DVZ_ALLOC_FLAGS_NONE); + if (dvz_images_create(image_) != 0) + throw std::runtime_error("failed to create Datoviz point image"); + + view_ = allocate_wrapper(dvz_image_views_create_wrapper, + "failed to allocate Datoviz image view"); + dvz_image_views(image_, view_); + dvz_image_views_type(view_, VK_IMAGE_VIEW_TYPE_2D); + dvz_image_views_aspect(view_, VK_IMAGE_ASPECT_COLOR_BIT); + dvz_image_views_mip(view_, 0, 1); + dvz_image_views_layers(view_, 0, 1); + if (dvz_image_views_create(view_) != 0) + throw std::runtime_error("failed to create Datoviz point image view"); + + commands_ = allocate_wrapper(dvz_commands_create_wrapper, + "failed to allocate Datoviz commands"); + dvz_commands(device, queue, 1, commands_); + if (dvz_commands_handle(commands_) == VK_NULL_HANDLE) + throw std::runtime_error("failed to create Datoviz command buffer"); + + fence_ = allocate_wrapper(dvz_fence_create_wrapper, + "failed to allocate Datoviz fence"); + dvz_fence(device, true, fence_); + if (dvz_fence_handle(fence_) == VK_NULL_HANDLE) + throw std::runtime_error("failed to create Datoviz fence"); + submit_ = allocate_wrapper(dvz_submit_create_wrapper, + "failed to allocate Datoviz submit"); + + readback_ = allocate_wrapper(dvz_buffer_create_wrapper, + "failed to allocate Datoviz readback"); + dvz_buffer(device, allocator, readback_); + dvz_buffer_size(readback_, byte_size_); + dvz_buffer_flags(readback_, DVZ_ALLOC_HOST_ACCESS_RANDOM | DVZ_ALLOC_MAPPED); + dvz_buffer_usage(readback_, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + if (dvz_buffer_create(readback_) != 0) + throw std::runtime_error("failed to create Datoviz readback buffer"); + } catch (...) { + destroy(); + throw; + } + } + + ~Frame_Target() { destroy(); } + + void begin() { + if (!dvz_fence_wait(fence_)) + throw std::runtime_error("failed to wait for Datoviz frame fence"); + dvz_cmd_reset(commands_); + if (dvz_cmd_begin_result(commands_) != 0) + throw std::runtime_error("failed to begin Datoviz command buffer"); + + DvzBarriers barriers{}; + dvz_barriers(&barriers); + auto* image_barrier = dvz_barriers_image(&barriers, dvz_image_handle(image_, 0)); + if (completed_layout_ == VK_IMAGE_LAYOUT_UNDEFINED) { + dvz_barrier_image_stage(image_barrier, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + dvz_barrier_image_access( + image_barrier, 0, + VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + } else { + dvz_barrier_image_stage(image_barrier, VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + dvz_barrier_image_access( + image_barrier, VK_ACCESS_2_TRANSFER_READ_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + } + dvz_barrier_image_layout(image_barrier, completed_layout_, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + dvz_barrier_image_aspect(image_barrier, VK_IMAGE_ASPECT_COLOR_BIT); + dvz_barrier_image_mip(image_barrier, 0, 1); + dvz_barrier_image_layers(image_barrier, 0, 1); + dvz_cmd_barriers(commands_, &barriers); + recording_ = true; + } + + [[nodiscard]] DvzStreamFrame stream_frame() const { + DvzStreamFrame frame{}; + frame.image = dvz_image_handle(image_, 0); + frame.command_buffer = dvz_commands_handle(commands_); + frame.image_view = dvz_image_views_handle(view_, 0); + frame.extent = {extent_.width, extent_.height}; + frame.color_format = VK_FORMAT_R8G8B8A8_UNORM; + frame.image_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + frame.usage = DVZ_STREAM_FRAME_USAGE_RENDER_TARGET | DVZ_STREAM_FRAME_USAGE_COPY_SRC; + frame.command_buffer_recording = recording_; + frame.image_borrowed = true; + frame.image_view_borrowed = true; + frame.command_buffer_borrowed = true; + frame.handles_dirty = true; + frame.resource_generation = generation_; + frame.image_valid = true; + frame.memory_fd = -1; + frame.wait_semaphore_fd = -1; + return frame; + } + + [[nodiscard]] std::vector finish() { + if (!recording_) + throw std::logic_error("Datoviz frame target is not recording"); + DvzBarriers image_barriers{}; + dvz_barriers(&image_barriers); + auto* image_barrier = + dvz_barriers_image(&image_barriers, dvz_image_handle(image_, 0)); + dvz_barrier_image_stage(image_barrier, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_2_TRANSFER_BIT); + dvz_barrier_image_access(image_barrier, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_2_TRANSFER_READ_BIT); + dvz_barrier_image_layout(image_barrier, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + dvz_barrier_image_aspect(image_barrier, VK_IMAGE_ASPECT_COLOR_BIT); + dvz_barrier_image_mip(image_barrier, 0, 1); + dvz_barrier_image_layers(image_barrier, 0, 1); + dvz_cmd_barriers(commands_, &image_barriers); + + DvzImageRegion region{}; + dvz_image_region(®ion); + dvz_image_region_extent(®ion, extent_.width, extent_.height, 1); + dvz_cmd_copy_image_to_buffer( + commands_, dvz_image_handle(image_, 0), + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, ®ion, + dvz_buffer_handle(readback_), 0); + + DvzBarriers buffer_barriers{}; + dvz_barriers(&buffer_barriers); + auto* buffer_barrier = dvz_barriers_buffer( + &buffer_barriers, dvz_buffer_handle(readback_), 0, byte_size_); + dvz_barrier_buffer_stage(buffer_barrier, VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_PIPELINE_STAGE_2_HOST_BIT); + dvz_barrier_buffer_access(buffer_barrier, VK_ACCESS_2_TRANSFER_WRITE_BIT, + VK_ACCESS_2_HOST_READ_BIT); + dvz_cmd_barriers(commands_, &buffer_barriers); + + if (dvz_cmd_end_result(commands_) != 0) + throw std::runtime_error("failed to end Datoviz command buffer"); + recording_ = false; + dvz_fence_reset(fence_); + dvz_submit(submit_); + dvz_submit_command(submit_, dvz_commands_handle(commands_)); + DvzQueue* queue = dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN); + if (dvz_submit_send(submit_, dvz_queue_handle(queue), + dvz_fence_handle(fence_)) != VK_SUCCESS || + !dvz_fence_wait(fence_)) + throw std::runtime_error("failed to submit Datoviz point frame"); + completed_layout_ = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + + std::vector pixels(static_cast(byte_size_)); + dvz_buffer_download(readback_, 0, byte_size_, pixels.data()); + return pixels; + } + + void abort() noexcept { + if (recording_ && commands_ != nullptr) + dvz_cmd_reset(commands_); + recording_ = false; + } + +private: + void destroy() noexcept { + if (gpu_context_ != nullptr && fence_ != nullptr) + (void)dvz_fence_wait(fence_); + if (readback_ != nullptr) { + dvz_buffer_destroy(readback_); + dvz_buffer_free(readback_); + readback_ = nullptr; + } + if (fence_ != nullptr) { + dvz_fence_destroy(fence_); + dvz_fence_free(fence_); + fence_ = nullptr; + } + if (submit_ != nullptr) { + dvz_submit_free(submit_); + submit_ = nullptr; + } + if (commands_ != nullptr) { + dvz_commands_destroy(commands_); + dvz_commands_free(commands_); + commands_ = nullptr; + } + if (view_ != nullptr) { + dvz_image_views_destroy(view_); + dvz_image_views_free(view_); + view_ = nullptr; + } + if (image_ != nullptr) { + dvz_images_destroy(image_); + dvz_images_free(image_); + image_ = nullptr; + } + } + + DvzGpuCtx* gpu_context_{}; + Extent extent_{}; + std::uint64_t generation_{}; + DvzSize byte_size_{}; + DvzImages* image_{}; + DvzImageViews* view_{}; + DvzCommands* commands_{}; + DvzFence* fence_{}; + DvzSubmit* submit_{}; + DvzBuffer* readback_{}; + VkImageLayout completed_layout_{VK_IMAGE_LAYOUT_UNDEFINED}; + bool recording_{}; +}; + +Datoviz_Point_Backend::Datoviz_Point_Backend( + std::uint32_t gpu_index, bool validation_enabled, + const Scene_State& initial_scene) + : domain_thread_(std::this_thread::get_id()) { + try { + DvzGpuCtxConfig configuration = dvz_gpu_ctx_config(); + dvz_gpu_ctx_config_validation(&configuration, validation_enabled); + dvz_gpu_ctx_config_gpu(&configuration, gpu_index); + dvz_gpu_ctx_config_enable_canvas_extensions(&configuration, false); + gpu_context_ = dvz_gpu_ctx(&configuration); + if (gpu_context_ == nullptr) + throw std::runtime_error("failed to create Datoviz GPU context"); + DvzDrp2RuntimeConfig runtime_configuration = dvz_drp2_runtime_vklite_config( + dvz_gpu_ctx_device(gpu_context_), dvz_gpu_ctx_alloc(gpu_context_)); + runtime_ = dvz_drp2_runtime_vklite(&runtime_configuration); + if (runtime_ == nullptr) + throw std::runtime_error("failed to create Datoviz DRP2 runtime"); + create_scene(initial_scene); + } catch (...) { + destroy(); + throw; + } +} + +Datoviz_Point_Backend::~Datoviz_Point_Backend() { destroy(); } + +void Datoviz_Point_Backend::require_domain() const { + if (std::this_thread::get_id() != domain_thread_) + throw std::logic_error("Datoviz objects may only be used on the render domain"); +} + +void Datoviz_Point_Backend::create_scene(const Scene_State& initial_scene) { + scene_ = dvz_scene(); + if (scene_ == nullptr) + throw std::runtime_error("failed to create Datoviz scene"); + const DvzCapabilitySnapshot capabilities = dvz_capability_snapshot(); + if (dvz_scene_set_capabilities(scene_, &capabilities) != DVZ_OK) + throw std::runtime_error("failed to configure Datoviz scene capabilities"); + figure_ = dvz_figure(scene_, initial_scene.viewport.width, + initial_scene.viewport.height, 0); + panel_ = figure_ != nullptr ? dvz_panel_full(figure_) : nullptr; + visual_ = panel_ != nullptr ? dvz_point(scene_, 0) : nullptr; + if (figure_ == nullptr || panel_ == nullptr || visual_ == nullptr || + dvz_panel_add_visual(panel_, visual_, nullptr) != DVZ_OK) + throw std::runtime_error("failed to create Datoviz point visual"); + + DvzCameraDesc camera = dvz_camera_desc(); + camera.view.eye[0] = 0.0F; + camera.view.eye[1] = 0.0F; + camera.view.eye[2] = 4.0F; + camera.view.target[0] = 0.0F; + camera.view.target[1] = 0.0F; + camera.view.target[2] = 0.0F; + camera.projection.near_clip = 0.01F; + camera.projection.far_clip = 100.0F; + if (dvz_panel_set_camera_desc(panel_, &camera) != DVZ_OK) + throw std::runtime_error("failed to create Datoviz point camera"); + DvzController* controller = dvz_arcball(scene_, nullptr); + if (controller == nullptr || + dvz_panel_bind_controller(panel_, controller, DVZ_DIM_MASK_XYZ) != DVZ_OK) + throw std::runtime_error("failed to bind Datoviz arcball controller"); + + input_router_ = dvz_input_router(); + gesture_handler_ = input_router_ != nullptr + ? dvz_pointer_gesture_handler(input_router_) + : nullptr; + if (input_router_ == nullptr || gesture_handler_ == nullptr || + dvz_panel_connect_input(panel_, input_router_) != DVZ_OK) + throw std::runtime_error("failed to connect Datoviz point input"); + + DvzInputResizeEvent resize{ + initial_scene.viewport.width, initial_scene.viewport.height, + initial_scene.viewport.width, initial_scene.viewport.height, 1.0F, 1.0F}; + dvz_input_emit_resize(input_router_, &resize); +} + +void Datoviz_Point_Backend::apply(const Point_Frame_Data& frame) { + require_domain(); + if (frame.scene_revision != applied_scene_revision_) { + if (dvz_figure_resize(figure_, frame.scene.viewport.width, + frame.scene.viewport.height) != DVZ_OK) + throw std::runtime_error("failed to resize Datoviz point figure"); + DvzInputResizeEvent resize{ + frame.scene.viewport.width, frame.scene.viewport.height, + frame.scene.viewport.width, frame.scene.viewport.height, 1.0F, 1.0F}; + dvz_input_emit_resize(input_router_, &resize); + applied_scene_revision_ = frame.scene_revision; + } + + if (frame.point.state_revision != applied_state_revision_) { + const auto& state = frame.point.state; + DvzPointStyleDesc style = dvz_point_style_desc(); + style.edge_color.r = state.style.edge_color.red; + style.edge_color.g = state.style.edge_color.green; + style.edge_color.b = state.style.edge_color.blue; + style.edge_color.a = state.style.edge_color.alpha; + style.stroke_width_px = state.style.stroke_width_px; + style.aspect = aspect(state.style.aspect); + mat4 transform{}; + for (std::size_t row = 0; row < 4; ++row) { + for (std::size_t column = 0; column < 4; ++column) + transform[row][column] = state.transform.values[row * 4 + column]; + } + if (dvz_point_set_style(visual_, &style) != DVZ_OK || + dvz_visual_set_transform(visual_, transform) != DVZ_OK || + dvz_visual_set_depth_test(visual_, state.depth_test) != DVZ_OK || + dvz_visual_set_visible(visual_, state.visible && !frame.point.data.empty()) != DVZ_OK) + throw std::runtime_error("failed to apply Datoviz point state"); + applied_state_revision_ = frame.point.state_revision; + } + + if (frame.point.data_revision != applied_data_revision_) { + if (frame.point.data.empty()) { + if (dvz_visual_set_visible(visual_, false) != DVZ_OK) + throw std::runtime_error("failed to hide empty Datoviz point visual"); + } else { + std::vector> positions; + std::vector> colors; + std::vector diameters; + positions.reserve(frame.point.data.size()); + colors.reserve(frame.point.data.size()); + diameters.reserve(frame.point.data.size()); + for (const auto& point : frame.point.data) { + positions.push_back( + {point.position.x, point.position.y, point.position.z}); + colors.push_back({point.color.red, point.color.green, + point.color.blue, point.color.alpha}); + diameters.push_back(point.diameter_px); + } + if (positions.size() > std::numeric_limits::max()) + throw std::length_error("Datoviz point payload is too large"); + const auto count = static_cast(positions.size()); + const std::array updates{{ + {"position", positions.data(), count}, + {"color", colors.data(), count}, + {"diameter_px", diameters.data(), count}, + }}; + if (dvz_visual_set_data_many(visual_, updates.data(), + static_cast(updates.size())) != DVZ_OK || + dvz_visual_set_visible(visual_, frame.point.state.visible) != DVZ_OK) + throw std::runtime_error("failed to upload Datoviz point payload"); + } + applied_data_revision_ = frame.point.data_revision; + } +} + +void Datoviz_Point_Backend::dispatch(const Input_Command& command, Extent viewport) { + require_domain(); + const float width = static_cast(viewport.width); + const float height = static_cast(viewport.height); + switch (command.type) { + case Input_Command_Type::Pointer_Move: + case Input_Command_Type::Pointer_Press: + case Input_Command_Type::Pointer_Release: { + const DvzPointerEventType type = command.type == Input_Command_Type::Pointer_Move + ? DVZ_POINTER_EVENT_MOVE + : command.type == Input_Command_Type::Pointer_Press + ? DVZ_POINTER_EVENT_PRESS + : DVZ_POINTER_EVENT_RELEASE; + dvz_pointer_emit_position(input_router_, type, command.x, command.y, + width, height, button(command.button), + modifiers(command.modifiers), 1.0F, + dvz_input_timestamp_ns(), nullptr); + break; + } + case Input_Command_Type::Wheel: + dvz_pointer_emit_wheel(input_router_, command.x, command.y, width, height, + command.delta_x, command.delta_y, + modifiers(command.modifiers), 1.0F, + dvz_input_timestamp_ns(), nullptr); + break; + case Input_Command_Type::Key_Press: + case Input_Command_Type::Key_Repeat: + case Input_Command_Type::Key_Release: { + const DvzKeyboardEventType type = command.type == Input_Command_Type::Key_Press + ? DVZ_KEYBOARD_EVENT_PRESS + : command.type == Input_Command_Type::Key_Repeat + ? DVZ_KEYBOARD_EVENT_REPEAT + : DVZ_KEYBOARD_EVENT_RELEASE; + dvz_keyboard_emit(input_router_, type, + key_code(command.key, command.native_key), + modifiers(command.modifiers), nullptr); + break; + } + } +} + +DvzSceneFrameArtifact* Datoviz_Point_Backend::emit(const Point_Frame_Data& frame) { + DvzFramePlanEmitConfig configuration = dvz_frame_plan_emit_config(); + configuration.shader_format = DVZ_SCENE_SHADER_FORMAT_GLSL; + configuration.external_color_target = true; + configuration.color_target_id = color_target_id; + configuration.color_target_format = DVZ_FORMAT_R8G8B8A8_UNORM; + configuration.target_width = frame.scene.viewport.width; + configuration.target_height = frame.scene.viewport.height; + configuration.clear_color[0] = frame.scene.clear_color.red; + configuration.clear_color[1] = frame.scene.clear_color.green; + configuration.clear_color[2] = frame.scene.clear_color.blue; + configuration.clear_color[3] = frame.scene.clear_color.alpha; + const DvzCapabilitySnapshot capabilities = dvz_capability_snapshot(); + DvzDiagnosticReport report{}; + dvz_diagnostic_report_init(&report); + return dvz_figure_emit_frame(figure_, &capabilities, &report, &configuration); +} + +std::shared_ptr Datoviz_Point_Backend::render( + const Point_Frame_Data& frame) { + require_domain(); + if (frame.scene.viewport.empty()) + return {}; + apply(frame); + for (const auto& command : frame.input) + dispatch(command, frame.scene.viewport); + + if (target_ == nullptr || target_extent_ != frame.scene.viewport) { + target_.reset(); + target_extent_ = frame.scene.viewport; + target_ = std::make_unique(gpu_context_, target_extent_, + ++target_generation_); + } + + target_->begin(); + DvzSceneFrameArtifact* artifact = emit(frame); + if (artifact == nullptr) { + target_->abort(); + throw std::runtime_error("failed to emit Datoviz point frame"); + } + const DvzDrp2CommandStream* stream = dvz_scene_frame_artifact_stream(artifact); + const DvzStreamFrame target_frame = target_->stream_frame(); + const bool attached = stream != nullptr && + dvz_drp2_runtime_attach_frame_target( + runtime_, color_target_id, &target_frame); + const DvzDrp2ValidationResult result = + attached ? dvz_drp2_runtime_execute(runtime_, stream) + : DvzDrp2ValidationResult{}; + dvz_scene_frame_artifact_destroy(artifact); + if (!attached) { + target_->abort(); + throw std::runtime_error("failed to attach the Datoviz point frame target"); + } + if (!result.ok) { + target_->abort(); + throw std::runtime_error( + "failed to execute Datoviz point frame: validation code " + + std::to_string(static_cast(result.code)) + + ", command " + std::to_string(result.command_index)); + } + + auto output = std::make_shared(); + output->extent = frame.scene.viewport; + output->sequence = frame.frame_sequence; + output->rgba8 = target_->finish(); + return output; +} + +void Datoviz_Point_Backend::destroy() noexcept { + if (std::this_thread::get_id() != domain_thread_) + std::terminate(); + if (runtime_ != nullptr) { + dvz_drp2_runtime_destroy(runtime_); + runtime_ = nullptr; + } + target_.reset(); + if (panel_ != nullptr && input_router_ != nullptr) + (void)dvz_panel_connect_input(panel_, nullptr); + if (gesture_handler_ != nullptr) { + dvz_pointer_gesture_handler_destroy(gesture_handler_); + gesture_handler_ = nullptr; + } + if (input_router_ != nullptr) { + dvz_input_router_destroy(input_router_); + input_router_ = nullptr; + } + visual_ = nullptr; + panel_ = nullptr; + figure_ = nullptr; + if (scene_ != nullptr) { + dvz_scene_destroy(scene_); + scene_ = nullptr; + } + if (gpu_context_ != nullptr) { + dvz_gpu_ctx_destroy(gpu_context_); + gpu_context_ = nullptr; + } +} + +} // namespace renderive::render_3d::detail diff --git a/render_3D/render_3D/detail/Datoviz_Point_Backend.h b/render_3D/render_3D/detail/Datoviz_Point_Backend.h new file mode 100644 index 0000000..8ff355c --- /dev/null +++ b/render_3D/render_3D/detail/Datoviz_Point_Backend.h @@ -0,0 +1,58 @@ +#pragma once + +#include "Point_Core.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace renderive::render_3d::detail { + +class Datoviz_Point_Backend final { +public: + Datoviz_Point_Backend(std::uint32_t gpu_index, bool validation_enabled, + const Scene_State& initial_scene); + ~Datoviz_Point_Backend(); + + Datoviz_Point_Backend(const Datoviz_Point_Backend&) = delete; + Datoviz_Point_Backend& operator=(const Datoviz_Point_Backend&) = delete; + + [[nodiscard]] std::shared_ptr render(const Point_Frame_Data& frame); + +private: + class Frame_Target; + + void require_domain() const; + void create_scene(const Scene_State& initial_scene); + void apply(const Point_Frame_Data& frame); + void dispatch(const Input_Command& command, Extent viewport); + [[nodiscard]] DvzSceneFrameArtifact* emit(const Point_Frame_Data& frame); + void destroy() noexcept; + + std::thread::id domain_thread_; + DvzGpuCtx* gpu_context_{}; + DvzDrp2Runtime* runtime_{}; + DvzScene* scene_{}; + DvzFigure* figure_{}; + DvzPanel* panel_{}; + DvzVisual* visual_{}; + DvzInputRouter* input_router_{}; + DvzPointerGestureHandler* gesture_handler_{}; + std::unique_ptr target_; + Extent target_extent_{}; + std::uint64_t target_generation_{}; + std::uint64_t applied_scene_revision_{}; + std::uint64_t applied_state_revision_{}; + std::uint64_t applied_data_revision_{}; +}; + +} // namespace renderive::render_3d::detail diff --git a/render_3D/render_3D/detail/Point_Core.h b/render_3D/render_3D/detail/Point_Core.h new file mode 100644 index 0000000..fe8a64e --- /dev/null +++ b/render_3D/render_3D/detail/Point_Core.h @@ -0,0 +1,250 @@ +#pragma once + +#include "render_3D/Point_Scene.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::render_3d::detail { + +struct Published_Point { + Point_State state; + std::vector data; + std::uint64_t state_revision{}; + std::uint64_t data_revision{}; +}; + +class Point_State_Access final { +public: + [[nodiscard]] static Published_Point publish(Point_Visual& visual); +}; + +struct Scene_State { + Extent viewport{560, 320}; + Clear_Color clear_color; + bool operator==(const Scene_State&) const = default; +}; + +struct Scene_State_Base {}; + +class Scene_State_Buffer final + : public ::Double_State_Strategy { + using Base = ::Double_State_Strategy; + +public: + struct Published { + Scene_State state; + std::uint64_t revision{}; + }; + + explicit Scene_State_Buffer(const Scene_State& initial) : Base(initial) {} + + [[nodiscard]] Published publish_snapshot() { + std::lock_guard lock(publication_mutex_); + Base::publish(); + return {Base::render_state_value(), Base::state_revision()}; + } + +private: + std::mutex publication_mutex_; +}; + +enum class Input_Command_Type : std::uint8_t { + Pointer_Move, + Pointer_Press, + Pointer_Release, + Wheel, + Key_Press, + Key_Repeat, + Key_Release, +}; + +struct Input_Command { + Input_Command_Type type{}; + float x{}; + float y{}; + float delta_x{}; + float delta_y{}; + ::renderive::Mouse_Button button{::renderive::Mouse_Button::None}; + ::renderive::Mouse_Button_Mask buttons{}; + ::renderive::Keyboard_Modifier modifiers{::renderive::Keyboard_Modifier::None}; + ::renderive::Key key{::renderive::Key::Unknown}; + std::uint32_t native_key{}; +}; + +class Input_Collector final { +public: + void push(Input_Command command) { + std::lock_guard lock(mutex_); + commands_.push_back(command); + } + + [[nodiscard]] std::vector drain() { + std::lock_guard lock(mutex_); + std::vector result; + result.reserve(commands_.size()); + while (!commands_.empty()) { + result.push_back(commands_.front()); + commands_.pop_front(); + } + return result; + } + +private: + std::mutex mutex_; + std::deque commands_; +}; + +struct Point_Frame_Data { + Scene_State scene; + Published_Point point; + std::vector input; + std::uint64_t scene_revision{}; + std::uint64_t frame_sequence{}; +}; + +class Frame_Scheduler final { + using Manual = ::Manual_Refresh_Strategy; + using Low_Latency = ::Low_Latency_Strategy; + using Playback = ::Flow_Refresh_Strategy; + using Strategy = std::variant, std::unique_ptr, + std::unique_ptr>; + +public: + Frame_Scheduler(Frame_Mode mode, double maximum_frames_per_second) + : strategy_(make_strategy(mode, maximum_frames_per_second)) {} + + [[nodiscard]] bool prepare(Scene_State_Buffer& scene, Point_Visual& point, + Input_Collector& input) { + return std::visit( + [&](auto& strategy) { + auto lease = strategy->acquire_painter(); + if (!lease) + return false; + auto published_scene = scene.publish_snapshot(); + lease->scene = std::move(published_scene.state); + lease->scene_revision = published_scene.revision; + lease->point = Point_State_Access::publish(point); + lease->input = input.drain(); + lease->frame_sequence = lease->statistics.sequence; + return true; + }, + strategy_); + } + + [[nodiscard]] bool refresh() { + auto* manual = std::get_if>(&strategy_); + return manual != nullptr && (*manual)->refresh(); + } + + [[nodiscard]] bool activate_for_request() { + if (auto* manual = std::get_if>(&strategy_)) + return (*manual)->refresh(); + return true; + } + + [[nodiscard]] bool discard_pending() { + if (auto* manual = std::get_if>(&strategy_)) + return (*manual)->discard_pending_frame(); + if (auto* low_latency = std::get_if>(&strategy_)) + return (*low_latency)->discard_pending_frame(); + return false; + } + + template + requires std::invocable + [[nodiscard]] bool render(Render&& render) { + return std::visit( + [&](auto& strategy) { + auto lease = strategy->acquire_renderer(); + if (!lease) + return false; + return static_cast(std::invoke( + std::forward(render), + static_cast(*lease))); + }, + strategy_); + } + + [[nodiscard]] Frame_Status status() const { + return std::visit( + [](const auto& strategy) { + using Strategy_Type = std::remove_cvref_t; + Frame_Status result; + if constexpr (std::same_as) { + result.mode = Frame_Mode::Manual; + const auto state = strategy->state(); + result.produced_frame_count = state.prepared_frame_count; + result.consumed_frame_count = state.render_count; + result.dropped_frame_count = state.replaced_prepared_frame_count + + state.discarded_prepared_frame_count; + result.failed_operation_count = state.failed_refresh_count; + result.pending_frame_count = state.pending_frame ? 1U : 0U; + result.latest_sequence = state.render_frame_sequence; + } else if constexpr (std::same_as) { + result.mode = Frame_Mode::Low_Latency; + const auto state = strategy->state(); + const auto counters = strategy->counter_statistics(); + result.frequency_hz = state.frequency_hz; + result.produced_frame_count = counters.published_frame_count; + result.consumed_frame_count = state.completed_lifecycle_count; + result.dropped_frame_count = counters.abandoned_frame_count + + counters.manually_discarded_frame_count; + result.failed_operation_count = counters.swap_failure_count; + const auto retired = result.consumed_frame_count + + result.dropped_frame_count; + result.pending_frame_count = + result.produced_frame_count > retired ? 1U : 0U; + result.latest_sequence = state.frame_sequence; + result.next_refresh_interval_ns = state.next_refresh_interval_ns; + } else { + result.mode = Frame_Mode::Playback; + const auto state = strategy->state(); + result.produced_frame_count = state.enqueued_frame_count; + result.consumed_frame_count = state.rendered_frame_count; + result.failed_operation_count = state.empty_acquire_count; + result.pending_frame_count = state.pending_frame_count; + } + return result; + }, + strategy_); + } + +private: + static Strategy make_strategy(Frame_Mode mode, double maximum_frames_per_second) { + switch (mode) { + case Frame_Mode::Manual: + return Strategy(std::in_place_type>, + std::make_unique()); + case Frame_Mode::Low_Latency: + return Strategy( + std::in_place_type>, + std::make_unique( + Observer_State<>{}, + typename Low_Latency::Configuration{maximum_frames_per_second})); + case Frame_Mode::Playback: + return Strategy(std::in_place_type>, + std::make_unique()); + } + throw std::invalid_argument("unknown Point_Scene frame mode"); + } + + Strategy strategy_; +}; + +} // namespace renderive::render_3d::detail diff --git a/render_3D/render_3D/detail/Render_Domain.h b/render_3D/render_3D/detail/Render_Domain.h new file mode 100644 index 0000000..3bbc0ff --- /dev/null +++ b/render_3D/render_3D/detail/Render_Domain.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::render_3d::detail { + +class Render_Domain final { +public: + Render_Domain() : thread_([this] { run(); }) {} + ~Render_Domain() { + { + std::lock_guard lock(mutex_); + stopping_ = true; + } + condition_.notify_one(); + if (thread_.joinable()) + thread_.join(); + } + + Render_Domain(const Render_Domain&) = delete; + Render_Domain& operator=(const Render_Domain&) = delete; + + template + auto invoke(Function&& function) -> std::invoke_result_t { + using Result = std::invoke_result_t; + auto task = std::make_shared>( + std::forward(function)); + auto result = task->get_future(); + { + std::lock_guard lock(mutex_); + if (stopping_) + throw std::runtime_error("Point_Scene render domain is stopping"); + tasks_.emplace([task] { (*task)(); }); + } + condition_.notify_one(); + if constexpr (std::is_void_v) + result.get(); + else + return result.get(); + } + +private: + void run() { + for (;;) { + std::function task; + { + std::unique_lock lock(mutex_); + condition_.wait(lock, [&] { return stopping_ || !tasks_.empty(); }); + if (stopping_ && tasks_.empty()) + return; + task = std::move(tasks_.front()); + tasks_.pop(); + } + task(); + } + } + + std::mutex mutex_; + std::condition_variable condition_; + std::queue> tasks_; + bool stopping_{}; + std::thread thread_; +}; + +} // namespace renderive::render_3d::detail diff --git a/render_3D/tests/Point_Render_Integration_Tests.cpp b/render_3D/tests/Point_Render_Integration_Tests.cpp new file mode 100644 index 0000000..7c79f3a --- /dev/null +++ b/render_3D/tests/Point_Render_Integration_Tests.cpp @@ -0,0 +1,105 @@ +#include "render_3D/Point_Demo.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::render_3d { +namespace { + +struct Event_Point { + float x{}; + float y{}; +}; + +std::size_t colored_pixel_count(const Pixel_Frame& frame) { + if (frame.rgba8.size() < 4) + return 0; + const auto* bytes = reinterpret_cast(frame.rgba8.data()); + const std::array background{ + bytes[0], bytes[1], bytes[2], bytes[3]}; + std::size_t result{}; + for (std::size_t offset = 0; offset + 3 < frame.rgba8.size(); offset += 4) { + result += bytes[offset] != background[0] || + bytes[offset + 1] != background[1] || + bytes[offset + 2] != background[2] || + bytes[offset + 3] != background[3]; + } + return result; +} + +TEST(PointRenderIntegration, RendersRealRgbaAndResizes) { + try { + auto demo = make_point_demo(Scene_Options{.viewport = {320, 200}}); + ASSERT_TRUE(demo.scene->request_frame()); + auto first = demo.scene->latest_frame(); + ASSERT_NE(first, nullptr); + EXPECT_EQ(first->extent, (Extent{320, 200})); + EXPECT_EQ(first->rgba8.size(), 320U * 200U * 4U); + EXPECT_GT(colored_pixel_count(*first), 100U); + + demo.scene->resize({480, 270}); + ASSERT_TRUE(demo.scene->request_frame()); + auto resized = demo.scene->latest_frame(); + ASSERT_NE(resized, nullptr); + EXPECT_EQ(resized->extent, (Extent{480, 270})); + EXPECT_EQ(resized->rgba8.size(), 480U * 270U * 4U); + EXPECT_GT(colored_pixel_count(*resized), 100U); + } catch (const std::exception& error) { + GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); + } +} + +TEST(PointRenderIntegration, KernelEventsReachDatovizArcballOnItsDomain) { + try { + auto demo = make_point_demo(Scene_Options{.viewport = {320, 200}}); + ASSERT_TRUE(demo.scene->request_frame()); + const auto before = demo.scene->latest_frame()->rgba8; + + ::renderive::Basic_Pointer_Event press( + ::renderive::Event_Type::Pointer_Press); + press.position = {120.0F, 90.0F}; + press.button = ::renderive::Mouse_Button::Left; + demo.scene->dispatch(press); + ::renderive::Basic_Pointer_Event move( + ::renderive::Event_Type::Pointer_Move); + move.position = {210.0F, 125.0F}; + move.buttons = 1; + demo.scene->dispatch(move); + move.position = {245.0F, 145.0F}; + demo.scene->dispatch(move); + ::renderive::Basic_Pointer_Event release( + ::renderive::Event_Type::Pointer_Release); + release.position = move.position; + release.button = ::renderive::Mouse_Button::Left; + demo.scene->dispatch(release); + + ASSERT_TRUE(demo.scene->request_frame()); + const auto after_drag = demo.scene->latest_frame(); + ASSERT_NE(after_drag, nullptr); + EXPECT_NE(after_drag->rgba8, before); + + ::renderive::Basic_Wheel_Event wheel; + wheel.position = {160.0F, 100.0F}; + wheel.pixel_delta_y = 180.0; + wheel.angle_delta_y = -216.0; + demo.scene->dispatch(wheel); + + ASSERT_TRUE(demo.scene->request_frame()); + const auto after_wheel = demo.scene->latest_frame(); + ASSERT_NE(after_wheel, nullptr); + EXPECT_GT(colored_pixel_count(*after_wheel), 100U); + EXPECT_NE(after_wheel->rgba8, after_drag->rgba8); + } catch (const std::exception& error) { + GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); + } +} + +} // namespace +} // namespace renderive::render_3d diff --git a/render_3D/tests/Point_State_Tests.cpp b/render_3D/tests/Point_State_Tests.cpp new file mode 100644 index 0000000..a1e4443 --- /dev/null +++ b/render_3D/tests/Point_State_Tests.cpp @@ -0,0 +1,128 @@ +#include "render_3D/Point_Visual.h" +#include "render_3D/detail/Point_Core.h" + +#include + +#include +#include +#include +#include +#include + +namespace renderive::render_3d { +namespace { + +TEST(PointState, UsesKernelDoubleStateAtFrameBoundary) { + auto data = std::make_shared(); + data->update(std::vector{{{1.0F, 2.0F, 3.0F}, {1, 2, 3, 255}, 7.0F}}); + Point_Visual visual(data); + + Point_State configured; + configured.visible = false; + configured.style.stroke_width_px = 3.0F; + visual.configure(configured); + + const auto published = detail::Point_State_Access::publish(visual); + EXPECT_EQ(published.state, configured); + ASSERT_EQ(published.data.size(), 1U); + EXPECT_EQ(published.data.front().position, (Vec3{1.0F, 2.0F, 3.0F})); + EXPECT_EQ(published.state_revision, 1U); + EXPECT_EQ(published.data_revision, 1U); +} + +TEST(PointState, UsesKernelLatestRealTimeDataForBulkPayloads) { + auto data = std::make_shared(); + Point_Visual visual(data); + constexpr int update_count = 500; + std::atomic done{}; + + std::thread writer([&] { + for (int value = 1; value <= update_count; ++value) { + std::vector points(static_cast(value % 31 + 1)); + for (auto& point : points) { + point.position.x = static_cast(value); + point.diameter_px = static_cast(value % 12 + 1); + } + data->update(std::move(points)); + } + done.store(true, std::memory_order_release); + }); + + do { + const auto published = detail::Point_State_Access::publish(visual); + if (!published.data.empty()) { + const float expected = published.data.front().position.x; + for (const auto& point : published.data) + EXPECT_EQ(point.position.x, expected); + } + } while (!done.load(std::memory_order_acquire)); + writer.join(); + + const auto published = detail::Point_State_Access::publish(visual); + EXPECT_EQ(published.data_revision, update_count); + ASSERT_FALSE(published.data.empty()); + EXPECT_EQ(published.data.front().position.x, static_cast(update_count)); +} + +TEST(PointState, RejectsInvalidStateAndPayloadAtTheOwningBoundary) { + auto data = std::make_shared(); + Point_Visual visual(data); + Point_State invalid; + invalid.style.stroke_width_px = -1.0F; + EXPECT_THROW(visual.configure(invalid), std::invalid_argument); + + data->update(std::vector{{{}, {}, 0.0F}}); + EXPECT_THROW((void)detail::Point_State_Access::publish(visual), + std::invalid_argument); +} + +TEST(PointFrameControl, ReusesKernelManualAndPlaybackStrategies) { + auto data = std::make_shared(); + data->update(std::vector{{{}, {}, 8.0F}}); + Point_Visual visual(data); + detail::Scene_State_Buffer scene({{320, 180}, {}}); + detail::Input_Collector input; + + detail::Frame_Scheduler manual(Frame_Mode::Manual, 60.0); + EXPECT_TRUE(manual.prepare(scene, visual, input)); + auto manual_status = manual.status(); + EXPECT_EQ(manual_status.mode, Frame_Mode::Manual); + EXPECT_EQ(manual_status.produced_frame_count, 1U); + EXPECT_EQ(manual_status.pending_frame_count, 1U); + bool rendered{}; + EXPECT_FALSE(manual.render([&](const detail::Point_Frame_Data&) { + rendered = true; + return true; + })); + EXPECT_FALSE(rendered); + EXPECT_TRUE(manual.refresh()); + EXPECT_TRUE(manual.render([&](const detail::Point_Frame_Data& frame) { + rendered = true; + EXPECT_EQ(frame.point.data.size(), 1U); + return true; + })); + EXPECT_TRUE(rendered); + manual_status = manual.status(); + EXPECT_EQ(manual_status.consumed_frame_count, 1U); + EXPECT_EQ(manual_status.pending_frame_count, 0U); + + detail::Frame_Scheduler playback(Frame_Mode::Playback, 60.0); + EXPECT_TRUE(playback.prepare(scene, visual, input)); + data->update(std::vector(2, Point{{}, {}, 8.0F})); + EXPECT_TRUE(playback.prepare(scene, visual, input)); + auto playback_status = playback.status(); + EXPECT_EQ(playback_status.produced_frame_count, 2U); + EXPECT_EQ(playback_status.pending_frame_count, 2U); + EXPECT_TRUE(playback.render([](const detail::Point_Frame_Data& frame) { + return frame.point.data.size() == 1U; + })); + EXPECT_TRUE(playback.render([](const detail::Point_Frame_Data& frame) { + return frame.point.data.size() == 2U; + })); + playback_status = playback.status(); + EXPECT_EQ(playback_status.consumed_frame_count, 2U); + EXPECT_EQ(playback_status.pending_frame_count, 0U); +} + +} // namespace +} // namespace renderive::render_3d diff --git a/render_3D/调试.md b/render_3D/调试.md index 6fa1821..9471ba5 100644 --- a/render_3D/调试.md +++ b/render_3D/调试.md @@ -1,3 +1,6 @@ +"C:\Program Files\JetBrains\CLion 2026.1\bin\cmake\win\x64\bin\cmake.exe" -DCMAKE_BUILD_TYPE=Debug --preset vs2022_debug +-S D:\ae\proj\Renderive -B D:\ae\proj\Renderive\cmake-build-vs2022_debug + 默认每次运行程序都通过CDB运行 D:\ae\ewdk\EWDK_22621_230929-1800\Program Files\Windows Kits\10\Debuggers\x64\cdb.exe D:\ae\tools 可能会有有用的工具 diff --git a/renderive_package_走偏的一版.zip b/renderive_package_走偏的一版.zip deleted file mode 100644 index 5adb94a..0000000 Binary files a/renderive_package_走偏的一版.zip and /dev/null differ diff --git a/web_server/CMakeLists.txt b/web_server/CMakeLists.txt index b5629e1..04a4481 100644 --- a/web_server/CMakeLists.txt +++ b/web_server/CMakeLists.txt @@ -108,6 +108,10 @@ if (NOT TARGET Renderive_render_2D) rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_Web requires Renderive_render_2D") return() endif () +if (NOT TARGET Renderive_render_3D) + rcl_log_append(${CMAKE_CURRENT_LIST_LINE} "[FATAL_ERROR] Renderive_Web requires Renderive_render_3D") + return() +endif () if (NOT TARGET Adminive::Nlohmann) set(Renderive_Web_saved_build_testing "${BUILD_TESTING}") set(BUILD_TESTING OFF) @@ -129,13 +133,14 @@ target_compile_features(Renderive_Web PUBLIC cxx_std_20) target_compile_definitions(Renderive_Web PRIVATE NOMINMAX) target_link_libraries(Renderive_Web PUBLIC Renderive_render_2D Drogon::Drogon Adminive::MagicEnum magic_enum::magic_enum - PRIVATE Adminive::Nlohmann spdlog::spdlog + PRIVATE Renderive_render_3D Adminive::Nlohmann spdlog::spdlog ) set(Renderive_Web_Server_source_dir "${CMAKE_CURRENT_LIST_DIR}/server") append_glob_source(Renderive_Web_Server_sources "${Renderive_Web_Server_source_dir}") add_executable(Renderive_Web_Server ${Renderive_Web_Server_sources}) target_compile_features(Renderive_Web_Server PRIVATE cxx_std_20) target_link_libraries(Renderive_Web_Server PRIVATE Renderive_Web) +renderive_stage_render_3D_runtime(Renderive_Web_Server) #add_dependencies(Renderive_Web_Server Renderive_Web_Assets) if (MSVC) target_compile_options(Renderive_Web PRIVATE /utf-8) @@ -156,6 +161,7 @@ if (RENDERIVE_BUILD_TESTS) set(Renderive_Web_test_target "Renderive_Web_${Renderive_Web_test_name}_${Renderive_Web_test_hash}") add_executable("${Renderive_Web_test_target}" "${Renderive_Web_test_source}") target_link_libraries("${Renderive_Web_test_target}" PRIVATE Renderive_Web Adminive::Nlohmann GTest::gtest_main) + renderive_stage_render_3D_runtime("${Renderive_Web_test_target}") add_test(NAME "${Renderive_Web_test_target}" COMMAND "${Renderive_Web_test_target}") endforeach () -endif () \ No newline at end of file +endif () diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index 81ae9f1..991fe76 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -7,6 +7,7 @@ #include "Pixel_Frame.h" #include "Web_Performance_Log.h" #include "render_2D/export.h" +#include "render_3D/Point_Demo.h" #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +93,9 @@ public: [[nodiscard]] virtual adminive::Update_Result apply_patch(std::string_view target, const nlohmann::json& patch) = 0; virtual void resize(Size size) = 0; virtual void dispatch(const Event& event) = 0; + virtual void dispatch(const Pointer_Event& event) = 0; + virtual void dispatch(const Wheel_Event& event) = 0; + virtual void dispatch(const Key_Event& event) = 0; virtual bool render_latest_frame() = 0; [[nodiscard]] virtual std::optional encode_latest_pixels() = 0; virtual void record_pixel_response(std::chrono::steady_clock::time_point request_started, std::chrono::steady_clock::time_point encode_started, std::chrono::steady_clock::time_point encode_finished, std::size_t pixel_bytes) = 0; @@ -235,6 +240,15 @@ public: plot_.deactivate_view(); plot_.dispatch_event(event); } + void dispatch(const Pointer_Event& event) { + plot_.dispatch_event(event); + } + void dispatch(const Wheel_Event& event) { + plot_.dispatch_event(event); + } + void dispatch(const Key_Event& event) { + plot_.dispatch_event(event); + } bool render_latest_frame() { if (!plot_.view_active()) return false; @@ -1302,7 +1316,350 @@ private: bool rendered_since_last_pixel_{}; std::string last_action_result_; }; + +render_3d::Frame_Mode point_frame_mode(Gallery_Frame_Mode mode) { + switch (mode) { + case Gallery_Frame_Mode::Manual: + return render_3d::Frame_Mode::Manual; + case Gallery_Frame_Mode::Low_Latency: + return render_3d::Frame_Mode::Low_Latency; + case Gallery_Frame_Mode::Playback: + return render_3d::Frame_Mode::Playback; + } + throw std::invalid_argument("unknown 3D frame mode"); +} + +class Point_Gallery_Scene final : public Gallery_Scene_Interface { +public: + Point_Gallery_Scene(Gallery_Frame_Mode mode, bool automatic_low_latency) + : mode_(mode), automatic_low_latency_(automatic_low_latency), + performance_started_(std::chrono::steady_clock::now()), + demo_(render_3d::make_point_demo({ + {560, 320}, {}, point_frame_mode(mode), 60.0, 0, false})) { + if (mode_ == Gallery_Frame_Mode::Playback) { + for (int index = 0; index < 24; ++index) { + phase_ = -1.20F + static_cast(index) * 0.05F; + publish_demo_points(); + (void)demo_.scene->prepare_frame(); + } + (void)record_render([this] { return demo_.scene->render_prepared_frame(); }); + } else { + (void)record_render([this] { return demo_.scene->request_frame(); }); + } + } + + [[nodiscard]] const std::string& case_id() const noexcept override { + return case_id_; + } + [[nodiscard]] nlohmann::json controls() const override { + return {{"resources", nlohmann::json::array()}, + {"observers", nlohmann::json::array()}, + {"render_plan", nullptr}, + {"performance_capture", nullptr}}; + } + [[nodiscard]] Gallery_Frame_Mode frame_mode() const noexcept override { + return mode_; + } + void reset_monitoring() override { + performance_started_ = std::chrono::steady_clock::now(); + render_attempt_count_ = 0; + successful_render_count_ = 0; + render_duration_statistics_.clear(); + pixel_encode_statistics_.clear(); + pixel_request_statistics_.clear(); + last_render_ms_ = 0.0; + last_pixel_encode_ms_ = 0.0; + last_pixel_request_ms_ = 0.0; + last_pixel_bytes_ = 0; + client_performance_ = {}; + } + [[nodiscard]] bool can_render_automatically() const noexcept override { + return active_ && automatic_low_latency_ && + mode_ == Gallery_Frame_Mode::Low_Latency; + } + [[nodiscard]] std::uint64_t kernel_refresh_interval_ns() const override { + const auto interval = demo_.scene->frame_status().next_refresh_interval_ns; + return interval == 0 ? 16'666'667U : interval; + } + void set_client_metrics(Gallery_Client_Performance metrics) noexcept override { + client_performance_ = metrics; + } + [[nodiscard]] adminive::Update_Result apply_patch( + std::string_view target, const nlohmann::json&) override { + adminive::Update_Result result; + result.message = "point_3d has no patchable control: " + std::string(target); + return result; + } + void resize(Size size) override { + if (size.width <= 0 || size.height <= 0) + return; + demo_.scene->resize({static_cast(size.width), + static_cast(size.height)}); + rendered_since_last_pixel_ = false; + } + void dispatch(const Event& event) override { + if (event.type == Event_Type::Show) + active_ = true; + else if (event.type == Event_Type::Hide) + active_ = false; + demo_.scene->dispatch(event); + } + void dispatch(const Pointer_Event& event) override { + if (active_) + demo_.scene->dispatch(event); + } + void dispatch(const Wheel_Event& event) override { + if (active_) + demo_.scene->dispatch(event); + } + void dispatch(const Key_Event& event) override { + if (active_) + demo_.scene->dispatch(event); + } + bool render_latest_frame() override { + if (!active_) + return false; + if (can_render_automatically()) { + phase_ += 0.045F; + publish_demo_points(); + } + return record_render([this] { return demo_.scene->request_frame(); }); + } + [[nodiscard]] std::optional encode_latest_pixels() override { + if (!active_) + return std::nullopt; + if (!rendered_since_last_pixel_ && !can_render_automatically() && + !render_latest_frame()) + return std::nullopt; + const auto frame = demo_.scene->latest_frame(); + if (!frame) + return std::nullopt; + rendered_since_last_pixel_ = false; + auto encoded = encode_rgba8_pixel_frame( + frame->rgba8.data(), frame->extent.width, frame->extent.height, + static_cast(frame->extent.width) * 4U); + return encoded.empty() ? std::nullopt + : std::optional(std::move(encoded)); + } + void record_pixel_response( + std::chrono::steady_clock::time_point request_started, + std::chrono::steady_clock::time_point encode_started, + std::chrono::steady_clock::time_point encode_finished, + std::size_t pixel_bytes) override { + last_pixel_encode_ms_ = std::chrono::duration( + encode_finished - encode_started) + .count(); + last_pixel_request_ms_ = std::chrono::duration( + encode_finished - request_started) + .count(); + last_pixel_bytes_ = pixel_bytes; + pixel_encode_statistics_.add(last_pixel_encode_ms_); + pixel_request_statistics_.add(last_pixel_request_ms_); + } + [[nodiscard]] std::string action(const Gallery_Action_Request& request, + bool& recognized) override { + recognized = true; + if (request.id == "mode_prepare") { + const bool prepared = demo_.scene->prepare_frame(); + last_action_result_ = prepared ? "prepared" : "prepare_rejected"; + return prepared ? "Point frame prepared" : "Point frame prepare rejected"; + } + if (request.id == "mode_refresh") { + const bool refreshed = demo_.scene->refresh_manual_frame(); + last_action_result_ = refreshed ? "refresh_succeeded" : "refresh_failed"; + return refreshed ? "Point manual frame refreshed" : "No manual frame to refresh"; + } + if (request.id == "mode_render" || request.id == "mode_dequeue") { + const bool rendered = record_render( + [this] { return demo_.scene->render_prepared_frame(); }); + last_action_result_ = rendered ? "rendered" : "render_queue_empty"; + return rendered ? "Point frame rendered" : "No Point frame available"; + } + if (request.id == "mode_discard") { + const bool discarded = demo_.scene->discard_pending_frame(); + last_action_result_ = discarded ? "manually_discarded" : "discard_failed"; + return discarded ? "Pending Point frame discarded" : "No Point frame to discard"; + } + if (request.id == "mode_enqueue") { + phase_ += 0.05F; + publish_demo_points(); + const bool enqueued = demo_.scene->prepare_frame(); + last_action_result_ = enqueued ? "enqueued" : "enqueue_failed"; + return enqueued ? "Point playback frame enqueued" : "Point enqueue failed"; + } + if (request.id == "orbit_points") { + phase_ += 0.25F; + publish_demo_points(); + last_action_result_ = "points_orbited"; + rendered_since_last_pixel_ = false; + return "Point positions updated through Latest_Real_Time_Data"; + } + if (request.id == "add_point") { + auto points = current_points(); + points.push_back({{0.12F, -0.72F, 0.58F}, {55, 235, 230, 255}, 46.0F}); + demo_.points->update(std::move(points)); + last_action_result_ = "point_added"; + rendered_since_last_pixel_ = false; + return "Point added to the bulk payload"; + } + if (request.id == "remove_point") { + auto points = current_points(); + if (points.size() > render_3d::point_demo_data().size()) { + points.pop_back(); + demo_.points->update(std::move(points)); + last_action_result_ = "point_removed"; + rendered_since_last_pixel_ = false; + return "Last added Point removed"; + } + last_action_result_ = "no_added_point"; + return "No added Point to remove"; + } + if (request.id == "point_churn") { + auto stable = current_points(); + auto transient = stable; + transient.push_back({{0.0F, 0.0F, 0.8F}, {255, 255, 255, 255}, 72.0F}); + demo_.points->update(std::move(transient)); + demo_.points->update(std::move(stable)); + last_action_result_ = "point_churned"; + rendered_since_last_pixel_ = false; + return "Transient Point added and removed before the next frame snapshot"; + } + if (request.id == "reset_points") { + phase_ = 0.0F; + publish_demo_points(); + last_action_result_ = "points_reset"; + rendered_since_last_pixel_ = false; + return "Point demo payload reset"; + } + recognized = false; + return {}; + } + void record_action_notice_if_empty(std::string_view notice) override { + if (last_action_result_.empty()) + last_action_result_ = notice; + } + [[nodiscard]] std::string telemetry_json() const override { + const auto status = demo_.scene->frame_status(); + const auto frame = demo_.scene->latest_frame(); + const auto points = current_points(); + const auto render_window = render_duration_statistics_.snapshot(); + const auto encode_window = pixel_encode_statistics_.snapshot(); + const auto request_window = pixel_request_statistics_.snapshot(); + const double elapsed = std::max( + std::chrono::duration(std::chrono::steady_clock::now() - + performance_started_) + .count(), + 1e-9); + Gallery_Render_Performance performance; + performance.render_attempt_count = render_attempt_count_; + performance.successful_render_count = successful_render_count_; + performance.failed_render_count = render_attempt_count_ - successful_render_count_; + performance.lifetime_average_fps = successful_render_count_ / elapsed; + performance.last_render_ms = last_render_ms_; + performance.average_render_ms = render_window.average; + performance.maximum_render_ms = render_window.maximum; + performance.render_deviation_ms = render_window.deviation; + performance.render_p50_ms = render_window.p50; + performance.render_p95_ms = render_window.p95; + performance.render_p99_ms = render_window.p99; + performance.render_sample_count = render_window.sample_count; + performance.last_pixel_encode_ms = last_pixel_encode_ms_; + performance.average_pixel_encode_ms = encode_window.average; + performance.maximum_pixel_encode_ms = encode_window.maximum; + performance.pixel_encode_deviation_ms = encode_window.deviation; + performance.pixel_encode_p50_ms = encode_window.p50; + performance.pixel_encode_p95_ms = encode_window.p95; + performance.pixel_encode_p99_ms = encode_window.p99; + performance.pixel_encode_sample_count = encode_window.sample_count; + performance.last_pixel_request_ms = last_pixel_request_ms_; + performance.average_pixel_request_ms = request_window.average; + performance.pixel_request_deviation_ms = request_window.deviation; + performance.pixel_request_p95_ms = request_window.p95; + performance.pixel_request_p99_ms = request_window.p99; + performance.last_pixel_bytes = last_pixel_bytes_; + performance.automatic_low_latency_scheduler = can_render_automatically(); + + const auto extent = frame ? frame->extent : render_3d::Extent{560, 320}; + nlohmann::json telemetry{ + {"case", case_id_}, + {"frame_mode", gallery_enum_id(mode_)}, + {"frame_index", status.latest_sequence}, + {"viewport", {{"width", extent.width}, {"height", extent.height}}}, + {"view_active", active_}, + {"kernel_frame_count", status.consumed_frame_count}, + {"performance", adminive::to_frontend_json(performance)}, + {"kernel_observer", + {{"mode", gallery_enum_id(mode_)}, + {"last_event", last_action_result_.empty() + ? (frame ? "rendered" : "none") + : last_action_result_}, + {"frequency_hz", status.frequency_hz}, + {"produced_frame_count", status.produced_frame_count}, + {"consumed_frame_count", status.consumed_frame_count}, + {"dropped_frame_count", status.dropped_frame_count}, + {"failed_operation_count", status.failed_operation_count}, + {"pending_frame_count", status.pending_frame_count}, + {"latest_sequence", status.latest_sequence}, + {"next_refresh_interval_ns", status.next_refresh_interval_ns}}}, + {"consumer_feedback", nlohmann::json::object()}, + {"client_performance", + adminive::to_frontend_json(client_performance_)}, + {"renderable_observers", nlohmann::json::array()}, + {"performance_capture", nullptr}, + {"last_action_result", last_action_result_}, + {"point_3d", {{"point_count", points.size()}, + {"data_revision", demo_.points->revision()}}}, + {"data_shape", {{"input_elements", points.size()}, + {"rendered_elements", points.size()}}}} + ; + return telemetry.dump(); + } + +private: + [[nodiscard]] std::vector current_points() const { + return demo_.points->snapshot().value_or(std::vector{}); + } + void publish_demo_points() { + demo_.points->update(render_3d::point_demo_data(phase_)); + } + template + bool record_render(Render&& render) { + ++render_attempt_count_; + const auto started = std::chrono::steady_clock::now(); + const bool rendered = std::invoke(std::forward(render)); + last_render_ms_ = std::chrono::duration( + std::chrono::steady_clock::now() - started) + .count(); + render_duration_statistics_.add(last_render_ms_); + successful_render_count_ += rendered ? 1U : 0U; + rendered_since_last_pixel_ = rendered; + return rendered; + } + + std::string case_id_{"point_3d"}; + Gallery_Frame_Mode mode_; + bool automatic_low_latency_{}; + bool active_{true}; + std::chrono::steady_clock::time_point performance_started_; + render_3d::Point_Demo demo_; + float phase_{}; + std::uint64_t render_attempt_count_{}; + std::uint64_t successful_render_count_{}; + double last_render_ms_{}; + Rolling_Statistics render_duration_statistics_{256}; + double last_pixel_encode_ms_{}; + double last_pixel_request_ms_{}; + std::size_t last_pixel_bytes_{}; + Rolling_Statistics pixel_encode_statistics_{256}; + Rolling_Statistics pixel_request_statistics_{256}; + Gallery_Client_Performance client_performance_; + bool rendered_since_last_pixel_{}; + std::string last_action_result_; +}; + std::unique_ptr make_gallery_scene(std::uint64_t session_id, std::string case_id, Gallery_Frame_Mode mode, bool automatic_low_latency) { + if (case_id == "point_3d") + return std::make_unique(mode, automatic_low_latency); switch (mode) { case Gallery_Frame_Mode::Manual: return std::make_unique>(session_id, std::move(case_id), mode, automatic_low_latency); diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index e5e53be..b6e6258 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -53,6 +53,8 @@ const std::vector& cases() { "鼠标框选、多区域保留、标注样式和清空。", 70}, {"constellation", "星座图", "Constellation_Diagram", "点图控件", "PSK4/PSK8/PSK16 模式、相位、寿命、坐标范围和方形拟合。", 80} + ,{"point_3d", "Datoviz 3D 点组件", "Point Visual", "3D", + "Kernel 状态双缓冲、实时批量点数据、单线程 Datoviz 渲染与 RGBA 回读。", 90} }; return value; } @@ -93,6 +95,39 @@ std::size_t session_control_count(Gallery_Frame_Mode frame_mode) { std::vector registered_actions(std::string_view case_id, Gallery_Frame_Mode frame_mode) { std::vector result; + if (case_id == "point_3d") { + const auto keep_frame_action = [frame_mode](std::string_view id) { + switch (frame_mode) { + case Gallery_Frame_Mode::Manual: + return id == "mode_prepare" || id == "mode_refresh" || + id == "mode_render" || id == "mode_discard"; + case Gallery_Frame_Mode::Low_Latency: + return id == "mode_prepare" || id == "mode_render" || + id == "mode_discard"; + case Gallery_Frame_Mode::Playback: + return id == "mode_enqueue" || id == "mode_dequeue"; + } + return false; + }; + for (auto& action : gallery_frame_actions(frame_mode)) { + if (keep_frame_action(action.id)) + result.push_back(std::move(action)); + } + const auto add = [&result](std::string id, std::string label, + std::string api, std::string description) { + result.push_back({std::move(id), std::move(label), std::move(api), + std::move(description), "Point Visual", {}, {}, 0.0, + true}); + }; + add("orbit_points", "移动点", "Point_Data::update", + "通过 Kernel Latest_Real_Time_Data 发布一批新位置"); + add("add_point", "添加点", "Point_Data::update", "向批量点数据追加一个点"); + add("remove_point", "移除点", "Point_Data::update", "移除最后追加的点"); + add("point_churn", "点增删压力", "Point_Data::update", + "在帧快照前连续追加并移除临时点"); + add("reset_points", "重置点", "Point_Data::update", "恢复 3D 点演示数据"); + return result; + } append_session_actions(frame_mode, result); for (auto& action : gallery_frame_actions(frame_mode)) result.push_back(std::move(action)); @@ -520,8 +555,9 @@ std::string Gallery_Protocol::catalog_json() { const std::size_t renderable_controls = gallery_renderable_control_count(item.id); for (const auto mode : frame_modes) { const std::string key = gallery_enum_id(mode); - const std::size_t controls = - gallery_detail::session_control_count(mode) + renderable_controls; + const std::size_t controls = item.id == "point_3d" + ? 0U + : gallery_detail::session_control_count(mode) + renderable_controls; const std::size_t actions = gallery_detail::registered_actions(item.id, mode).size(); entry["control_count_by_mode"][key] = controls; @@ -529,9 +565,10 @@ std::string Gallery_Protocol::catalog_json() { controls_total += controls; actions_total += actions; } - entry["control_count"] = - gallery_detail::session_control_count(Gallery_Frame_Mode::Low_Latency) + - renderable_controls; + entry["control_count"] = item.id == "point_3d" + ? 0U + : gallery_detail::session_control_count(Gallery_Frame_Mode::Low_Latency) + + renderable_controls; entry["action_count"] = gallery_detail::registered_actions( item.id, Gallery_Frame_Mode::Low_Latency).size(); diff --git a/web_server/app/Pixel_Frame.cpp b/web_server/app/Pixel_Frame.cpp index 20d5758..32a2cd9 100644 --- a/web_server/app/Pixel_Frame.cpp +++ b/web_server/app/Pixel_Frame.cpp @@ -1,6 +1,7 @@ #include "Pixel_Frame.h" #include +#include #include #if defined(_M_X64) || defined(__x86_64__) #include @@ -110,4 +111,35 @@ std::string encode_pixel_frame(Image_View image, Color background) { return frame; } +std::string encode_rgba8_pixel_frame(const std::byte* pixels, + std::uint32_t width, + std::uint32_t height, + std::size_t stride) { + constexpr std::size_t bytes_per_pixel = 4; + if (pixels == nullptr || width == 0 || height == 0) + return {}; + if (width > std::numeric_limits::max() / bytes_per_pixel) + return {}; + const std::size_t row_bytes = static_cast(width) * bytes_per_pixel; + if (stride < row_bytes || + height > (std::numeric_limits::max() - pixel_frame_header_size) / + row_bytes) + return {}; + + std::string frame(pixel_frame_header_size + row_bytes * height, '\0'); + frame[0] = 'R'; + frame[1] = 'V'; + frame[2] = 'P'; + frame[3] = '1'; + write_u32_le(frame.data() + 4, width); + write_u32_le(frame.data() + 8, height); + write_u32_le(frame.data() + 12, static_cast(row_bytes)); + char* output = frame.data() + pixel_frame_header_size; + for (std::uint32_t row = 0; row < height; ++row) { + std::memcpy(output + static_cast(row) * row_bytes, + pixels + static_cast(row) * stride, row_bytes); + } + return frame; +} + } // namespace renderive::web diff --git a/web_server/app/Pixel_Frame.h b/web_server/app/Pixel_Frame.h index 91de4b3..e4723f5 100644 --- a/web_server/app/Pixel_Frame.h +++ b/web_server/app/Pixel_Frame.h @@ -1,9 +1,14 @@ #pragma once #include "render_2D/base/Types.h" #include +#include #include namespace renderive::web { inline constexpr std::size_t pixel_frame_header_size = 16; [[nodiscard]] std::string encode_pixel_frame(Image_View image, Color background = Color::black()); +[[nodiscard]] std::string encode_rgba8_pixel_frame(const std::byte* pixels, + std::uint32_t width, + std::uint32_t height, + std::size_t stride); } // namespace renderive::web diff --git a/web_server/tests/Datoviz_Gallery_Tests.cpp b/web_server/tests/Datoviz_Gallery_Tests.cpp index eab06a5..b9ce5d6 100644 --- a/web_server/tests/Datoviz_Gallery_Tests.cpp +++ b/web_server/tests/Datoviz_Gallery_Tests.cpp @@ -34,7 +34,7 @@ Gallery_Request gallery_request(Gallery_Request_Kind kind, std::string message) std::string open_message(std::string_view mode) { return std::string( - R"({"category":"event","type":"gallery_open","case":"datoviz_3d","frame_mode":")") + + R"({"category":"event","type":"gallery_open","case":"point_3d","frame_mode":")") + std::string(mode) + R"("})"; } @@ -181,7 +181,7 @@ TEST(RenderiveWebDatovizGallery, CatalogUsesOnlyThreeDimensionalActions) { const auto catalog = nlohmann::json::parse(Gallery_Protocol::catalog_json()); const auto item = std::find_if( catalog.at("cases").begin(), catalog.at("cases").end(), - [](const nlohmann::json& value) { return value.at("id") == "datoviz_3d"; }); + [](const nlohmann::json& value) { return value.at("id") == "point_3d"; }); ASSERT_NE(item, catalog.at("cases").end()); EXPECT_EQ(item->at("category"), "3D"); EXPECT_EQ(item->at("control_count"), 0); @@ -193,11 +193,11 @@ TEST(RenderiveWebDatovizGallery, CatalogUsesOnlyThreeDimensionalActions) { R"({"resources":[],"observers":[],"render_plan":null,"performance_capture":null})"; const auto state_for = [&](Gallery_Frame_Mode mode) { return nlohmann::json::parse(Gallery_Protocol::case_json_from_controls( - "datoviz_3d", empty_controls, "{}", {}, mode, false)); + "point_3d", empty_controls, "{}", {}, mode, false)); }; const std::set common{ - "add_sphere", "move_sphere", "remove_sphere", "reset_camera", - "sphere_churn", "toggle_projection"}; + "add_point", "orbit_points", "remove_point", "point_churn", + "reset_points"}; auto expected = common; expected.insert({"mode_prepare", "mode_refresh", "mode_render", "mode_discard"}); EXPECT_EQ(action_ids(state_for(Gallery_Frame_Mode::Manual)), expected); @@ -239,12 +239,39 @@ TEST(RenderiveWebDatovizGallery, AutomaticLowLatencyFramesCarryAnimatedState) { expect_actual_datoviz_pixels(later, 560, 320); } +TEST(RenderiveWebDatovizGallery, WebPointerEventsReachTheDatovizController) { + Gallery_Plot_Session session; + ASSERT_EQ(open_scene(session, "low_latency").at("type"), "case_state"); + const std::string before = request_pixels(session); + + Pointer_Event press(Event_Type::Pointer_Press); + press.position = {210.0, 135.0}; + press.button = Mouse_Button::Left; + EXPECT_FALSE(session.handle(Web_Event{press}).has_value()); + + Pointer_Event move(Event_Type::Pointer_Move); + move.position = {330.0, 190.0}; + move.buttons = 1; + EXPECT_FALSE(session.handle(Web_Event{move}).has_value()); + move.position = {390.0, 225.0}; + EXPECT_FALSE(session.handle(Web_Event{move}).has_value()); + + Pointer_Event release(Event_Type::Pointer_Release); + release.position = move.position; + release.button = Mouse_Button::Left; + EXPECT_FALSE(session.handle(Web_Event{release}).has_value()); + + const std::string after = request_pixels(session); + expect_actual_datoviz_pixels(after, 560, 320); + EXPECT_NE(after, before); +} + TEST(RenderiveWebDatovizGallery, ManualRenderUsesThePreparedLogicalState) { Gallery_Plot_Session session; ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "move_sphere").at("type"), "case_state"); + ASSERT_EQ(invoke_action(session, "orbit_points").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); - ASSERT_EQ(invoke_action(session, "move_sphere").at("type"), "case_state"); + ASSERT_EQ(invoke_action(session, "orbit_points").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state"); ASSERT_EQ(invoke_action(session, "mode_render").at("type"), "case_state"); const std::string prepared = request_pixels(session); @@ -288,7 +315,7 @@ TEST(RenderiveWebDatovizGallery, AddedAndChurnedVisualsAreActuallyRemoved) { ASSERT_EQ(open_scene(session, "manual").at("type"), "case_state"); const std::string baseline = request_pixels(session); - ASSERT_EQ(invoke_action(session, "add_sphere").at("type"), "case_state"); + ASSERT_EQ(invoke_action(session, "add_point").at("type"), "case_state"); manual_render_cycle(session); const std::string added = request_pixels(session); EXPECT_NE(added, baseline); @@ -314,23 +341,24 @@ TEST(RenderiveWebDatovizGallery, AddedAndChurnedVisualsAreActuallyRemoved) { const auto green_y = centroid_y([](unsigned red, unsigned green, unsigned blue) { return green > red + 20U && green > blue + 20U; }); - const auto yellow_y = centroid_y([](unsigned red, unsigned green, unsigned blue) { - return red > 100U && green > 100U && blue < 100U; + const auto cyan_y = centroid_y([](unsigned red, unsigned green, unsigned blue) { + return green > red + 50U && blue > red + 50U && + std::abs(static_cast(green) - static_cast(blue)) < 30; }); ASSERT_TRUE(green_y.has_value()); - ASSERT_TRUE(yellow_y.has_value()); - EXPECT_LT(*yellow_y, *green_y) + ASSERT_TRUE(cyan_y.has_value()); + EXPECT_GT(*cyan_y, *green_y) << "Frame_3D_View must expose top-left-origin RGBA rows"; - ASSERT_EQ(invoke_action(session, "remove_sphere").at("type"), "case_state"); + ASSERT_EQ(invoke_action(session, "remove_point").at("type"), "case_state"); manual_render_cycle(session); const std::string removed = request_pixels(session); EXPECT_EQ(removed, baseline); - const auto churned = invoke_action(session, "sphere_churn"); + const auto churned = invoke_action(session, "point_churn"); ASSERT_EQ(churned.at("type"), "case_state"); EXPECT_EQ(churned.at("telemetry").at("kernel_observer").at("last_event"), - "sphere_churned"); + "point_churned"); const std::string after_churn = request_pixels(session); expect_actual_datoviz_pixels(after_churn, 560, 320); EXPECT_EQ(after_churn, baseline); diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index bda08e8..5083e2a 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -228,10 +228,10 @@ TEST(RenderiveWebGallery, AdminiveCatalogCoversEveryControlCaseAndThreeModes) { EXPECT_EQ(catalog.at("protocol"), "renderive.control-gallery"); EXPECT_EQ(catalog.at("protocol_version"), 4); EXPECT_EQ(catalog.at("case_descriptor").at("protocol"), "adminive.resource"); - EXPECT_EQ(catalog.at("cases").size(), 8U); + EXPECT_EQ(catalog.at("cases").size(), 9U); EXPECT_EQ(catalog.at("frame_modes").size(), 3U); EXPECT_EQ(catalog.at("coverage").at("page_count"), 3); - EXPECT_EQ(catalog.at("coverage").at("canvas_count"), 24); + EXPECT_EQ(catalog.at("coverage").at("canvas_count"), 27); EXPECT_GT(catalog.at("coverage").at("manual_control_count").get(), 180U); EXPECT_GT(catalog.at("coverage").at("manual_action_count").get(), 50U); EXPECT_EQ(catalog.at("coverage").at("frequency_modes"), diff --git a/webapp_gallery/src/plot/plot_canvas.tsx b/webapp_gallery/src/plot/plot_canvas.tsx index 308f888..7546626 100644 --- a/webapp_gallery/src/plot/plot_canvas.tsx +++ b/webapp_gallery/src/plot/plot_canvas.tsx @@ -1,3 +1,132 @@ -import {Box} from "@mui/material";import {useEffect,useRef} from "react";import type {Gallery_Plot_Session} from "../session/gallery_plot_session";import {use_element_size} from "../hooks/use_element_size"; -function modifiers(event: {shiftKey:boolean;ctrlKey:boolean;altKey:boolean;metaKey:boolean}): number {return(event.shiftKey?1:0)|(event.ctrlKey?2:0)|(event.altKey?4:0)|(event.metaKey?8:0);} -export function Plot_Canvas({session}:{session:Gallery_Plot_Session}) {const shell_ref=useRef(null),canvas_ref=useRef(null);const size=use_element_size(shell_ref);useEffect(()=>{const canvas=canvas_ref.current;if(!canvas)return;session.attach_canvas(canvas);return()=>session.detach_canvas();},[session]);useEffect(()=>{if(size.width&&size.height)session.resize(size.width,size.height);},[session,size]);const position=(event:React.PointerEvent|React.WheelEvent)=>{const canvas=canvas_ref.current!;const rect=canvas.getBoundingClientRect();return{x:(event.clientX-rect.left)*canvas.width/Math.max(1,rect.width),y:(event.clientY-rect.top)*canvas.height/Math.max(1,rect.height)};};const pointer=(type:"pointer_move"|"pointer_press"|"pointer_release",event:React.PointerEvent)=>session.pointer(type,{...position(event),button:["left","middle","right"][event.button]??"none",buttons:event.buttons,modifiers:modifiers(event)});return session.key("key_press",{key:event.key,nativeKey:event.keyCode,repeat:event.repeat,modifiers:modifiers(event)})} onKeyUp={event=>session.key("key_release",{key:event.key,nativeKey:event.keyCode,repeat:false,modifiers:modifiers(event)})}>pointer("pointer_move",event)} onPointerDown={event=>{if(event.button!==2){shell_ref.current?.focus();event.currentTarget.setPointerCapture(event.pointerId);pointer("pointer_press",event);}}} onPointerUp={event=>event.button!==2&&pointer("pointer_release",event)} onPointerLeave={()=>session.leave()} onWheel={event=>{event.preventDefault();session.wheel({...position(event),pixelDeltaX:event.deltaX,pixelDeltaY:event.deltaY,angleDeltaX:-event.deltaX*8,angleDeltaY:-event.deltaY*8,buttons:event.buttons,modifiers:modifiers(event)});}}/>;} +import {Box} from "@mui/material"; +import {useEffect, useRef} from "react"; + +import {use_element_size} from "../hooks/use_element_size"; +import type {Gallery_Plot_Session} from "../session/gallery_plot_session"; + +function modifiers(event: { + shiftKey: boolean; + ctrlKey: boolean; + altKey: boolean; + metaKey: boolean; +}): number { + return (event.shiftKey ? 1 : 0) | + (event.ctrlKey ? 2 : 0) | + (event.altKey ? 4 : 0) | + (event.metaKey ? 8 : 0); +} + +function canvas_position( + canvas: HTMLCanvasElement, + event: {clientX: number; clientY: number}, +): {x: number; y: number} { + const rect = canvas.getBoundingClientRect(); + return { + x: (event.clientX - rect.left) * canvas.width / Math.max(1, rect.width), + y: (event.clientY - rect.top) * canvas.height / Math.max(1, rect.height), + }; +} + +function angle_delta(delta: number, delta_mode: number): number { + // Kernel wheel events use Qt-compatible eighth-degree units. Browser pixel wheels commonly + // report about 100 px per notch, while line/page devices report much smaller logical deltas. + const eighth_degrees_per_unit = delta_mode === 1 ? 40 : delta_mode === 2 ? 120 : 1.2; + return -delta * eighth_degrees_per_unit; +} + +export function Plot_Canvas({session}: {session: Gallery_Plot_Session}) { + const shell_ref = useRef(null); + const canvas_ref = useRef(null); + const size = use_element_size(shell_ref); + + useEffect(() => { + const canvas = canvas_ref.current; + if (!canvas) + return; + + const wheel = (event: WheelEvent) => { + event.preventDefault(); + session.wheel({ + ...canvas_position(canvas, event), + pixelDeltaX: event.deltaX, + pixelDeltaY: event.deltaY, + angleDeltaX: angle_delta(event.deltaX, event.deltaMode), + angleDeltaY: angle_delta(event.deltaY, event.deltaMode), + buttons: event.buttons, + modifiers: modifiers(event), + }); + }; + + session.attach_canvas(canvas); + canvas.addEventListener("wheel", wheel, {passive: false}); + return () => { + canvas.removeEventListener("wheel", wheel); + session.detach_canvas(); + }; + }, [session]); + + useEffect(() => { + if (size.width && size.height) + session.resize(size.width, size.height); + }, [session, size]); + + const pointer = ( + type: "pointer_move" | "pointer_press" | "pointer_release", + event: React.PointerEvent, + ) => { + const canvas = canvas_ref.current; + if (!canvas) + return; + session.pointer(type, { + ...canvas_position(canvas, event), + button: ["left", "middle", "right"][event.button] ?? "none", + buttons: event.buttons, + modifiers: modifiers(event), + }); + }; + + return session.key("key_press", { + key: event.key, + nativeKey: event.keyCode, + repeat: event.repeat, + modifiers: modifiers(event), + })} + onKeyUp={event => session.key("key_release", { + key: event.key, + nativeKey: event.keyCode, + repeat: false, + modifiers: modifiers(event), + })} + > + pointer("pointer_move", event)} + onPointerDown={event => { + if (event.button === 2) + return; + shell_ref.current?.focus(); + event.currentTarget.setPointerCapture(event.pointerId); + pointer("pointer_press", event); + }} + onPointerUp={event => { + if (event.button !== 2) + pointer("pointer_release", event); + }} + onPointerLeave={() => session.leave()} + /> + ; +}