删除realtime 的一个组件

This commit is contained in:
2026-08-14 12:27:12 +08:00
parent 38231dbc1c
commit dfb2de6bf7
24 changed files with 3972 additions and 421 deletions
@@ -3,6 +3,7 @@
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <mutex>
#include <optional>
#include <tuple>
@@ -11,7 +12,6 @@
#include "renderive/base/Atomic_Mutex.hpp"
#include "renderive/base/Concepts.hpp"
#include "renderive/base/observer/Observer.hpp"
#include "base/State_Strategy_Base.hpp"
template <class Tag_Type, class Data_Type>
struct Buffered_Data {
using Tag = Tag_Type;
@@ -56,71 +56,14 @@ struct Entry_Index {
};
template <class Tag, class... Entries>
using Entry_Of = std::tuple_element_t<Entry_Index<Tag, Entries...>::value, std::tuple<Entries...>>;
}
template <Derivable_Type That, class Data_Type, Mutex_Type Mutex = Atomic_Spin_Mutex, class Observer = Observer_State<>>
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<Observer, Observation>);
Double_Buffer_Strategy() requires std::default_initializable<That> && std::default_initializable<Data> : That() {}
explicit Double_Buffer_Strategy(With_Observer<Observer> option) requires std::default_initializable<That> && std::default_initializable<Data> : That(), observer(std::move(option.observer)) {}
template <class... Args>
explicit Double_Buffer_Strategy(std::in_place_t, Args&&... args) requires std::default_initializable<Data> : That(std::forward<Args>(args)...) {}
template <class... Args>
Double_Buffer_Strategy(std::in_place_t, With_Observer<Observer> option, Args&&... args) requires std::default_initializable<Data> : That(std::forward<Args>(args)...), observer(std::move(option.observer)) {}
Self& write(Data value) {
std::optional<Observation> 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> 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<Data> slot;
mutable Mutex mtx;
struct No_Publish_Effect {
void operator()() const noexcept {}
};
}
template <Derivable_Type That, class Layout, Mutex_Type Mutex = Atomic_Spin_Mutex, class Observer = Observer_State<>>
struct Multi_Double_Buffer_Strategy;
template <Derivable_Type That, Mutex_Type Mutex, class Observer, Buffered_Data_Entry... Entries>
struct Multi_Double_Buffer_Strategy<That, Double_Buffer_Layout<Entries...>, Mutex, Observer> : That, State_Strategy_Base {
struct Multi_Double_Buffer_Strategy<That, Double_Buffer_Layout<Entries...>, Mutex, Observer> : That {
using Self = Multi_Double_Buffer_Strategy;
enum class Observation_Event {
cache_updated,
@@ -163,24 +106,31 @@ struct Multi_Double_Buffer_Strategy<That, Double_Buffer_Layout<Entries...>, Mute
observer.observe(*observation);
return *this;
}
void publish() override {
template <class Side_Effect = double_buffer_detail::No_Publish_Effect>
requires std::invocable<Side_Effect>
bool publish(Side_Effect&& side_effect = {}) {
std::array<Observation, sizeof...(Entries)> observations{};
std::size_t observation_count{};
bool published{};
{
std::lock_guard lock(mtx);
bool published{};
(publish_entry<Entries>(observations, observation_count, published), ...);
if(published) {
++revision;
++publish_revision;
}
}
for(std::size_t i = 0; i < observation_count; ++i) {
observer.observe(observations[i]);
}
if(!published) {
return false;
}
std::invoke(std::forward<Side_Effect>(side_effect));
return true;
}
std::uint64_t state_revision() const override {
std::uint64_t revision() const {
std::lock_guard lock(mtx);
return revision;
return publish_revision;
}
template <class Tag>
std::uint64_t buffer_revision() const {
@@ -217,6 +167,6 @@ private:
}
Observer observer;
std::tuple<double_buffer_detail::Slot<typename Entries::Data>...> slots;
std::uint64_t revision{};
std::uint64_t publish_revision{};
mutable Mutex mtx;
};
@@ -6,11 +6,53 @@
#include <mutex>
#include <thread>
#include <vector>
#include "renderive/real_time_data/Double_Buffer_Strategy.hpp"
#include "renderive/real_time_data/Real_Time_Data.hpp"
#include "renderive/renderable/Renderable.hpp"
#include "renderive/renderable/Renderable_Test_Harness.hpp"
#include "renderive/scene/Scene.hpp"
#include "renderive/scene/Scene_Test_Helpers.hpp"
#include "renderive/state/base/State_Strategy_Base.hpp"
namespace {
struct Double_Buffer_Test_Base {};
struct Double_Buffer_Test_Tag {};
using Double_Buffer_Test_Layout =
Double_Buffer_Layout<Buffered_Data<Double_Buffer_Test_Tag,
std::vector<int>>>;
class Double_Buffer_Test_Strategy final
: public Multi_Double_Buffer_Strategy<Double_Buffer_Test_Base,
Double_Buffer_Test_Layout,
std::mutex> {
using Base = Multi_Double_Buffer_Strategy<Double_Buffer_Test_Base,
Double_Buffer_Test_Layout,
std::mutex>;
public:
using Base::buffer_revision;
using Base::publish;
using Base::revision;
using Base::write;
[[nodiscard]] const std::vector<int>& published() const noexcept {
return Base::render_buffer_value<Double_Buffer_Test_Tag>();
}
};
static_assert(!std::derived_from<Double_Buffer_Test_Strategy,
State_Strategy_Base>);
}
TEST(double_buffer_strategy_test,
single_entry_layout_publishes_with_external_side_effect) {
Double_Buffer_Test_Strategy strategy;
int side_effect_count{};
strategy.write<Double_Buffer_Test_Tag>({1, 2, 3});
EXPECT_TRUE(strategy.publish([&] { ++side_effect_count; }));
EXPECT_EQ(strategy.published(), (std::vector<int>{1, 2, 3}));
EXPECT_EQ(strategy.revision(), 1U);
EXPECT_EQ(strategy.buffer_revision<Double_Buffer_Test_Tag>(), 1U);
EXPECT_EQ(side_effect_count, 1);
EXPECT_FALSE(strategy.publish([&] { ++side_effect_count; }));
EXPECT_EQ(side_effect_count, 1);
}
struct Real_Time_Data_Test_Time_Source {
std::shared_ptr<std::atomic<std::uint64_t>> time_ns{std::make_shared<std::atomic<std::uint64_t>>()};
std::uint64_t now_ns() const noexcept {
File diff suppressed because it is too large Load Diff
@@ -3,23 +3,12 @@
#include <renderive/base/observer/Observer.hpp>
#include <renderive/real_time_data/Frame_Strategy_Observer.hpp>
#include <renderive/real_time_data/History_Real_Time_Data.hpp>
#include <renderive/real_time_data/Latest_Real_Time_Data.hpp>
#include <mutex>
namespace renderive::detail {
using Plottable_Real_Time_Data_Observer = Observer_State<Frame_Strategy_Real_Time_Data_Observer>;
inline Plottable_Real_Time_Data_Observer observe_real_time_data(Renderable& renderable) {
return Plottable_Real_Time_Data_Observer(Frame_Strategy_Real_Time_Data_Observer(renderable));
}
template <class Value>
class Plottable_Latest_Real_Time_Data
: public Latest_Real_Time_Data<Value, std::mutex, Plottable_Real_Time_Data_Observer> {
using Base = Latest_Real_Time_Data<Value, std::mutex, Plottable_Real_Time_Data_Observer>;
public:
explicit Plottable_Latest_Real_Time_Data(Renderable& owner)
: Base(observe_real_time_data(owner)), binding_(this->bind_renderable(owner)) {}
private:
Real_Time_Data_Binding binding_;
};
template <class Value, class Container>
class Plottable_History_Real_Time_Data
: public History_Real_Time_Data<Value, Container, std::mutex, Plottable_Real_Time_Data_Observer> {
+61 -56
View File
@@ -1,21 +1,14 @@
#include "Spectrum.h"
#include "Curve_Sampling.h"
#include "Plottable_Real_Time_Data.h"
#include "../render/Blend2D_Cache.h"
#include "../renderable/Render_Partition.h"
#include "../renderable/Renderable_p.h"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <optional>
#include <sstream>
namespace renderive::detail {
namespace {
struct Spectrum_Frame {
std::vector<double> samples;
std::vector<double> maxima;
std::vector<double> minima;
};
struct Spectrum_Interaction_Base {};
struct Spectrum_Interaction {
std::vector<double> markers;
@@ -49,7 +42,7 @@ struct Spectrum_Extreme_Buffer {
};
struct Spectrum_Prepare_Buffer {
Spectrum_Properties properties;
Spectrum_Frame frame;
const Spectrum_Frame* frame{};
Spectrum_Interaction interaction;
Axis_Transform frequency_axis;
Axis_Transform power_axis;
@@ -145,53 +138,62 @@ struct Spectrum_Control::Impl : Renderable::Impl {
power_axis(std::move(power)) {}
renderive_Owner<Frequency_Axis> frequency_axis;
renderive_Owner<Axis> power_axis;
std::optional<Plottable_Latest_Real_Time_Data<Spectrum_Frame>> frame;
Spectrum_Interaction_State interaction;
std::mutex frame_update_mutex;
Adaptive_Render_Partitioner partitioner;
Spectrum_Prepare_Buffer prepare_buffer;
};
Spectrum_Control::Spectrum_Control(const Spectrum_Properties& properties, renderive_Owner<Frequency_Axis> frequency_axis, renderive_Owner<Axis> power_axis)
: Plottable_State(properties, std::make_unique<Impl>(
std::move(frequency_axis), std::move(power_axis))) {
d_func<Impl>().frame.emplace(*this);
}
: Spectrum_Real_Time_Data_Strategy(
std::in_place,
properties,
std::make_unique<Impl>(std::move(frequency_axis),
std::move(power_axis))) {}
Spectrum_Control::~Spectrum_Control() = default;
void Spectrum_Control::update_samples(std::span<const double> values) {
if(get<&Spectrum_Properties::frequency_point_size>() <= 0)
set<&Spectrum_Properties::frequency_point_size>(static_cast<int>(values.size()));
std::lock_guard lock(d_func<Impl>().frame_update_mutex);
Spectrum_Frame frame = d_func<Impl>().frame->snapshot().value_or(Spectrum_Frame{});
frame.samples.assign(values.begin(), values.end());
if(frame.maxima.size() != values.size())
frame.maxima.assign(values.begin(), values.end());
const auto& published = render_buffer_value<Spectrum_Frame_Tag>();
const bool shape_changed = published.samples.size() != values.size();
Spectrum_Frame next;
next.samples.assign(values.begin(), values.end());
if (published.maxima.size() != values.size())
next.maxima = next.samples;
else {
next.maxima.resize(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
next.maxima[index] = std::max(published.maxima[index], values[index]);
}
if (published.minima.size() != values.size())
next.minima = next.samples;
else {
next.minima.resize(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
next.minima[index] = std::min(published.minima[index], values[index]);
}
Spectrum_Real_Time_Data_Strategy::write<Spectrum_Frame_Tag>(std::move(next));
if (shape_changed)
render_graph_changed();
else
for(std::size_t index = 0; index < values.size(); ++index)
frame.maxima[index] = std::max(frame.maxima[index], values[index]);
if(frame.minima.size() != values.size())
frame.minima.assign(values.begin(), values.end());
else
for(std::size_t index = 0; index < values.size(); ++index)
frame.minima[index] = std::min(frame.minima[index], values[index]);
d_func<Impl>().frame->update(std::move(frame));
render_graph_changed();
changed();
}
void Spectrum_Control::update_samples(std::pmr::vector<double>&& values) {
update_samples(std::span<const double>(values.data(), values.size()));
}
std::size_t Spectrum_Control::sample_count() const {
const auto frame = d_func<Impl>().frame->snapshot();
return frame ? frame->samples.size() : 0;
return render_buffer_value<Spectrum_Frame_Tag>().samples.size();
}
std::size_t Spectrum_Control::rendered_point_count() const {
const auto state = properties();
const auto frame = d_func<Impl>().frame->snapshot();
return frame ? curve_points(frame->samples, state.frequency_range, d_func<Impl>().frequency_axis->transform(), d_func<Impl>().power_axis->transform(), state.visible_range_only, state.interpolation_mode).size() : 0;
return curve_points(render_buffer_value<Spectrum_Frame_Tag>().samples,
state.frequency_range,
d_func<Impl>().frequency_axis->transform(),
d_func<Impl>().power_axis->transform(),
state.visible_range_only, state.interpolation_mode).size();
}
double Spectrum_Control::power_at(double frequency, bool& ok) const {
const auto state = properties();
const auto frame = d_func<Impl>().frame->snapshot();
return frame ? spectrum_power_at(state, *frame, frequency, ok) : (ok = false, 0.0);
return spectrum_power_at(
state, render_buffer_value<Spectrum_Frame_Tag>(), frequency, ok);
}
void Spectrum_Control::add_custom_marker(double frequency) {
add_custom_line_marker(frequency);
@@ -294,15 +296,20 @@ void Spectrum_Control::handle_event(const Event& event) {
}
void Spectrum_Control::publish() {
publish_properties();
Spectrum_Real_Time_Data_Strategy::publish();
d_func<Impl>().interaction.publish();
}
std::uint64_t Spectrum_Control::state_revision() const {
return Plottable_State<Spectrum_Properties>::Base::state_revision() +
Spectrum_Real_Time_Data_Strategy::revision();
}
void Spectrum_Control::build_prepare_graph(Renderable_Graph_Builder& builder) {
const auto view = d_func().render_state_view();
const auto state = properties();
const auto& published_frame = view.get(*d_func<Impl>().frame);
const std::size_t work_size = published_frame && published_frame->samples.size() > 1
? published_frame->samples.size() - 1
const auto& published_frame = render_buffer_value<Spectrum_Frame_Tag>();
const std::size_t work_size = published_frame.samples.size() > 1
? published_frame.samples.size() - 1
: 0;
const int partition_count = d_func<Impl>().partitioner.graph_partition_count(
state.partition_mode, state.partition_count.get(),
@@ -313,7 +320,7 @@ void Spectrum_Control::build_prepare_graph(Renderable_Graph_Builder& builder) {
prepare_render_frame(context.frame.render_state, partition_count);
if (context.metrics) {
context.metrics->set(Node_Metric_Kind::input_count,
d_func<Impl>().prepare_buffer.frame.samples.size());
d_func<Impl>().prepare_buffer.frame->samples.size());
context.metrics->set(Node_Metric_Kind::chunk_size,
d_func<Impl>().prepare_buffer.work_size /
std::max(1, d_func<Impl>().prepare_buffer.active_partitions));
@@ -338,11 +345,10 @@ void Spectrum_Control::build_prepare_graph(Renderable_Graph_Builder& builder) {
}
void Spectrum_Control::build_paint_graph(Renderable_Graph_Builder& builder) {
const auto view = d_func().render_state_view();
const auto state = properties();
const auto& published_frame = view.get(*d_func<Impl>().frame);
const std::size_t work_size = published_frame && published_frame->samples.size() > 1
? published_frame->samples.size() - 1
const auto& published_frame = render_buffer_value<Spectrum_Frame_Tag>();
const std::size_t work_size = published_frame.samples.size() > 1
? published_frame.samples.size() - 1
: 0;
const int partition_count = d_func<Impl>().partitioner.graph_partition_count(
state.partition_mode, state.partition_count.get(),
@@ -369,18 +375,17 @@ void Spectrum_Control::build_paint_graph(Renderable_Graph_Builder& builder) {
void Spectrum_Control::prepare_render_frame(const Render_State_View& view,
int graph_partition_count) {
const auto& published_frame = view.get(*d_func<Impl>().frame);
const auto& published_frame = render_buffer_value<Spectrum_Frame_Tag>();
auto& output = d_func<Impl>().prepare_buffer;
output = {};
output.properties = render_properties(view);
if (published_frame)
output.frame = *published_frame;
output.frame = &published_frame;
output.interaction = view.get(d_func<Impl>().interaction);
output.frequency_axis = d_func<Impl>().frequency_axis->transform(view);
output.power_axis = d_func<Impl>().power_axis->transform(view);
output.valid = axes_are_orthogonal(output.frequency_axis, output.power_axis);
output.work_size = published_frame && published_frame->samples.size() > 1
? published_frame->samples.size() - 1
output.work_size = published_frame.samples.size() > 1
? published_frame.samples.size() - 1
: 0;
output.active_partitions = d_func<Impl>().partitioner.begin(graph_partition_count,
output.work_size);
@@ -416,16 +421,16 @@ void Spectrum_Control::prepare_render_frame(const Render_State_View& view,
? Spectrum_Marker_Style::selected
: Spectrum_Marker_Style::marker});
}
if (!output.frame.samples.empty() &&
if (!output.frame->samples.empty() &&
(state.max_marker_visible || state.use_min_marker)) {
const auto prepare_extreme = [&](bool maximum) {
const auto iterator = maximum
? std::max_element(output.frame.samples.begin(), output.frame.samples.end())
: std::min_element(output.frame.samples.begin(), output.frame.samples.end());
? std::max_element(output.frame->samples.begin(), output.frame->samples.end())
: std::min_element(output.frame->samples.begin(), output.frame->samples.end());
const std::size_t index = static_cast<std::size_t>(
std::distance(output.frame.samples.begin(), iterator));
const double denominator = output.frame.samples.size() > 1
? static_cast<double>(output.frame.samples.size() - 1)
std::distance(output.frame->samples.begin(), iterator));
const double denominator = output.frame->samples.size() > 1
? static_cast<double>(output.frame->samples.size() - 1)
: 1.0;
const double frequency = state.frequency_range.origin +
state.frequency_range.length() * static_cast<double>(index) / denominator;
@@ -443,7 +448,7 @@ void Spectrum_Control::prepare_render_frame(const Render_State_View& view,
const double frequency = output.frequency_axis.point_to_coord(
output.interaction.tooltip.position);
bool ok{};
const double power = spectrum_power_at(state, output.frame, frequency, ok);
const double power = spectrum_power_at(state, *output.frame, frequency, ok);
if (ok) {
std::ostringstream text;
text << std::fixed << std::setprecision(2) << frequency << " Hz " << power;
@@ -460,7 +465,7 @@ void Spectrum_Control::prepare_partition(int partition_index) {
if (!output.valid || partition_index >= output.active_partitions)
return;
const auto& state = output.properties;
const auto& frame = output.frame;
const auto& frame = *output.frame;
const auto current = curve_partition(frame.samples, state.frequency_range,
partition_index, output.active_partitions);
if (current.values.empty())
+28 -1
View File
@@ -3,6 +3,7 @@
#include "Plottable.h"
#include "../axis/Axis.h"
#include "../renderable/Render_Partition.h"
#include <renderive/real_time_data/Double_Buffer_Strategy.hpp>
#include <renderive/renderable/Render_Frame_Completion.hpp>
#include <memory_resource>
#include <span>
@@ -34,7 +35,32 @@ struct Spectrum_Properties : Hover_Tooltip_Properties {
Brush sweep_region_brush{Color{255, 255, 0, 100}, Brush_Style::Solid};
};
namespace detail {
class LIB_DECL Spectrum_Control : public Plottable_State<Spectrum_Properties>,
struct Spectrum_Frame {
std::vector<double> samples;
std::vector<double> maxima;
std::vector<double> minima;
};
struct Spectrum_Frame_Tag {};
using Spectrum_Buffer_Layout =
::Double_Buffer_Layout<
::Buffered_Data<Spectrum_Frame_Tag, Spectrum_Frame>>;
class Spectrum_Real_Time_Data_Strategy
: public ::Multi_Double_Buffer_Strategy<
Plottable_State<Spectrum_Properties>, Spectrum_Buffer_Layout,
std::mutex> {
using Base =
::Multi_Double_Buffer_Strategy<
Plottable_State<Spectrum_Properties>, Spectrum_Buffer_Layout,
std::mutex>;
protected:
using Base::Base;
using Base::publish;
using Base::render_buffer_value;
using Base::revision;
using Base::write;
};
class LIB_DECL Spectrum_Control : public Spectrum_Real_Time_Data_Strategy,
public Event_Handler,
public ::Render_Frame_Completion {
public:
@@ -79,6 +105,7 @@ private:
void paint_overlay(Painter& painter, const Render_State_View& state);
void render_frame_completed(std::uint64_t target_interval_ns) override;
void publish() override;
[[nodiscard]] std::uint64_t state_revision() const override;
};
}
using Spectrum = detail::Attach_Plottable<detail::Spectrum_Control, Spectrum_Properties>;
+6 -6
View File
@@ -8,8 +8,8 @@ Renderive 只负责逻辑状态收集、批量数据收集、Kernel 帧策略调
```text
Point_State --Kernel Double_State_Strategy--+
+--帧快照--> Kernel Frame Strategy
Point_Data --Kernel Latest_Real_Time_Data----+ |
+--帧边界--> Point_Frame_Strategy
Point payload --Kernel Multi_Double_Buffer_Strategy-----------+
Kernel Event --Input_Collector---------------+ v
Render_Domain 单线程
|
@@ -38,9 +38,9 @@ Kernel Event --Input_Collector---------------+ v
## 已完成的 Point 封装
- `Point_Visual`PImpl`Point_State` 只保存样式、变换、可见性和深度测试。
- `Point_Data`直接使用 Kernel `Latest_Real_Time_Data<std::vector<Point>>`,不在 visual 中复制大数据
- `Point_Scene`直接组合 Kernel Manual、Low Latency、Playback 三种策略;统一发布 `Frame_Status` 查询快照
- `Point_Visual`直接组合状态与实时数据策略`Point_State` 只保存样式、变换、可见性和深度测试。
- `Point_Visual`状态使用 Kernel `Double_State_Strategy`,不可变点集使用单 Entry 的 `Multi_Double_Buffer_Strategy` 在帧边界发布
- `Point_Scene`只使用满足 Kernel frame-control concept 的 3D `Point_Frame_Strategy`;统一发布 `Frame_Status` 查询。
- `Datoviz_Point_Backend`:所有 Datoviz/Vulkan 资源均限制在单一 `Render_Domain`;批量属性使用 `dvz_visual_set_data_many()` 原子提交。
- 输入:Kernel pointer/wheel/key 事件映射到 Datoviz router/arcballWeb 保留事件的真实派生类型,不经过 `Event` 切片。
- 输出:外部 RGBA8 target 渲染、同步回读并发布不可变 `Pixel_Frame`
@@ -70,7 +70,7 @@ Datoviz 生产源码和上游测试已经迁入;以下是尚未实现的 Rende
| 状态 | 唯一权威来源 | 读取方式 |
| --- | --- | --- |
| Point 样式/变换/可见性 | `Point_Visual` 的 Kernel 双状态策略 | 帧边界发布快照 |
| Point 大批量数据 | `Point_Data` | revision + snapshot |
| Point 大批量数据 | `Point_Visual` 的 Kernel 双缓冲 | 不可变已发布点集 |
| viewport/clear color | `Scene_State_Buffer` | 帧边界发布快照 |
| 输入事件 | `Input_Collector` 队列 | 每帧 drain |
| 帧生命周期/计数 | Kernel frame strategy | `Frame_Status` 即时查询 |
+3 -5
View File
@@ -19,13 +19,11 @@ std::vector<Point> point_demo_data(float phase_radians) {
}
Point_Demo make_point_demo(Scene_Options options) {
auto data = std::make_shared<Point_Data>();
data->update(point_demo_data());
Point_State state;
state.style = {{12, 16, 24, 255}, 2.0F, Point_Aspect::Outline};
auto visual = std::make_shared<Point_Visual>(data, state);
auto scene = std::make_unique<Point_Scene>(options, std::move(visual));
return {std::move(data), std::move(scene)};
auto visual = std::make_shared<Point_Visual>(point_demo_data(), state);
auto scene = std::make_unique<Point_Scene>(options, visual);
return {std::move(visual), std::move(scene)};
}
} // namespace renderive::render_3d
+1 -1
View File
@@ -8,7 +8,7 @@
namespace renderive::render_3d {
struct Point_Demo {
std::shared_ptr<Point_Data> points;
std::shared_ptr<Point_Visual> visual;
std::unique_ptr<Point_Scene> scene;
};
+22 -11
View File
@@ -72,21 +72,23 @@ struct Point_Scene::Impl {
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;
});
auto lease = scheduler.acquire_renderer();
if (!lease)
return false;
auto rendered = backend->render(
static_cast<const detail::Point_Frame_Data&>(*lease));
if (!rendered)
return false;
std::lock_guard lock(frame_mutex);
latest = std::move(rendered);
return true;
});
}
std::shared_ptr<Point_Visual> visual;
detail::Scene_State_Buffer states;
detail::Input_Collector input;
detail::Frame_Scheduler scheduler;
detail::Point_Frame_Strategy scheduler;
detail::Render_Domain render_domain;
std::unique_ptr<detail::Datoviz_Point_Backend> backend;
mutable std::mutex frame_mutex;
@@ -172,8 +174,17 @@ void Point_Scene::dispatch(const ::renderive::Key_Event& event) {
}
bool Point_Scene::prepare_frame() {
return impl_->accepting.load(std::memory_order_acquire) &&
impl_->scheduler.prepare(impl_->states, *impl_->visual, impl_->input);
if (!impl_->accepting.load(std::memory_order_acquire))
return false;
auto lease = impl_->scheduler.acquire_painter();
if (!lease)
return false;
auto scene = impl_->states.publish_state();
lease->scene = std::move(scene.state);
lease->scene_revision = scene.revision;
lease->point = detail::Point_State_Access::publish(*impl_->visual);
lease->input = impl_->input.drain();
return true;
}
bool Point_Scene::refresh_manual_frame() {
+57 -32
View File
@@ -2,10 +2,7 @@
#include "detail/Point_Core.h"
#include <renderive/state/Double_State_Strategy.hpp>
#include <cmath>
#include <mutex>
#include <stdexcept>
#include <utility>
@@ -31,49 +28,77 @@ void validate(const std::vector<Point>& points) {
}
}
struct Point_State_Base {};
} // namespace
struct Point_Visual::Impl final
: ::Double_State_Strategy<Point_State_Base, Point_State, std::mutex> {
using Base = ::Double_State_Strategy<Point_State_Base, Point_State, std::mutex>;
Impl(std::shared_ptr<Point_Data> 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<Point>{});
validate(payload);
return {Base::render_state_value(), std::move(payload),
Base::state_revision(), data_revision};
}
std::shared_ptr<Point_Data> data;
std::mutex publication_mutex;
};
Point_Visual::Point_Visual(std::shared_ptr<Point_Data> data, Point_State initial) {
if (!data)
throw std::invalid_argument("Point_Visual requires a Point_Data source");
Point_Visual::Point_Visual(std::vector<Point> points, Point_State initial)
: detail::Point_Visual_Strategy(std::in_place, initial) {
validate(initial);
impl_ = std::make_unique<Impl>(std::move(data), initial);
validate(points);
detail::Point_Visual_Strategy::write<detail::Point_Payload_Tag>(
std::make_shared<const std::vector<Point>>(std::move(points)));
}
Point_Visual::~Point_Visual() = default;
void Point_Visual::update_points(std::vector<Point> points) {
validate(points);
detail::Point_Visual_Strategy::write<detail::Point_Payload_Tag>(
std::make_shared<const std::vector<Point>>(std::move(points)));
}
void Point_Visual::edit_points(
const std::function<void(std::vector<Point>&)>& edit) {
if (!edit)
throw std::invalid_argument("Point_Visual point edit is empty");
const auto& published =
detail::Point_Visual_Strategy::render_buffer_value<
detail::Point_Payload_Tag>();
auto points = published ? *published : std::vector<Point>{};
edit(points);
update_points(std::move(points));
}
std::size_t Point_Visual::point_count() const {
const auto& points =
detail::Point_Visual_Strategy::render_buffer_value<
detail::Point_Payload_Tag>();
return points ? points->size() : 0U;
}
std::uint64_t Point_Visual::data_revision() const {
return detail::Point_Visual_Strategy::revision();
}
void Point_Visual::configure(Point_State state) {
validate(state);
impl_->update([state = std::move(state)](Point_State& target) mutable {
detail::Point_State_Strategy::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();
return visual.publish_frame();
}
detail::Published_Point Point_Visual::publish_frame() {
publish();
return {
detail::Point_State_Strategy::render_state_value(),
detail::Point_Visual_Strategy::render_buffer_value<
detail::Point_Payload_Tag>(),
detail::Point_State_Strategy::state_revision(),
detail::Point_Visual_Strategy::revision()};
}
void Point_Visual::publish() {
detail::Point_State_Strategy::publish();
detail::Point_Visual_Strategy::publish();
}
std::uint64_t Point_Visual::state_revision() const {
return detail::Point_State_Strategy::state_revision() +
detail::Point_Visual_Strategy::revision();
}
} // namespace renderive::render_3d
+37 -8
View File
@@ -1,9 +1,13 @@
#pragma once
#include <array>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <vector>
#include <renderive/real_time_data/Latest_Real_Time_Data.hpp>
#include <renderive/base/Concepts.hpp>
#include <renderive/real_time_data/Double_Buffer_Strategy.hpp>
#include <renderive/state/Double_State_Strategy.hpp>
namespace renderive::render_3d {
struct Vec3 {
float x{};
@@ -51,21 +55,46 @@ struct Point_State {
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<std::vector<Point>>;
namespace detail {
struct Point_State_Base {};
struct Point_Payload_Tag {};
using Point_Payload = std::shared_ptr<const std::vector<Point>>;
using Point_State_Strategy =
::Double_State_Strategy<Point_State_Base, Point_State, std::mutex>;
using Point_Buffer_Layout =
::Double_Buffer_Layout<
::Buffered_Data<Point_Payload_Tag, Point_Payload>>;
class Point_Visual_Strategy
: public ::Multi_Double_Buffer_Strategy<
Point_State_Strategy, Point_Buffer_Layout, std::mutex> {
using Base = ::Multi_Double_Buffer_Strategy<
Point_State_Strategy, Point_Buffer_Layout, std::mutex>;
protected:
using Base::Base;
using Base::publish;
using Base::render_buffer_value;
using Base::revision;
using Base::write;
};
struct Published_Point;
class Point_State_Access;
}
// Thread-safe logical point visual. Datoviz resources deliberately do not live here.
class Point_Visual final : Non_Copyable {
class Point_Visual final : private detail::Point_Visual_Strategy,
Non_Copyable {
public:
explicit Point_Visual(std::shared_ptr<Point_Data> data, Point_State initial = {});
explicit Point_Visual(std::vector<Point> points = {}, Point_State initial = {});
~Point_Visual();
// Atomically replace the visual properties. Point payloads are updated on Point_Data.
void update_points(std::vector<Point> points);
void edit_points(const std::function<void(std::vector<Point>&)>& edit);
[[nodiscard]] std::size_t point_count() const;
[[nodiscard]] std::uint64_t data_revision() const;
void configure(Point_State state);
private:
friend class detail::Point_State_Access;
struct Impl;
std::unique_ptr<Impl> impl_;
[[nodiscard]] detail::Published_Point publish_frame();
void publish() override;
[[nodiscard]] std::uint64_t state_revision() const override;
};
} // namespace renderive::render_3d
@@ -409,6 +409,7 @@ void Datoviz_Point_Backend::create_scene(const Scene_State& initial_scene) {
void Datoviz_Point_Backend::apply(const Point_Frame_Data& frame) {
require_domain();
const auto& points = *frame.point.data;
if (frame.scene_revision != applied_scene_revision_) {
if (dvz_figure_resize(figure_, frame.scene.viewport.width,
frame.scene.viewport.height) != DVZ_OK)
@@ -437,23 +438,23 @@ void Datoviz_Point_Backend::apply(const Point_Frame_Data& frame) {
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)
dvz_visual_set_visible(visual_, state.visible && !points.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 (points.empty()) {
if (dvz_visual_set_visible(visual_, false) != DVZ_OK)
throw std::runtime_error("failed to hide empty Datoviz point visual");
} else {
std::vector<std::array<float, 3>> positions;
std::vector<std::array<std::uint8_t, 4>> colors;
std::vector<float> 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.reserve(points.size());
colors.reserve(points.size());
diameters.reserve(points.size());
for (const auto& point : points) {
positions.push_back(
{point.position.x, point.position.y, point.position.z});
colors.push_back({point.color.red, point.color.green,
+158 -117
View File
@@ -2,9 +2,7 @@
#include "render_3D/Point_Scene.h"
#include <renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp>
#include <renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp>
#include <renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp>
#include <renderive/frame_control/concept/Frame_Control_Strategy.hpp>
#include <renderive/state/Double_State_Strategy.hpp>
#include <cstdint>
@@ -13,18 +11,15 @@
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
namespace renderive::render_3d::detail {
struct Published_Point {
Point_State state;
std::vector<Point> data;
std::shared_ptr<const std::vector<Point>> data;
std::uint64_t state_revision{};
std::uint64_t data_revision{};
};
@@ -54,7 +49,7 @@ public:
explicit Scene_State_Buffer(const Scene_State& initial) : Base(initial) {}
[[nodiscard]] Published publish_snapshot() {
[[nodiscard]] Published publish_state() {
std::lock_guard lock(publication_mutex_);
Base::publish();
return {Base::render_state_value(), Base::state_revision()};
@@ -118,133 +113,179 @@ struct Point_Frame_Data {
std::uint64_t frame_sequence{};
};
class Frame_Scheduler final {
using Manual = ::Manual_Refresh_Strategy<Point_Frame_Data, std::mutex>;
using Low_Latency = ::Low_Latency_Strategy<Point_Frame_Data, std::mutex>;
using Playback = ::Flow_Refresh_Strategy<Point_Frame_Data, std::mutex>;
using Strategy = std::variant<std::unique_ptr<Manual>, std::unique_ptr<Low_Latency>,
std::unique_ptr<Playback>>;
class Point_Frame_Strategy final : public ::Frame_Control_Strategy_Base {
public:
Frame_Scheduler(Frame_Mode mode, double maximum_frames_per_second)
: strategy_(make_strategy(mode, maximum_frames_per_second)) {}
struct Frame final : Point_Frame_Data {};
[[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_);
class Painter_Lease final {
public:
explicit Painter_Lease(Point_Frame_Strategy& strategy)
: strategy_(&strategy), frame_(strategy.make_frame()) {}
Painter_Lease(const Painter_Lease&) = delete;
Painter_Lease& operator=(const Painter_Lease&) = delete;
Painter_Lease(Painter_Lease&& other) noexcept
: strategy_(std::exchange(other.strategy_, nullptr)),
frame_(std::move(other.frame_)) {}
~Painter_Lease() {
if (strategy_ && frame_)
strategy_->submit(std::move(frame_));
}
explicit operator bool() const noexcept { return frame_ != nullptr; }
Frame* get() noexcept { return frame_.get(); }
const Frame* get() const noexcept { return frame_.get(); }
Frame* operator->() noexcept { return get(); }
const Frame* operator->() const noexcept { return get(); }
Frame& operator*() noexcept { return *frame_; }
const Frame& operator*() const noexcept { return *frame_; }
private:
Point_Frame_Strategy* strategy_{};
std::unique_ptr<Frame> frame_;
};
class Render_Lease final {
public:
explicit Render_Lease(Point_Frame_Strategy& strategy)
: strategy_(&strategy), frame_(strategy.take_ready()) {}
Render_Lease(const Render_Lease&) = delete;
Render_Lease& operator=(const Render_Lease&) = delete;
Render_Lease(Render_Lease&& other) noexcept
: strategy_(std::exchange(other.strategy_, nullptr)),
frame_(std::move(other.frame_)) {}
~Render_Lease() {
if (strategy_ && frame_)
strategy_->complete(frame_->frame_sequence);
}
explicit operator bool() const noexcept { return frame_ != nullptr; }
Frame* get() noexcept { return frame_.get(); }
const Frame* get() const noexcept { return frame_.get(); }
Frame* operator->() noexcept { return get(); }
const Frame* operator->() const noexcept { return get(); }
Frame& operator*() noexcept { return *frame_; }
const Frame& operator*() const noexcept { return *frame_; }
private:
Point_Frame_Strategy* strategy_{};
std::unique_ptr<Frame> frame_;
};
Point_Frame_Strategy(Frame_Mode mode, double maximum_frames_per_second)
: Frame_Control_Strategy_Base(
mode == Frame_Mode::Low_Latency
? maximum_frames_per_second
: invalid_frequency_hz(),
mode == Frame_Mode::Low_Latency
? frequency_interval_ns(maximum_frames_per_second)
: 0),
mode_(mode) {}
[[nodiscard]] Painter_Lease acquire_painter() { return Painter_Lease(*this); }
[[nodiscard]] Render_Lease acquire_renderer() { return Render_Lease(*this); }
void swap() override { swap_frame_control_state(); }
[[nodiscard]] double frequency_hz() const noexcept override {
return render_frame_control_state().frequency_hz;
}
[[nodiscard]] std::uint64_t next_refresh_interval_ns() const noexcept override {
return render_frame_control_state().next_refresh_interval_ns;
}
[[nodiscard]] Frame_Control_Strategy_Base::State frame_control_state() const override {
return render_frame_control_state();
}
void on_real_time_data_update(const Real_Time_Data_Observation&) noexcept override {}
[[nodiscard]] bool refresh() {
auto* manual = std::get_if<std::unique_ptr<Manual>>(&strategy_);
return manual != nullptr && (*manual)->refresh();
}
[[nodiscard]] bool activate_for_request() {
if (auto* manual = std::get_if<std::unique_ptr<Manual>>(&strategy_))
return (*manual)->refresh();
std::lock_guard lock(mutex_);
if (mode_ != Frame_Mode::Manual || !manual_pending_) {
++status_.failed_operation_count;
return false;
}
status_.dropped_frame_count += ready_.size();
ready_.clear();
ready_.push_back(std::move(manual_pending_));
return true;
}
[[nodiscard]] bool activate_for_request() {
return mode_ != Frame_Mode::Manual || refresh();
}
[[nodiscard]] bool discard_pending() {
if (auto* manual = std::get_if<std::unique_ptr<Manual>>(&strategy_))
return (*manual)->discard_pending_frame();
if (auto* low_latency = std::get_if<std::unique_ptr<Low_Latency>>(&strategy_))
return (*low_latency)->discard_pending_frame();
std::lock_guard lock(mutex_);
if (mode_ == Frame_Mode::Manual && manual_pending_) {
manual_pending_.reset();
++status_.dropped_frame_count;
return true;
}
if (mode_ == Frame_Mode::Low_Latency && !ready_.empty()) {
status_.dropped_frame_count += ready_.size();
ready_.clear();
return true;
}
return false;
}
template <class Render>
requires std::invocable<Render, const Point_Frame_Data&>
[[nodiscard]] bool render(Render&& render) {
return std::visit(
[&](auto& strategy) {
auto lease = strategy->acquire_renderer();
if (!lease)
return false;
return static_cast<bool>(std::invoke(
std::forward<Render>(render),
static_cast<const Point_Frame_Data&>(*lease)));
},
strategy_);
}
[[nodiscard]] Frame_Status status() const {
return std::visit(
[](const auto& strategy) {
using Strategy_Type = std::remove_cvref_t<decltype(*strategy)>;
Frame_Status result;
if constexpr (std::same_as<Strategy_Type, Manual>) {
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<Strategy_Type, Low_Latency>) {
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_);
std::lock_guard lock(mutex_);
auto result = status_;
const auto control = render_frame_control_state();
result.mode = mode_;
result.frequency_hz = control.frequency_hz;
result.next_refresh_interval_ns = control.next_refresh_interval_ns;
result.pending_frame_count = ready_.size() +
(manual_pending_ ? 1U : 0U);
return result;
}
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::unique_ptr<Manual>>,
std::make_unique<Manual>());
case Frame_Mode::Low_Latency:
return Strategy(
std::in_place_type<std::unique_ptr<Low_Latency>>,
std::make_unique<Low_Latency>(
Observer_State<>{},
typename Low_Latency::Configuration{maximum_frames_per_second}));
case Frame_Mode::Playback:
return Strategy(std::in_place_type<std::unique_ptr<Playback>>,
std::make_unique<Playback>());
}
throw std::invalid_argument("unknown Point_Scene frame mode");
[[nodiscard]] std::unique_ptr<Frame> make_frame() {
auto frame = std::make_unique<Frame>();
std::lock_guard lock(mutex_);
frame->frame_sequence = ++next_sequence_;
return frame;
}
Strategy strategy_;
void submit(std::unique_ptr<Frame> frame) {
std::lock_guard lock(mutex_);
++status_.produced_frame_count;
if (mode_ == Frame_Mode::Manual) {
if (manual_pending_)
++status_.dropped_frame_count;
manual_pending_ = std::move(frame);
return;
}
if (mode_ == Frame_Mode::Low_Latency) {
status_.dropped_frame_count += ready_.size();
ready_.clear();
}
ready_.push_back(std::move(frame));
}
[[nodiscard]] std::unique_ptr<Frame> take_ready() {
std::lock_guard lock(mutex_);
if (ready_.empty()) {
++status_.failed_operation_count;
return {};
}
auto frame = std::move(ready_.front());
ready_.pop_front();
return frame;
}
void complete(std::uint64_t sequence) {
std::lock_guard lock(mutex_);
++status_.consumed_frame_count;
status_.latest_sequence = sequence;
}
Frame_Mode mode_;
mutable std::mutex mutex_;
std::uint64_t next_sequence_{};
std::unique_ptr<Frame> manual_pending_;
std::deque<std::unique_ptr<Frame>> ready_;
Frame_Status status_;
};
static_assert(::Frame_Control_Strategy<Point_Frame_Strategy>);
} // namespace renderive::render_3d::detail
+50 -47
View File
@@ -13,9 +13,7 @@ namespace renderive::render_3d {
namespace {
TEST(PointState, UsesKernelDoubleStateAtFrameBoundary) {
auto data = std::make_shared<Point_Data>();
data->update(std::vector<Point>{{{1.0F, 2.0F, 3.0F}, {1, 2, 3, 255}, 7.0F}});
Point_Visual visual(data);
Point_Visual visual({{{1.0F, 2.0F, 3.0F}, {1, 2, 3, 255}, 7.0F}});
Point_State configured;
configured.visible = false;
@@ -24,15 +22,14 @@ TEST(PointState, UsesKernelDoubleStateAtFrameBoundary) {
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}));
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_Data>();
Point_Visual visual(data);
TEST(PointState, PublishesImmutableBulkPayloadsThroughDoubleBuffer) {
Point_Visual visual;
constexpr int update_count = 500;
std::atomic<bool> done{};
@@ -43,82 +40,88 @@ TEST(PointState, UsesKernelLatestRealTimeDataForBulkPayloads) {
point.position.x = static_cast<float>(value);
point.diameter_px = static_cast<float>(value % 12 + 1);
}
data->update(std::move(points));
visual.update_points(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)
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<float>(update_count));
EXPECT_GT(published.data_revision, 0U);
EXPECT_LE(published.data_revision, update_count);
ASSERT_FALSE(published.data->empty());
EXPECT_EQ(published.data->front().position.x, static_cast<float>(update_count));
}
TEST(PointState, RejectsInvalidStateAndPayloadAtTheOwningBoundary) {
auto data = std::make_shared<Point_Data>();
Point_Visual visual(data);
Point_Visual visual;
Point_State invalid;
invalid.style.stroke_width_px = -1.0F;
EXPECT_THROW(visual.configure(invalid), std::invalid_argument);
data->update(std::vector<Point>{{{}, {}, 0.0F}});
EXPECT_THROW((void)detail::Point_State_Access::publish(visual),
EXPECT_THROW(visual.update_points(std::vector<Point>{{{}, {}, 0.0F}}),
std::invalid_argument);
}
TEST(PointFrameControl, ReusesKernelManualAndPlaybackStrategies) {
auto data = std::make_shared<Point_Data>();
data->update(std::vector<Point>{{{}, {}, 8.0F}});
Point_Visual visual(data);
TEST(PointFrameControl, UsesDedicatedThreeDimensionalFrameStrategy) {
Point_Visual visual(std::vector<Point>{{{}, {}, 8.0F}});
detail::Scene_State_Buffer scene({{320, 180}, {}});
detail::Input_Collector input;
const auto prepare = [&](detail::Point_Frame_Strategy& strategy) {
auto lease = strategy.acquire_painter();
if (!lease)
return false;
auto published_scene = scene.publish_state();
lease->scene = std::move(published_scene.state);
lease->scene_revision = published_scene.revision;
lease->point = detail::Point_State_Access::publish(visual);
lease->input = input.drain();
return true;
};
detail::Frame_Scheduler manual(Frame_Mode::Manual, 60.0);
EXPECT_TRUE(manual.prepare(scene, visual, input));
detail::Point_Frame_Strategy manual(Frame_Mode::Manual, 60.0);
EXPECT_TRUE(prepare(manual));
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_FALSE(static_cast<bool>(manual.acquire_renderer()));
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);
{
auto rendered = manual.acquire_renderer();
ASSERT_TRUE(rendered);
EXPECT_EQ(rendered->point.data->size(), 1U);
}
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<Point>(2, Point{{}, {}, 8.0F}));
EXPECT_TRUE(playback.prepare(scene, visual, input));
detail::Point_Frame_Strategy playback(Frame_Mode::Playback, 60.0);
EXPECT_TRUE(prepare(playback));
visual.update_points(std::vector<Point>(2, Point{{}, {}, 8.0F}));
EXPECT_TRUE(prepare(playback));
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;
}));
{
auto first = playback.acquire_renderer();
ASSERT_TRUE(first);
EXPECT_EQ(first->point.data->size(), 1U);
}
{
auto second = playback.acquire_renderer();
ASSERT_TRUE(second);
EXPECT_EQ(second->point.data->size(), 2U);
}
playback_status = playback.status();
EXPECT_EQ(playback_status.consumed_frame_count, 2U);
EXPECT_EQ(playback_status.pending_frame_count, 0U);
+24 -22
View File
@@ -1492,21 +1492,26 @@ public:
publish_demo_points();
last_action_result_ = "points_orbited";
rendered_since_last_pixel_ = false;
return "Point positions updated through Latest_Real_Time_Data";
return "Point positions updated through Point_Visual double buffering";
}
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));
demo_.visual->edit_points([](auto& points) {
points.push_back(
{{0.12F, -0.72F, 0.58F}, {55, 235, 230, 255}, 46.0F});
});
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));
bool removed{};
demo_.visual->edit_points([&](auto& points) {
if (points.size() > render_3d::point_demo_data().size()) {
points.pop_back();
removed = true;
}
});
if (removed) {
last_action_result_ = "point_removed";
rendered_since_last_pixel_ = false;
return "Last added Point removed";
@@ -1515,11 +1520,11 @@ public:
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));
demo_.visual->edit_points([](auto& points) {
points.push_back(
{{0.0F, 0.0F, 0.8F}, {255, 255, 255, 255}, 72.0F});
points.pop_back();
});
last_action_result_ = "point_churned";
rendered_since_last_pixel_ = false;
return "Transient Point added and removed before the next frame snapshot";
@@ -1541,7 +1546,7 @@ public:
[[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 point_count = demo_.visual->point_count();
const auto render_window = render_duration_statistics_.snapshot();
const auto encode_window = pixel_encode_statistics_.snapshot();
const auto request_window = pixel_request_statistics_.snapshot();
@@ -1607,20 +1612,17 @@ public:
{"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()}}}}
{"point_3d", {{"point_count", point_count},
{"data_revision", demo_.visual->data_revision()}}},
{"data_shape", {{"input_elements", point_count},
{"rendered_elements", point_count}}}}
;
return telemetry.dump();
}
private:
[[nodiscard]] std::vector<render_3d::Point> current_points() const {
return demo_.points->snapshot().value_or(std::vector<render_3d::Point>{});
}
void publish_demo_points() {
demo_.points->update(render_3d::point_demo_data(phase_));
demo_.visual->update_points(render_3d::point_demo_data(phase_));
}
template <class Render>
bool record_render(Render&& render) {
+6 -6
View File
@@ -119,13 +119,13 @@ std::vector<Action_Model> registered_actions(std::string_view case_id,
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("orbit_points", "移动点", "Point_Visual::update_points",
"通过 Kernel Multi_Double_Buffer_Strategy 发布一批新位置");
add("add_point", "添加点", "Point_Visual::edit_points", "向批量点数据追加一个点");
add("remove_point", "移除点", "Point_Visual::edit_points", "移除最后追加的点");
add("point_churn", "点增删压力", "Point_Visual::edit_points",
"在帧快照前连续追加并移除临时点");
add("reset_points", "重置点", "Point_Data::update", "恢复 3D 点演示数据");
add("reset_points", "重置点", "Point_Visual::update_points", "恢复 3D 点演示数据");
return result;
}
append_session_actions(frame_mode, result);
+12 -1
View File
@@ -15,7 +15,8 @@
"@xyflow/react": "^12.4.4",
"elkjs": "^0.9.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-resizable-panels": "^4.12.2"
},
"devDependencies": {
"@playwright/test": "^1.51.1",
@@ -3396,6 +3397,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-resizable-panels": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.12.2.tgz",
"integrity": "sha512-NwY5LCo4WrxVvDh0xoMML6EMLPONP/8ckKcIdpnojxexoatZdjLiRqLJQjQK5CPkd4SYiB/2M5BVrjZBQtOO7Q==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+2 -1
View File
@@ -18,7 +18,8 @@
"@xyflow/react": "^12.4.4",
"elkjs": "^0.9.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-resizable-panels": "^4.12.2"
},
"devDependencies": {
"@playwright/test": "^1.51.1",
+141 -14
View File
@@ -1,17 +1,144 @@
import {Alert,Box,CircularProgress,Container,Stack,Typography} from "@mui/material";
import {useEffect,useMemo,useState} from "react";
import {Alert, Box, CircularProgress, Container, Stack, Typography, useMediaQuery} from "@mui/material";
import {useTheme} from "@mui/material/styles";
import {useEffect, useMemo, useState} from "react";
import {Group, Panel, Separator} from "react-resizable-panels";
import {Category_Filter} from "./gallery/category_filter";
import {Frame_Mode_Tabs} from "./gallery/frame_mode_tabs";
import {Gallery_Page} from "./gallery/gallery_page";
import {Gallery_Summary} from "./gallery/gallery_summary";
import {Gallery_Toolbar} from "./gallery/gallery_toolbar";
import type {Selected_Plot} from "./gallery/plot_card";
import {use_gallery_catalog} from "./hooks/use_gallery_catalog";
import {Gallery_Toolbar} from "./gallery/gallery_toolbar";import {Gallery_Summary} from "./gallery/gallery_summary";import {Frame_Mode_Tabs} from "./gallery/frame_mode_tabs";import {Category_Filter} from "./gallery/category_filter";import {Gallery_Page} from "./gallery/gallery_page";import type {Selected_Plot} from "./gallery/plot_card";import {Inspector_Drawer} from "./inspector/inspector_drawer";
import {Inspector_Panel} from "./inspector/inspector_panel";
export function App() {
const {catalog,state:socket_state,message:socket_message}=use_gallery_catalog();
const [active_mode,set_active_mode]=useState(""),[active_category,set_active_category]=useState(""),[streams_paused,set_streams_paused]=useState(false),[selected_plot,set_selected_plot]=useState<Selected_Plot|null>(null),[selected_inspector_tab,set_selected_inspector_tab]=useState("controls");
const [,set_visibility_epoch]=useState(0);
useEffect(()=>{const change=()=>set_visibility_epoch(value=>value+1);document.addEventListener("visibilitychange",change);return()=>document.removeEventListener("visibilitychange",change);},[]);
useEffect(()=>{if(catalog){set_active_mode(catalog.frame_modes.some(mode=>mode.id===catalog.navigation.default_mode)?catalog.navigation.default_mode:catalog.frame_modes[0]?.id??"");set_active_category(catalog.navigation.all_categories_label);}},[catalog]);
const modes=useMemo(()=>[...(catalog?.frame_modes??[])].sort((a,b)=>a.order-b.order),[catalog]);
const cases=useMemo(()=>[...(catalog?.cases??[])].sort((a,b)=>a.order-b.order),[catalog]);
const frame_mode=modes.find(mode=>mode.id===active_mode);
const categories=catalog?[catalog.navigation.all_categories_label,...new Set(cases.map(item=>item.category))]:[];
const visible_cases=catalog?cases.filter(item=>active_category===catalog.navigation.all_categories_label||item.category===active_category):[];
return <><Gallery_Toolbar socket_state={socket_state} socket_message={socket_message} streams_paused={streams_paused} on_toggle_streams={()=>set_streams_paused(value=>!value)}/><Container maxWidth={false} sx={{py:3}}><Stack spacing={2.5}>{!catalog&&socket_state!=="error"&&<Box sx={{display:"grid",placeItems:"center",minHeight:"55vh",gap:2}}><CircularProgress/><Typography></Typography></Box>}{!catalog&&socket_state==="error"&&<Alert severity="error">{socket_message}</Alert>}{catalog&&<><Gallery_Summary catalog={catalog}/><Frame_Mode_Tabs frame_modes={modes} active_mode={active_mode} on_change={id=>{set_active_mode(id);set_selected_plot(null);}}/><Category_Filter categories={categories} active_category={active_category} on_change={set_active_category}/>{frame_mode&&<><Typography color="text.secondary">{frame_mode.description}</Typography><Gallery_Page cases={visible_cases} frame_mode={frame_mode} streams_paused={streams_paused} on_open_inspector={set_selected_plot}/></>}</>}</Stack></Container><Inspector_Drawer open={selected_plot!==null} session={selected_plot?.session??null} active_tab={selected_inspector_tab} on_change_tab={set_selected_inspector_tab} on_close={()=>set_selected_plot(null)}/></>;
const {catalog, state: socket_state, message: socket_message} = use_gallery_catalog();
const [active_mode, set_active_mode] = useState("");
const [active_category, set_active_category] = useState("");
const [streams_paused, set_streams_paused] = useState(false);
const [selected_plot, set_selected_plot] = useState<Selected_Plot | null>(null);
const [selected_inspector_tab, set_selected_inspector_tab] = useState("controls");
const [, set_visibility_epoch] = useState(0);
const theme = useTheme();
const compact_layout = useMediaQuery(theme.breakpoints.down("md"));
useEffect(() => {
const change = () => set_visibility_epoch(value => value + 1);
document.addEventListener("visibilitychange", change);
return () => document.removeEventListener("visibilitychange", change);
}, []);
useEffect(() => {
if (!catalog)
return;
set_active_mode(
catalog.frame_modes.some(mode => mode.id === catalog.navigation.default_mode)
? catalog.navigation.default_mode
: catalog.frame_modes[0]?.id ?? "",
);
set_active_category(catalog.navigation.all_categories_label);
}, [catalog]);
const modes = useMemo(
() => [...(catalog?.frame_modes ?? [])].sort((left, right) => left.order - right.order),
[catalog],
);
const cases = useMemo(
() => [...(catalog?.cases ?? [])].sort((left, right) => left.order - right.order),
[catalog],
);
const frame_mode = modes.find(mode => mode.id === active_mode);
const categories = catalog
? [catalog.navigation.all_categories_label, ...new Set(cases.map(item => item.category))]
: [];
const visible_cases = catalog
? cases.filter(item => active_category === catalog.navigation.all_categories_label || item.category === active_category)
: [];
const gallery = <Box sx={{height: "100%", overflow: "auto"}}>
<Container maxWidth={false} sx={{py: 3}}>
<Stack spacing={2.5}>
{!catalog && socket_state !== "error" && <Box sx={{display: "grid", placeItems: "center", minHeight: "55vh", gap: 2}}>
<CircularProgress/>
<Typography></Typography>
</Box>}
{!catalog && socket_state === "error" && <Alert severity="error">{socket_message}</Alert>}
{catalog && <>
<Gallery_Summary catalog={catalog}/>
<Frame_Mode_Tabs
frame_modes={modes}
active_mode={active_mode}
on_change={id => {
set_active_mode(id);
set_selected_plot(null);
}}
/>
<Category_Filter
categories={categories}
active_category={active_category}
on_change={set_active_category}
/>
{frame_mode && <>
<Typography color="text.secondary">{frame_mode.description}</Typography>
<Gallery_Page
cases={visible_cases}
frame_mode={frame_mode}
streams_paused={streams_paused}
on_open_inspector={set_selected_plot}
/>
</>}
</>}
</Stack>
</Container>
</Box>;
return <Box sx={{height: "100dvh", display: "flex", flexDirection: "column", overflow: "hidden"}}>
<Gallery_Toolbar
socket_state={socket_state}
socket_message={socket_message}
streams_paused={streams_paused}
on_toggle_streams={() => set_streams_paused(value => !value)}
/>
<Box component="main" sx={{flex: 1, minHeight: 0}}>
<Group
id="gallery-inspector-layout"
orientation={compact_layout ? "vertical" : "horizontal"}
defaultLayout={selected_plot ? {gallery: 60, inspector: 40} : {gallery: 100}}
style={{height: "100%"}}
>
<Panel id="gallery" minSize={compact_layout ? "220px" : "420px"}>
{gallery}
</Panel>
{selected_plot && <>
<Separator
id="inspector-resize-handle"
aria-label="拖动调整图库和操作面板占比"
style={{
background: "rgba(89, 214, 197, .32)",
width: compact_layout ? "100%" : 8,
height: compact_layout ? 8 : "100%",
cursor: compact_layout ? "row-resize" : "col-resize",
touchAction: "none",
transition: "background-color 120ms ease",
}}
/>
<Panel
id="inspector"
defaultSize="40%"
minSize={compact_layout ? "260px" : "360px"}
maxSize="72%"
collapsible
collapsedSize={0}
>
<Inspector_Panel
session={selected_plot.session}
active_tab={selected_inspector_tab}
on_change_tab={set_selected_inspector_tab}
on_close={() => set_selected_plot(null)}
/>
</Panel>
</>}
</Group>
</Box>
</Box>;
}
+85 -3
View File
@@ -1,3 +1,85 @@
import {Background,Controls,MiniMap,ReactFlow} from "@xyflow/react";import {Box} from "@mui/material";import {useEffect,useMemo,useState} from "react";import type {Gallery_Captured_Frame,Gallery_Node_Statistics,Gallery_Render_Plan} from "../protocol/gallery_types";import {build_dag_model} from "./dag_model";import {layout_dag} from "./dag_layout";import {Dag_Node} from "./dag_node";import type {Dag_View_Model} from "./dag_types";
const node_types={dag_node:Dag_Node};
export function Render_Dag({plan,frame=null,statistics=[],selected_node_id,on_select_node}:{plan:Gallery_Render_Plan;frame?:Gallery_Captured_Frame|null;statistics?:Gallery_Node_Statistics[];selected_node_id:number|null;on_select_node:(id:number)=>void}) {const source=useMemo(()=>build_dag_model(plan,frame,statistics,selected_node_id),[plan,frame,statistics,selected_node_id]);const [model,set_model]=useState<Dag_View_Model>(source);useEffect(()=>{let current=true;void layout_dag(source).then(value=>current&&set_model(value));return()=>{current=false;};},[source]);return <Box sx={{height:520,border:"1px solid rgba(132,179,206,.2)",borderRadius:2,overflow:"hidden"}}><ReactFlow nodes={model.nodes} edges={model.edges} nodeTypes={node_types} fitView nodesDraggable={false} nodesConnectable={false} onNodeClick={(_,node)=>on_select_node(Number(node.id))}><Background/><MiniMap/><Controls/></ReactFlow></Box>;}
import {Background, Controls, MiniMap, ReactFlow} from "@xyflow/react";
import {Box, CircularProgress} from "@mui/material";
import {useEffect, useMemo, useState} from "react";
import type {
Gallery_Captured_Frame,
Gallery_Node_Statistics,
Gallery_Render_Plan,
} from "../protocol/gallery_types";
import {layout_dag} from "./dag_layout";
import {build_dag_model} from "./dag_model";
import {Dag_Node} from "./dag_node";
import type {Dag_View_Model} from "./dag_types";
const node_types = {dag_node: Dag_Node};
export function Render_Dag({
plan,
frame = null,
statistics = [],
selected_node_id,
on_select_node,
}: {
plan: Gallery_Render_Plan;
frame?: Gallery_Captured_Frame | null;
statistics?: Gallery_Node_Statistics[];
selected_node_id: number | null;
on_select_node: (id: number) => void;
}) {
const source = useMemo(
() => build_dag_model(plan, frame, statistics, selected_node_id),
[plan, frame, statistics, selected_node_id],
);
const layout_identity = useMemo(
() => `${plan.nodes.map(node => node.node_id).join(",")}:${plan.edges.map(edge => `${edge.from}>${edge.to}`).join(",")}`,
[plan],
);
const [layout, set_layout] = useState<{identity: string; model: Dag_View_Model} | null>(null);
useEffect(() => {
let current = true;
void layout_dag(source).then(value => {
if (current)
set_layout({identity: layout_identity, model: value});
});
return () => {
current = false;
};
// Selection and telemetry only decorate nodes; they must not restart ELK layout.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [layout_identity]);
const laid_out = layout?.identity === layout_identity ? layout.model : null;
const model = useMemo(() => {
if (!laid_out)
return null;
const positions = new Map(laid_out.nodes.map(node => [node.id, node.position]));
return {
...source,
nodes: source.nodes.map(node => ({
...node,
position: positions.get(node.id) ?? node.position,
})),
};
}, [source, laid_out]);
return <Box sx={{height: 520, border: "1px solid rgba(132,179,206,.2)", borderRadius: 2, overflow: "hidden"}}>
{!model
? <Box sx={{height: "100%", display: "grid", placeItems: "center"}}><CircularProgress size={28}/></Box>
: <ReactFlow
key={layout_identity}
nodes={model.nodes}
edges={model.edges}
nodeTypes={node_types}
fitView
onlyRenderVisibleElements
nodesDraggable={false}
nodesConnectable={false}
onNodeClick={(_, node) => on_select_node(Number(node.id))}
>
<Background/>
<MiniMap/>
<Controls/>
</ReactFlow>}
</Box>;
}
@@ -1,4 +0,0 @@
import CloseIcon from "@mui/icons-material/Close";import RefreshIcon from "@mui/icons-material/Refresh";import RestartAltIcon from "@mui/icons-material/RestartAlt";
import {Box,Drawer,IconButton,Stack,Tooltip,Typography} from "@mui/material";import {useState,useSyncExternalStore} from "react";import type {Gallery_Plot_Session} from "../session/gallery_plot_session";import {EMPTY_PLOT_SNAPSHOT} from "../session/gallery_plot_snapshot";import {Inspector_Tabs} from "./inspector_tabs";import {Controls_Panel} from "./controls_panel";import {Actions_Panel} from "./actions_panel";import {Observer_Panel} from "./observer_panel";import {Performance_Panel} from "./performance_panel";import {Capture_Panel} from "../capture/capture_panel";import {Render_Dag} from "../dag/render_dag";import {Empty_State} from "../common/empty_state";
const empty_subscribe=()=>()=>undefined;const empty_snapshot=()=>EMPTY_PLOT_SNAPSHOT;
export function Inspector_Drawer({open,session,active_tab,on_change_tab,on_close}:{open:boolean;session:Gallery_Plot_Session|null;active_tab:string;on_change_tab:(tab:string)=>void;on_close:()=>void}) {const snapshot=useSyncExternalStore(session?.subscribe??empty_subscribe,session?.get_snapshot??empty_snapshot,session?.get_snapshot??empty_snapshot);const [selected_node_id,set_selected_node_id]=useState<number|null>(null);let body=null;if(session){if(active_tab==="controls")body=<Controls_Panel controls={snapshot.controls} on_patch={(target,patch)=>session.patch(target,patch)}/>;else if(active_tab==="actions")body=<Actions_Panel actions={snapshot.actions} on_action={(action,argument)=>{session.action(action,argument);setTimeout(()=>session.request_frame(performance.now(),true),40);}}/>;else if(active_tab==="observer")body=<Observer_Panel telemetry={snapshot.telemetry}/>;else if(active_tab==="performance")body=<Performance_Panel telemetry={snapshot.telemetry}/>;else if(active_tab==="performance_capture")body=snapshot.performance_capture?<Capture_Panel capture={snapshot.performance_capture} on_capture={(action,count)=>{session.action(action,count);setTimeout(()=>session.request_frame(performance.now(),true),40);}}/>:<Empty_State message="尚无 Performance Capture 数据"/>;else if(active_tab==="render_plan")body=snapshot.render_plan?<Render_Dag plan={snapshot.render_plan} selected_node_id={selected_node_id} on_select_node={set_selected_node_id}/>:<Empty_State message="场景尚未编译 Render Plan"/>;}return <Drawer anchor="right" open={open} onClose={on_close} PaperProps={{sx:{width:{xs:"100%",md:"min(900px,72vw)"}}}}><Stack direction="row" alignItems="center" sx={{p:2,pb:1}}><Box flex={1}><Typography variant="overline" color="primary.main">{session&&`${session.frame_mode.strategy} / ${session.gallery_case.component}`}</Typography><Typography variant="h5">{session?.gallery_case.title??"Inspector"}</Typography></Box><Tooltip title="重置监测"><IconButton onClick={()=>session?.reset_monitoring()}><RestartAltIcon/></IconButton></Tooltip><Tooltip title="刷新当前页"><IconButton onClick={()=>session?.refresh()}><RefreshIcon/></IconButton></Tooltip><IconButton onClick={on_close}><CloseIcon/></IconButton></Stack><Inspector_Tabs active_tab={active_tab} on_change={on_change_tab}/><Box sx={{p:2,overflow:"auto",flex:1}}>{body}</Box></Drawer>;}
@@ -0,0 +1,90 @@
import CloseIcon from "@mui/icons-material/Close";
import RefreshIcon from "@mui/icons-material/Refresh";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import {Box, IconButton, Paper, Stack, Tooltip, Typography} from "@mui/material";
import {useEffect, useState, useSyncExternalStore} from "react";
import {Capture_Panel} from "../capture/capture_panel";
import {Empty_State} from "../common/empty_state";
import {Render_Dag} from "../dag/render_dag";
import type {Gallery_Plot_Session} from "../session/gallery_plot_session";
import {Actions_Panel} from "./actions_panel";
import {Controls_Panel} from "./controls_panel";
import {Inspector_Tabs} from "./inspector_tabs";
import {Observer_Panel} from "./observer_panel";
import {Performance_Panel} from "./performance_panel";
export function Inspector_Panel({
session,
active_tab,
on_change_tab,
on_close,
}: {
session: Gallery_Plot_Session;
active_tab: string;
on_change_tab: (tab: string) => void;
on_close: () => void;
}) {
const snapshot = useSyncExternalStore(
session.subscribe,
session.get_snapshot,
session.get_snapshot,
);
const [selected_node_id, set_selected_node_id] = useState<number | null>(null);
useEffect(() => set_selected_node_id(null), [session]);
let body = null;
if (active_tab === "controls")
body = <Controls_Panel controls={snapshot.controls} on_patch={(target, patch) => session.patch(target, patch)}/>;
else if (active_tab === "actions")
body = <Actions_Panel actions={snapshot.actions} on_action={(action, argument) => {
session.action(action, argument);
setTimeout(() => session.request_frame(performance.now(), true), 40);
}}/>;
else if (active_tab === "observer")
body = <Observer_Panel telemetry={snapshot.telemetry}/>;
else if (active_tab === "performance")
body = <Performance_Panel telemetry={snapshot.telemetry}/>;
else if (active_tab === "performance_capture")
body = snapshot.performance_capture
? <Capture_Panel capture={snapshot.performance_capture} on_capture={(action, count) => {
session.action(action, count);
setTimeout(() => session.request_frame(performance.now(), true), 40);
}}/>
: <Empty_State message="尚无 Performance Capture 数据"/>;
else if (active_tab === "render_plan")
body = snapshot.render_plan
? <Render_Dag
plan={snapshot.render_plan}
selected_node_id={selected_node_id}
on_select_node={set_selected_node_id}
/>
: <Empty_State message="场景尚未编译 Render Plan"/>;
return <Paper
square
elevation={0}
data-testid="inspector-panel"
sx={{height: "100%", minWidth: 0, display: "flex", flexDirection: "column", overflow: "hidden"}}
>
<Stack direction="row" alignItems="center" sx={{p: 2, pb: 1}}>
<Box minWidth={0} flex={1}>
<Typography variant="overline" color="primary.main" noWrap>
{`${session.frame_mode.strategy} / ${session.gallery_case.component}`}
</Typography>
<Typography variant="h5" noWrap>{session.gallery_case.title}</Typography>
</Box>
<Tooltip title="重置监测">
<IconButton onClick={() => session.reset_monitoring()}><RestartAltIcon/></IconButton>
</Tooltip>
<Tooltip title="刷新当前页">
<IconButton onClick={() => session.refresh()}><RefreshIcon/></IconButton>
</Tooltip>
<Tooltip title="隐藏操作面板">
<IconButton aria-label="隐藏操作面板" onClick={on_close}><CloseIcon/></IconButton>
</Tooltip>
</Stack>
<Inspector_Tabs active_tab={active_tab} on_change={on_change_tab}/>
<Box sx={{p: 2, overflow: "auto", flex: 1, minHeight: 0}}>{body}</Box>
</Paper>;
}
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/theme.ts","./src/capture/capture_frames.tsx","./src/capture/capture_panel.tsx","./src/capture/capture_sessions.tsx","./src/capture/capture_toolbar.tsx","./src/capture/frame_summary.tsx","./src/capture/node_detail.tsx","./src/capture/plan_comparison.tsx","./src/capture/worker_timeline.tsx","./src/common/copy_button.tsx","./src/common/empty_state.tsx","./src/common/json_viewer.tsx","./src/common/metric_grid.tsx","./src/common/metric_value.tsx","./src/common/status_chip.tsx","./src/dag/dag_layout.ts","./src/dag/dag_legend.tsx","./src/dag/dag_model.ts","./src/dag/dag_node.tsx","./src/dag/dag_types.ts","./src/dag/render_dag.tsx","./src/gallery/category_filter.tsx","./src/gallery/frame_mode_tabs.tsx","./src/gallery/gallery_page.tsx","./src/gallery/gallery_summary.tsx","./src/gallery/gallery_toolbar.tsx","./src/gallery/plot_card.tsx","./src/hooks/use_element_size.ts","./src/hooks/use_gallery_catalog.ts","./src/hooks/use_plot_session.ts","./src/inspector/actions_panel.tsx","./src/inspector/control_field.tsx","./src/inspector/controls_panel.tsx","./src/inspector/inspector_drawer.tsx","./src/inspector/inspector_tabs.tsx","./src/inspector/observer_panel.tsx","./src/inspector/performance_panel.tsx","./src/plot/kernel_observer_summary.tsx","./src/plot/performance_strip.tsx","./src/plot/plot_canvas.tsx","./src/plot/plot_status.tsx","./src/protocol/format.ts","./src/protocol/gallery_descriptor.ts","./src/protocol/gallery_messages.ts","./src/protocol/gallery_parser.ts","./src/protocol/gallery_types.ts","./src/protocol/pixel_frame.ts","./src/runtime/client_performance.ts","./src/runtime/frame_request_controller.ts","./src/runtime/pixel_presenter.ts","./src/session/gallery_plot_session.ts","./src/session/gallery_plot_snapshot.ts","./src/transport/catalog_loader.ts","./src/transport/gallery_socket.ts","./tests/setup.ts","./tests/components/control_field.test.tsx","./tests/dag/dag_model.test.ts","./tests/e2e/gallery.spec.ts","./tests/protocol/gallery_parser.test.ts","./tests/protocol/pixel_frame.test.ts","./tests/runtime/client_performance.test.ts","./tests/runtime/frame_request_controller.test.ts","./vite.config.ts"],"version":"5.9.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/theme.ts","./src/capture/capture_frames.tsx","./src/capture/capture_panel.tsx","./src/capture/capture_sessions.tsx","./src/capture/capture_toolbar.tsx","./src/capture/frame_summary.tsx","./src/capture/node_detail.tsx","./src/capture/plan_comparison.tsx","./src/capture/worker_timeline.tsx","./src/common/copy_button.tsx","./src/common/empty_state.tsx","./src/common/json_viewer.tsx","./src/common/metric_grid.tsx","./src/common/metric_value.tsx","./src/common/status_chip.tsx","./src/dag/dag_layout.ts","./src/dag/dag_legend.tsx","./src/dag/dag_model.ts","./src/dag/dag_node.tsx","./src/dag/dag_types.ts","./src/dag/render_dag.tsx","./src/gallery/category_filter.tsx","./src/gallery/frame_mode_tabs.tsx","./src/gallery/gallery_page.tsx","./src/gallery/gallery_summary.tsx","./src/gallery/gallery_toolbar.tsx","./src/gallery/plot_card.tsx","./src/hooks/use_element_size.ts","./src/hooks/use_gallery_catalog.ts","./src/hooks/use_plot_session.ts","./src/inspector/actions_panel.tsx","./src/inspector/control_field.tsx","./src/inspector/controls_panel.tsx","./src/inspector/inspector_panel.tsx","./src/inspector/inspector_tabs.tsx","./src/inspector/observer_panel.tsx","./src/inspector/performance_panel.tsx","./src/plot/kernel_observer_summary.tsx","./src/plot/performance_strip.tsx","./src/plot/plot_canvas.tsx","./src/plot/plot_status.tsx","./src/protocol/format.ts","./src/protocol/gallery_descriptor.ts","./src/protocol/gallery_messages.ts","./src/protocol/gallery_parser.ts","./src/protocol/gallery_types.ts","./src/protocol/pixel_frame.ts","./src/runtime/client_performance.ts","./src/runtime/frame_request_controller.ts","./src/runtime/pixel_presenter.ts","./src/session/gallery_plot_session.ts","./src/session/gallery_plot_snapshot.ts","./src/transport/catalog_loader.ts","./src/transport/gallery_socket.ts","./tests/setup.ts","./tests/components/control_field.test.tsx","./tests/dag/dag_model.test.ts","./tests/e2e/gallery.spec.ts","./tests/protocol/gallery_parser.test.ts","./tests/protocol/pixel_frame.test.ts","./tests/runtime/client_performance.test.ts","./tests/runtime/frame_request_controller.test.ts","./vite.config.ts"],"version":"5.9.3"}