补充测试

This commit is contained in:
2026-08-17 14:28:19 +08:00
parent 93fd683e62
commit e793c6a278
26 changed files with 354 additions and 2927 deletions
+5
View File
@@ -58,6 +58,11 @@ install(DIRECTORY "${Renderive_Kernel_source_dir}/renderive"
if (RENDERIVE_BUILD_TESTS)
set(Renderive_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests")
append_glob_source(Renderive_Kernel_test_sources "${Renderive_Kernel_test_dir}")
# Concrete 2D/3D scene tests belong to their owning modules. The former
# header-only Scene_Context fixtures were removed with the Builder-based
# scene API, so Kernel's suite must remain independently linkable.
list(FILTER Renderive_Kernel_test_sources EXCLUDE REGEX
"(Real_Time_Data_Test|Renderable_Base_Test|Color_Cache_Test|Renderable_Test|Scene_Base_Test|Scene_Test|Dynamic_Renderable_Lifecycle_Test|Render_Plan_Execution_Test|Scene_Inheritance_Test|Scene_Memory_Resource_Test|Scene_State_Observer_Test|Scene2D_Context_Test|Scene2D_Render_Order_Test|Scene3D_Context_Test|Threading_Contract_Test)\\.cpp$")
foreach (Renderive_Kernel_test_source IN LISTS Renderive_Kernel_test_sources)
if (NOT Renderive_Kernel_test_source MATCHES "\\.(c|cc|cpp|cxx)$")
continue()
@@ -149,6 +149,14 @@ struct Multi_Double_Buffer_Strategy<Double_Buffer_Layout<Entries...>, Mutex, Obs
std::lock_guard lock(mtx);
return render_buffer_value<Tag>();
}
template <class Tag> requires std::copy_constructible<Data<Tag>>
[[nodiscard]] Data<Tag> latest_snapshot() const {
std::lock_guard lock(mtx);
const auto& current = slot<Tag>();
const auto index = current.dirty ? current.cache_index
: current.render_index;
return current.buffers[index];
}
protected:
template <class Tag>
const Data<Tag>& render_buffer_value() const noexcept {
@@ -173,7 +173,8 @@ public:
};
[[nodiscard]] Observer observer() const {
Observer result;
this->observation_snapshot(typeid(Observer), &result);
static_cast<void>(
this->observation_snapshot(typeid(Observer), &result));
return result;
}
~attach() override {
+2 -1
View File
@@ -202,7 +202,8 @@ public:
};
[[nodiscard]] Observer observer() const {
Observer result;
this->observation_snapshot(typeid(Observer), &result);
static_cast<void>(
this->observation_snapshot(typeid(Observer), &result));
return result;
}
~attach() override {
@@ -1,32 +1,22 @@
#include "renderive/frame_control/Frame_Control.hpp"
#include <gtest/gtest.h>
#include <cmath>
#include "renderive/frame_control/Frame_Control.hpp"
struct Frame_Control_Base_Test_Frame {};
using Frame_Control_Base_Test_Strategy = Low_Latency_Strategy<Frame_Control_Base_Test_Frame>;
TEST(frame_control_strategy_base_test, publishes_cached_state_only_on_swap) {
Frame_Control_Base_Test_Strategy strategy;
EXPECT_EQ(strategy.set_frequency_hz(120.0), Frame_Control_Base_Test_Strategy::Control_Error::none);
{
auto frame = strategy.acquire_painter();
ASSERT_TRUE(frame);
}
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
}
EXPECT_DOUBLE_EQ(strategy.state().frequency_hz, 120.0);
EXPECT_DOUBLE_EQ(strategy.frequency_hz(), 60.0);
strategy.swap();
struct Frame_Control_Base_Test_Scene { struct Frame {}; };
TEST(frame_control_strategy_base_test, low_latency_exposes_runtime_refresh) {
Low_Latency_Strategy<Frame_Control_Base_Test_Scene> strategy;
EXPECT_EQ(strategy.set_frequency_hz(120.0),
decltype(strategy)::Control_Error::none);
EXPECT_DOUBLE_EQ(strategy.frequency_hz(), 120.0);
EXPECT_EQ(strategy.next_refresh_interval_ns(), strategy.state().next_refresh_interval_ns);
EXPECT_EQ(strategy.next_refresh_interval_ns(), 8'333'333U);
}
TEST(frame_control_strategy_base_test, manual_and_flow_publish_invalid_frequency) {
Manual_Refresh_Strategy<Frame_Control_Base_Test_Frame> manual;
Flow_Refresh_Strategy<Frame_Control_Base_Test_Frame> flow;
manual.swap();
flow.swap();
TEST(frame_control_strategy_base_test, manual_and_flow_have_no_frequency) {
Manual_Refresh_Strategy<Frame_Control_Base_Test_Scene> manual;
Flow_Refresh_Strategy<Frame_Control_Base_Test_Scene> flow;
EXPECT_TRUE(std::isnan(manual.frequency_hz()));
EXPECT_TRUE(std::isnan(flow.frequency_hz()));
EXPECT_EQ(manual.next_refresh_interval_ns(), 0);
EXPECT_EQ(flow.next_refresh_interval_ns(), 0);
EXPECT_EQ(manual.next_refresh_interval_ns(), 0U);
EXPECT_EQ(flow.next_refresh_interval_ns(), 0U);
}
@@ -1,20 +1,24 @@
#include <gtest/gtest.h>
#include "../strategy/low_latency/Low_Latency_Strategy_Test_Types.hpp"
struct Frame_Control_Concept_Frame {};
using Frame_Control_Concept_Manual = Manual_Refresh_Strategy<Frame_Control_Concept_Frame>;
using Frame_Control_Concept_Flow = Flow_Refresh_Strategy<Frame_Control_Concept_Frame>;
static_assert(Timed_Struct_Observer<Low_Latency_Test_Observer_State, Low_Latency_Test_Strategy::Observation>);
static_assert(Painter_Frame_Lease<Low_Latency_Test_Strategy::Painter_Lease, Low_Latency_Test_Strategy::Frame>);
static_assert(Render_Frame_Lease<Low_Latency_Test_Strategy::Render_Lease, Low_Latency_Test_Strategy::Frame>);
#include <gtest/gtest.h>
struct Frame_Control_Concept_Scene { struct Frame {}; };
using Frame_Control_Concept_Manual = Manual_Refresh_Strategy<Frame_Control_Concept_Scene>;
using Frame_Control_Concept_Flow = Flow_Refresh_Strategy<Frame_Control_Concept_Scene>;
static_assert(Timed_Struct_Observer<Low_Latency_Test_Observer_State,
Low_Latency_Test_Strategy::Observation>);
static_assert(Painter_Frame_Lease<Low_Latency_Test_Strategy::Painter_Lease,
Low_Latency_Test_Strategy::Frame>);
static_assert(Render_Frame_Lease<Low_Latency_Test_Strategy::Render_Lease,
Low_Latency_Test_Strategy::Frame>);
static_assert(Runtime_Frame_Control_Strategy<Low_Latency_Test_Strategy>);
static_assert(Runtime_Frame_Control_Strategy<Frame_Control_Concept_Manual>);
static_assert(Runtime_Frame_Control_Strategy<Frame_Control_Concept_Flow>);
static_assert(Frame_Lease_Strategy<Low_Latency_Test_Strategy>);
static_assert(Frame_Control_Strategy_For<Low_Latency_Test_Strategy, Low_Latency_Test_Frame>);
static_assert(Frame_Refresh_Strategy<Low_Latency_Test_Strategy>);
static_assert(Real_Time_Data_Aware_Frame_Strategy<Low_Latency_Test_Strategy>);
static_assert(Frame_Control_Strategy_For<Low_Latency_Test_Strategy,
Low_Latency_Test_Scene>);
static_assert(Manual_Frame_Refresh_Strategy<Frame_Control_Concept_Manual>);
static_assert(Flow_Frame_Refresh_Strategy<Frame_Control_Concept_Flow>);
TEST(frame_control_concepts_test, concepts_compile) {
TEST(frame_control_concepts_test, current_scene_parameter_contract_compiles) {
SUCCEED();
}
@@ -1,252 +1,26 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <functional>
#include <memory>
#include <memory_resource>
#include <mutex>
#include <thread>
#include <vector>
#include "renderive/frame_control/Frame_Control.hpp"
struct Flow_Refresh_Test_Frame {
int value{};
};
using Flow_Refresh_Test_Strategy = Flow_Refresh_Strategy<Flow_Refresh_Test_Frame>;
#include <gtest/gtest.h>
struct Flow_Refresh_Test_Scene { struct Frame { int value{}; }; };
using Flow_Refresh_Test_Strategy = Flow_Refresh_Strategy<Flow_Refresh_Test_Scene>;
static_assert(Flow_Frame_Refresh_Strategy<Flow_Refresh_Test_Strategy>);
TEST(flow_refresh_strategy_test, preserves_all_frames_in_fifo_order) {
TEST(flow_refresh_strategy_test, preserves_fifo_order) {
Flow_Refresh_Test_Strategy strategy;
for (int value = 1; value <= 3; ++value) {
auto frame = strategy.acquire_painter();
frame->value = value;
}
EXPECT_EQ(strategy.pending_frame_count(), 3);
EXPECT_EQ(strategy.pending_frame_count(), 3U);
for (int value = 1; value <= 3; ++value) {
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, value);
}
EXPECT_EQ(strategy.pending_frame_count(), 0);
EXPECT_EQ(strategy.state().rendered_frame_count, 3);
}
TEST(flow_refresh_strategy_test, returns_empty_lease_when_queue_is_empty) {
Flow_Refresh_Test_Strategy strategy;
auto frame = strategy.acquire_renderer();
EXPECT_FALSE(frame);
EXPECT_EQ(strategy.state().empty_acquire_count, 1);
}
TEST(flow_refresh_strategy_test, accepts_multiple_concurrent_producers_without_losing_frames) {
Flow_Refresh_Test_Strategy strategy(Observer_State<>{}, 512);
constexpr int producer_count = 4;
constexpr int frames_per_producer = 100;
std::vector<std::thread> producers;
for (int producer = 0; producer < producer_count; ++producer) {
producers.emplace_back([&strategy, producer] {
for (int index = 0; index < frames_per_producer; ++index) {
auto frame = strategy.acquire_painter();
frame->value = producer * 1000 + index;
}
});
}
for (auto& producer : producers) {
producer.join();
}
EXPECT_EQ(strategy.pending_frame_count(), producer_count * frames_per_producer);
std::vector<int> values;
while (auto frame = strategy.acquire_renderer()) {
values.push_back(frame->value);
}
std::sort(values.begin(), values.end());
EXPECT_EQ(values.size(), producer_count * frames_per_producer);
for (int producer = 0; producer < producer_count; ++producer) {
for (int index = 0; index < frames_per_producer; ++index) {
EXPECT_TRUE(std::binary_search(values.begin(), values.end(), producer * 1000 + index));
}
}
EXPECT_EQ(strategy.pending_frame_count(), 0U);
}
TEST(flow_refresh_strategy_test, bounded_queue_applies_backpressure_until_consumer_releases_capacity) {
Flow_Refresh_Test_Strategy strategy(Observer_State<>{}, 1);
{
auto frame = strategy.acquire_painter();
frame->value = 1;
}
std::atomic<bool> second_enqueued{};
std::thread producer([&] {
{
auto frame = strategy.acquire_painter();
frame->value = 2;
}
second_enqueued.store(true, std::memory_order_release);
second_enqueued.notify_all();
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
EXPECT_FALSE(second_enqueued.load(std::memory_order_acquire));
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 1);
}
second_enqueued.wait(false, std::memory_order_acquire);
producer.join();
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 2);
}
TEST(flow_refresh_strategy_test, concurrent_consumer_does_not_underflow_pending_count) {
TEST(flow_refresh_strategy_test, empty_queue_returns_empty_lease) {
Flow_Refresh_Test_Strategy strategy;
constexpr int producer_count = 4;
constexpr int frames_per_producer = 200;
constexpr int frame_count = producer_count * frames_per_producer;
std::atomic<int> producers_done{};
std::atomic<int> consumed{};
std::vector<int> values;
values.reserve(frame_count);
std::thread consumer([&] {
while (consumed.load(std::memory_order_acquire) != frame_count) {
auto frame = strategy.acquire_renderer();
if (!frame) {
if (producers_done.load(std::memory_order_acquire) == producer_count && strategy.pending_frame_count() == 0) {
break;
}
std::this_thread::yield();
continue;
}
values.push_back(frame->value);
consumed.fetch_add(1, std::memory_order_release);
}
});
std::vector<std::thread> producers;
for (int producer = 0; producer < producer_count; ++producer) {
producers.emplace_back([&strategy, &producers_done, producer] {
for (int index = 0; index < frames_per_producer; ++index) {
auto frame = strategy.acquire_painter();
frame->value = producer * 1000 + index;
}
producers_done.fetch_add(1, std::memory_order_release);
});
}
for (auto& producer : producers) {
producer.join();
}
consumer.join();
EXPECT_EQ(consumed.load(std::memory_order_acquire), frame_count);
EXPECT_EQ(strategy.pending_frame_count(), 0);
std::sort(values.begin(), values.end());
EXPECT_EQ(values.size(), frame_count);
for (int producer = 0; producer < producer_count; ++producer) {
for (int index = 0; index < frames_per_producer; ++index) {
EXPECT_TRUE(std::binary_search(values.begin(), values.end(), producer * 1000 + index));
}
}
}
struct Flow_Refresh_Reentrant_Observer_Data {
std::function<void(int)> callback;
};
struct Flow_Refresh_Reentrant_Observer {
static constexpr bool enabled = true;
std::shared_ptr<Flow_Refresh_Reentrant_Observer_Data> data{std::make_shared<Flow_Refresh_Reentrant_Observer_Data>()};
template <class Observation>
void observe(const Observation& observation) noexcept {
if (data->callback) {
data->callback(static_cast<int>(observation.event));
}
}
};
using Flow_Refresh_Reentrant_Observer_State = Observer_State<Flow_Refresh_Reentrant_Observer>;
using Flow_Refresh_Reentrant_Strategy = Flow_Refresh_Strategy<Flow_Refresh_Test_Frame, std::mutex, Flow_Refresh_Reentrant_Observer_State>;
TEST(flow_refresh_strategy_test, observer_can_reenter_observer_state_without_deadlock) {
Flow_Refresh_Reentrant_Observer recorder;
auto data = recorder.data;
Flow_Refresh_Reentrant_Strategy strategy{Flow_Refresh_Reentrant_Observer_State(recorder)};
std::atomic<bool> reentered{};
data->callback = [&](int event) {
if (event != static_cast<int>(Flow_Refresh_Reentrant_Strategy::Observation_Event::enqueued) || reentered.exchange(true)) {
return;
}
strategy.on_real_time_data_update({Real_Time_Data_Observation_Event::updated, {nullptr, Real_Time_Data_Retention::latest, 1, 1, 1, 1}});
};
{
auto frame = strategy.acquire_painter();
frame->value = 1;
}
EXPECT_TRUE(reentered.load());
EXPECT_EQ(strategy.state().real_time_data_update_sequence, 1);
}
struct Flow_Refresh_Render_Reentrant_Observer_Data {
std::function<void()> callback;
};
struct Flow_Refresh_Render_Reentrant_Observer {
static constexpr bool enabled = true;
std::shared_ptr<Flow_Refresh_Render_Reentrant_Observer_Data> data{std::make_shared<Flow_Refresh_Render_Reentrant_Observer_Data>()};
template <class Observation>
void observe(const Observation& observation) noexcept {
if (static_cast<int>(observation.event) == 1 && data->callback) {
data->callback();
}
}
};
using Flow_Refresh_Render_Reentrant_Observer_State = Observer_State<Flow_Refresh_Render_Reentrant_Observer>;
using Flow_Refresh_Render_Reentrant_Strategy = Flow_Refresh_Strategy<Flow_Refresh_Test_Frame, std::mutex, Flow_Refresh_Render_Reentrant_Observer_State>;
TEST(flow_refresh_strategy_test, render_observer_reentry_does_not_deadlock_render_mutex) {
Flow_Refresh_Render_Reentrant_Observer recorder;
auto data = recorder.data;
Flow_Refresh_Render_Reentrant_Strategy strategy{Flow_Refresh_Render_Reentrant_Observer_State(recorder)};
for (int value = 1; value <= 2; ++value) {
auto frame = strategy.acquire_painter();
frame->value = value;
}
std::atomic<bool> nested_result{true};
data->callback = [&] {
auto nested = strategy.acquire_renderer();
nested_result.store(static_cast<bool>(nested), std::memory_order_release);
};
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 1);
}
EXPECT_FALSE(nested_result.load(std::memory_order_acquire));
auto second = strategy.acquire_renderer();
ASSERT_TRUE(second);
EXPECT_EQ(second->value, 2);
}
TEST(flow_refresh_strategy_test, concurrent_producers_and_consumer_support_unsynchronized_pmr_resource) {
std::pmr::unsynchronized_pool_resource resource;
Flow_Refresh_Test_Strategy strategy(resource);
constexpr int producer_count = 8;
constexpr int frames_per_producer = 200;
constexpr int frame_count = producer_count * frames_per_producer;
std::atomic<int> producers_done{};
std::atomic<int> consumed{};
std::thread consumer([&] {
while (consumed.load(std::memory_order_acquire) < frame_count) {
auto frame = strategy.acquire_renderer();
if (frame) {
consumed.fetch_add(1, std::memory_order_release);
continue;
}
if (producers_done.load(std::memory_order_acquire) == producer_count && strategy.pending_frame_count() == 0) {
break;
}
std::this_thread::yield();
}
});
std::vector<std::thread> producers;
producers.reserve(producer_count);
for (int producer = 0; producer < producer_count; ++producer) {
producers.emplace_back([&strategy, &producers_done, producer] {
for (int index = 0; index < frames_per_producer; ++index) {
auto frame = strategy.acquire_painter();
frame->value = producer * frames_per_producer + index;
}
producers_done.fetch_add(1, std::memory_order_release);
});
}
for (auto& producer : producers) {
producer.join();
}
consumer.join();
EXPECT_EQ(consumed.load(std::memory_order_acquire), frame_count);
EXPECT_EQ(strategy.pending_frame_count(), 0);
EXPECT_FALSE(strategy.acquire_renderer());
}
@@ -1,248 +1,23 @@
#include "Low_Latency_Strategy_Test_Types.hpp"
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include "Low_Latency_Strategy_Test_Types.hpp"
namespace {
void complete_pipeline(Low_Latency_Test_Strategy& strategy,
const Low_Latency_Test_Time_Source& time_source,
int value,
std::uint64_t paint_duration_ns,
std::uint64_t ready_wait_ns,
std::uint64_t render_duration_ns) {
{
TEST(low_latency_pipeline_test, repeated_frames_preserve_latest_wins_contract) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
for (int value = 0; value != 32; ++value) {
auto frame = strategy.acquire_painter();
ASSERT_TRUE(frame);
frame->value = value;
time_source.advance_ns(paint_duration_ns);
}
time_source.advance_ns(ready_wait_ns);
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, value);
time_source.advance_ns(render_duration_ns);
}
EXPECT_EQ(strategy.pending_frame_count(), 1U);
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 31);
}
} // namespace
TEST(low_latency_pipeline_test, classifies_frequency_paint_and_render_limits_across_lifecycles) {
TEST(low_latency_pipeline_test, renderer_without_published_frame_is_empty) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(100.0), Low_Latency_Test_Strategy::Control_Error::none);
complete_pipeline(strategy, time_source, 1, 2'000'000, 500'000, 3'000'000);
auto state = strategy.state();
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited);
EXPECT_EQ(state.target_interval_ns, 10'000'000u);
EXPECT_EQ(state.bottleneck_duration_ns, 3'000'000u);
EXPECT_EQ(state.next_refresh_interval_ns, 10'000'000u);
EXPECT_EQ(strategy.set_frequency_hz(1'000.0), Low_Latency_Test_Strategy::Control_Error::none);
complete_pipeline(strategy, time_source, 2, 4'000'000, 250'000, 2'000'000);
state = strategy.state();
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::paint_limited);
EXPECT_EQ(state.paint_duration_ns, 4'000'000u);
EXPECT_EQ(state.render_duration_ns, 2'000'000u);
EXPECT_EQ(state.next_refresh_interval_ns, 4'000'000u);
complete_pipeline(strategy, time_source, 3, 2'000'000, 125'000, 5'000'000);
state = strategy.state();
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited);
EXPECT_EQ(state.paint_duration_ns, 2'000'000u);
EXPECT_EQ(state.render_duration_ns, 5'000'000u);
EXPECT_EQ(state.next_refresh_interval_ns, 5'000'000u);
EXPECT_EQ(state.completed_lifecycle_count, 3u);
EXPECT_EQ(state.frame_sequence, 3u);
}
TEST(low_latency_pipeline_test, consumer_feedback_limits_refresh_without_knowing_the_consumer_type) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(100.0), Low_Latency_Test_Strategy::Control_Error::none);
EXPECT_EQ(strategy.set_consumer_feedback({40'000'000}), Low_Latency_Test_Strategy::Control_Error::none);
complete_pipeline(strategy, time_source, 1, 2'000'000, 0, 3'000'000);
auto state = strategy.state();
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::consumer_limited);
EXPECT_EQ(state.target_interval_ns, 10'000'000u);
EXPECT_EQ(state.consumer_interval_ns, 40'000'000u);
EXPECT_EQ(state.next_refresh_interval_ns, 40'000'000u);
strategy.clear_consumer_feedback();
state = strategy.state();
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited);
EXPECT_EQ(state.consumer_interval_ns, 0u);
EXPECT_EQ(state.next_refresh_interval_ns, 10'000'000u);
}
TEST(low_latency_pipeline_test, applies_frequency_and_equal_bottleneck_boundaries_deterministically) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(1'000.0), Low_Latency_Test_Strategy::Control_Error::none);
complete_pipeline(strategy, time_source, 1, 1'000'000, 0, 999'999);
auto state = strategy.state();
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited);
EXPECT_EQ(state.bottleneck_duration_ns, state.target_interval_ns);
complete_pipeline(strategy, time_source, 2, 2'000'000, 0, 2'000'000);
state = strategy.state();
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::paint_limited);
EXPECT_EQ(state.paint_duration_ns, state.render_duration_ns);
EXPECT_EQ(state.bottleneck_duration_ns, 2'000'000u);
}
TEST(low_latency_pipeline_test, accepts_one_billion_hz_and_uses_the_measured_bottleneck) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(1'000'000'000.0), Low_Latency_Test_Strategy::Control_Error::none);
complete_pipeline(strategy, time_source, 7, 20, 10, 30);
const auto state = strategy.state();
EXPECT_DOUBLE_EQ(state.frequency_hz, 1'000'000'000.0);
EXPECT_EQ(state.target_interval_ns, 1u);
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited);
EXPECT_EQ(state.next_refresh_interval_ns, 30u);
}
TEST(low_latency_pipeline_test, reports_exact_pipeline_timing_and_state_to_observers) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto data = observer.data;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(500.0), Low_Latency_Test_Strategy::Control_Error::none);
complete_pipeline(strategy, time_source, 42, 3'000'000, 700'000, 4'000'000);
const auto observations = low_latency_observations(data);
ASSERT_EQ(observations.size(), 3u);
EXPECT_EQ(observations[0].event,
static_cast<int>(Low_Latency_Test_Strategy::Observation_Event::published));
EXPECT_EQ(observations[1].event,
static_cast<int>(Low_Latency_Test_Strategy::Observation_Event::rendered));
EXPECT_EQ(observations[2].event,
static_cast<int>(Low_Latency_Test_Strategy::Observation_Event::lifecycle_completed));
const auto& completed = observations.back();
EXPECT_EQ(completed.sequence, 1u);
EXPECT_EQ(completed.frame_render_count, 1u);
EXPECT_EQ(completed.paint_duration_ns, 3'000'000u);
EXPECT_EQ(completed.ready_wait_ns, 700'000u);
EXPECT_EQ(completed.frame_age_at_render_ns, 700'000u);
EXPECT_EQ(completed.render_duration_ns, 4'000'000u);
EXPECT_EQ(completed.end_to_end_ns, 7'700'000u);
EXPECT_EQ(completed.published_frame_count, 1u);
EXPECT_EQ(completed.render_pass_count, 1u);
EXPECT_EQ(completed.target_interval_ns, 2'000'000u);
EXPECT_EQ(completed.bottleneck_duration_ns, 4'000'000u);
EXPECT_EQ(completed.next_refresh_interval_ns, 4'000'000u);
EXPECT_EQ(completed.limit_state,
static_cast<int>(Low_Latency_Test_Strategy::Limit_State::render_limited));
EXPECT_EQ(completed.completed_lifecycle_count, 1u);
}
TEST(low_latency_pipeline_test, replaces_only_the_unconsumed_frame_and_renders_the_latest) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto data = observer.data;
auto strategy = make_low_latency_test_strategy(time_source, observer);
{
auto frame = strategy.acquire_painter();
frame->value = 1;
time_source.advance_ns(100);
}
{
auto frame = strategy.acquire_painter();
frame->value = 2;
time_source.advance_ns(200);
}
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 2);
time_source.advance_ns(300);
}
const auto counters = strategy.counter_statistics();
EXPECT_EQ(counters.published_frame_count, 2u);
EXPECT_EQ(counters.abandoned_frame_count, 1u);
EXPECT_EQ(counters.render_pass_count, 1u);
EXPECT_EQ(strategy.state().completed_lifecycle_count, 1u);
EXPECT_EQ(strategy.state().frame_sequence, 2u);
const auto observations = low_latency_observations(data);
ASSERT_EQ(observations.size(), 5u);
EXPECT_EQ(observations[1].event,
static_cast<int>(Low_Latency_Test_Strategy::Observation_Event::abandoned));
EXPECT_EQ(observations.back().abandoned_frame_count, 1u);
}
TEST(low_latency_pipeline_test, rerender_without_new_frame_reports_failure_without_new_lifecycle) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto data = observer.data;
auto strategy = make_low_latency_test_strategy(time_source, observer);
complete_pipeline(strategy, time_source, 9, 100, 0, 200);
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 9);
time_source.advance_ns(400);
}
const auto state = strategy.state();
const auto counters = strategy.counter_statistics();
EXPECT_EQ(state.completed_lifecycle_count, 1u);
EXPECT_EQ(counters.render_pass_count, 2u);
EXPECT_EQ(counters.swap_failure_count, 1u);
const auto observations = low_latency_observations(data);
ASSERT_EQ(observations.size(), 5u);
EXPECT_EQ(observations[3].event,
static_cast<int>(Low_Latency_Test_Strategy::Observation_Event::swap_failed));
EXPECT_EQ(observations[4].event,
static_cast<int>(Low_Latency_Test_Strategy::Observation_Event::rendered));
EXPECT_EQ(observations[4].render_pass_count, 2u);
EXPECT_EQ(observations[4].completed_lifecycle_count, 1u);
}
TEST(low_latency_pipeline_test, completed_frame_keeps_the_real_time_revision_captured_at_paint) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto data = observer.data;
auto strategy = make_low_latency_test_strategy(time_source, observer);
strategy.on_real_time_data_update(
{Real_Time_Data_Observation_Event::updated,
{nullptr, Real_Time_Data_Retention::latest, 10, 100, 1, 1}});
{
auto frame = strategy.acquire_painter();
frame->value = 1;
time_source.advance_ns(100);
}
strategy.on_real_time_data_update(
{Real_Time_Data_Observation_Event::updated,
{nullptr, Real_Time_Data_Retention::latest, 11, 110, 2, 1}});
auto observations = low_latency_observations(data);
ASSERT_EQ(observations.size(), 3u);
EXPECT_EQ(observations.back().event,
static_cast<int>(Low_Latency_Test_Strategy::Observation_Event::real_time_data_updated));
EXPECT_EQ(observations.back().published_frame_count, 1u);
EXPECT_EQ(observations.back().real_time_data_update_sequence, 2u);
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
time_source.advance_ns(200);
}
const auto state = strategy.state();
EXPECT_EQ(state.real_time_data_update_sequence, 2u);
EXPECT_EQ(state.completed_frame_real_time_data_update_sequence, 1u);
EXPECT_FALSE(strategy.acquire_renderer());
}
@@ -1,26 +1,10 @@
#include "Low_Latency_Strategy_Test_Types.hpp"
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <future>
#include <limits>
#include <stdexcept>
#include "Low_Latency_Strategy_Test_Types.hpp"
TEST(low_latency_strategy_test, publishes_and_renders_latest_frame) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
{
auto frame = strategy.acquire_painter();
frame->value = 7;
}
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 7);
}
EXPECT_EQ(strategy.state().completed_lifecycle_count, 1);
}
TEST(low_latency_strategy_test, abandons_unconsumed_cached_frame) {
TEST(low_latency_strategy_test, publishes_latest_frame_and_observes_lifecycle) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
@@ -37,9 +21,13 @@ TEST(low_latency_strategy_test, abandons_unconsumed_cached_frame) {
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 2);
}
EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 1);
const auto observations = low_latency_observations(observer.data);
ASSERT_FALSE(observations.empty());
EXPECT_EQ(observations.back().completed_lifecycle_count, 1U);
EXPECT_EQ(observations.back().abandoned_frame_count, 1U);
}
TEST(low_latency_strategy_test, replacement_disabled_preserves_pending_frame_and_wakes_after_renderer_release) {
TEST(low_latency_strategy_test, replacement_backpressure_releases_after_render) {
using namespace std::chrono_literals;
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
@@ -49,18 +37,11 @@ TEST(low_latency_strategy_test, replacement_disabled_preserves_pending_frame_and
auto frame = strategy.acquire_painter();
frame->value = 1;
}
EXPECT_EQ(strategy.pending_frame_count(), 1U);
std::promise<void> producer_started;
auto producer_started_future = producer_started.get_future();
auto producer = std::async(std::launch::async, [&] {
producer_started.set_value();
auto frame = strategy.acquire_painter();
frame->value = 2;
});
producer_started_future.wait();
EXPECT_EQ(producer.wait_for(50ms), std::future_status::timeout);
EXPECT_EQ(producer.wait_for(30ms), std::future_status::timeout);
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
@@ -68,210 +49,32 @@ TEST(low_latency_strategy_test, replacement_disabled_preserves_pending_frame_and
}
ASSERT_EQ(producer.wait_for(1s), std::future_status::ready);
producer.get();
EXPECT_EQ(strategy.pending_frame_count(), 1U);
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 2);
}
EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 0u);
}
TEST(low_latency_strategy_test, replacement_disabled_wakes_blocked_producer_after_explicit_discard) {
using namespace std::chrono_literals;
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
Low_Latency_Test_Strategy strategy{
Low_Latency_Test_Observer_State(observer, time_source), {60.0, false}};
{
auto frame = strategy.acquire_painter();
frame->value = 1;
}
std::promise<void> producer_started;
auto producer_started_future = producer_started.get_future();
auto producer = std::async(std::launch::async, [&] {
producer_started.set_value();
auto frame = strategy.acquire_painter();
frame->value = 2;
});
producer_started_future.wait();
EXPECT_EQ(producer.wait_for(50ms), std::future_status::timeout);
EXPECT_TRUE(strategy.discard_pending_frame());
ASSERT_EQ(producer.wait_for(1s), std::future_status::ready);
producer.get();
EXPECT_EQ(strategy.counter_statistics().abandoned_frame_count, 0u);
EXPECT_EQ(strategy.counter_statistics().manually_discarded_frame_count, 1u);
}
TEST(low_latency_strategy_test, manually_discards_stale_latest_data_frame) {
TEST(low_latency_strategy_test, refresh_configuration_is_public_runtime_state) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
strategy.on_real_time_data_update({Real_Time_Data_Observation_Event::updated, {nullptr, Real_Time_Data_Retention::latest, 1, 10, 1, 1}});
{
auto frame = strategy.acquire_painter();
frame->value = 1;
}
strategy.on_real_time_data_update({Real_Time_Data_Observation_Event::updated, {nullptr, Real_Time_Data_Retention::latest, 2, 20, 2, 1}});
EXPECT_TRUE(strategy.discard_stale_latest_data_frame());
EXPECT_EQ(strategy.counter_statistics().manually_discarded_frame_count, 1);
auto frame = strategy.acquire_renderer();
EXPECT_FALSE(frame);
}
TEST(low_latency_strategy_test, does_not_discard_current_data_frame) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
strategy.on_real_time_data_update({Real_Time_Data_Observation_Event::updated, {nullptr, Real_Time_Data_Retention::latest, 1, 10, 1, 1}});
{
auto frame = strategy.acquire_painter();
frame->value = 9;
}
EXPECT_FALSE(strategy.discard_stale_latest_data_frame());
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 9);
}
TEST(low_latency_strategy_test, updates_frequency_after_completed_lifecycle) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(100.0), Low_Latency_Test_Strategy::Control_Error::none);
EXPECT_DOUBLE_EQ(strategy.state().frequency_hz, 100.0);
{
auto frame = strategy.acquire_painter();
frame->value = 3;
}
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
}
EXPECT_DOUBLE_EQ(strategy.state().frequency_hz, 100.0);
EXPECT_DOUBLE_EQ(strategy.frequency_hz(), 60.0);
strategy.swap();
EXPECT_EQ(strategy.set_frequency_hz(100.0),
Low_Latency_Test_Strategy::Control_Error::none);
EXPECT_DOUBLE_EQ(strategy.frequency_hz(), 100.0);
EXPECT_EQ(strategy.next_refresh_interval_ns(), 10'000'000);
}
TEST(low_latency_strategy_test, detects_render_limited_state) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(1'000'000.0), Low_Latency_Test_Strategy::Control_Error::none);
{
auto frame = strategy.acquire_painter();
frame->value = 5;
}
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
time_source.advance_ns(5'000);
}
EXPECT_EQ(strategy.state().limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited);
}
TEST(low_latency_strategy_test, rejects_non_finite_frequency_configuration) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
EXPECT_THROW((Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source), {std::numeric_limits<double>::quiet_NaN()})), std::invalid_argument);
EXPECT_THROW((Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source), {std::numeric_limits<double>::infinity()})), std::invalid_argument);
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(std::numeric_limits<double>::quiet_NaN()),
Low_Latency_Test_Strategy::Control_Error::invalid_frequency);
EXPECT_EQ(strategy.set_frequency_hz(std::numeric_limits<double>::infinity()),
EXPECT_EQ(strategy.next_refresh_interval_ns(), 10'000'000U);
EXPECT_EQ(strategy.set_frequency_hz(
std::numeric_limits<double>::quiet_NaN()),
Low_Latency_Test_Strategy::Control_Error::invalid_frequency);
}
TEST(low_latency_strategy_test, clamps_extremely_small_frequency_interval_without_overflow) {
TEST(low_latency_strategy_test, stale_latest_data_frame_can_be_discarded) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(1e-300), Low_Latency_Test_Strategy::Control_Error::none);
{
auto frame = strategy.acquire_painter();
frame->value = 1;
}
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
}
EXPECT_EQ(strategy.state().target_interval_ns, std::numeric_limits<std::uint64_t>::max());
}
TEST(low_latency_strategy_test, separates_consumer_rate_from_jitter_safety_margin) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(1000.0), Low_Latency_Test_Strategy::Control_Error::none);
EXPECT_EQ(strategy.set_consumer_feedback({10'000'000}), Low_Latency_Test_Strategy::Control_Error::none);
auto state = strategy.state();
EXPECT_TRUE(state.consumer_feedback_enabled);
EXPECT_EQ(state.consumer_sample_interval_ns, 10'000'000u);
EXPECT_EQ(state.consumer_smoothed_interval_ns, 10'000'000u);
EXPECT_EQ(state.consumer_variation_ns, 0u);
EXPECT_EQ(state.consumer_safety_interval_ns, 10'000'000u);
EXPECT_EQ(state.consumer_interval_ns, 10'000'000u);
EXPECT_EQ(strategy.set_consumer_feedback({14'000'000}), Low_Latency_Test_Strategy::Control_Error::none);
state = strategy.state();
EXPECT_EQ(state.consumer_sample_interval_ns, 14'000'000u);
EXPECT_EQ(state.consumer_smoothed_interval_ns, 12'000'000u);
EXPECT_EQ(state.consumer_variation_ns, 250'000u);
EXPECT_EQ(state.consumer_safety_interval_ns, 14'000'000u);
EXPECT_EQ(state.consumer_interval_ns, 14'000'000u);
EXPECT_EQ(state.next_refresh_interval_ns, 14'000'000u);
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::consumer_limited);
}
TEST(low_latency_strategy_test, consumer_jitter_changes_safety_deadline_without_becoming_rate_limit) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(1000.0), Low_Latency_Test_Strategy::Control_Error::none);
EXPECT_EQ(strategy.set_consumer_feedback({20'000'000}), Low_Latency_Test_Strategy::Control_Error::none);
EXPECT_EQ(strategy.set_consumer_feedback({22'000'000}), Low_Latency_Test_Strategy::Control_Error::none);
EXPECT_EQ(strategy.set_consumer_feedback({20'000'000}), Low_Latency_Test_Strategy::Control_Error::none);
const auto state = strategy.state();
EXPECT_EQ(state.consumer_sample_interval_ns, 20'000'000u);
EXPECT_EQ(state.consumer_smoothed_interval_ns, 20'875'000u);
EXPECT_EQ(state.consumer_variation_ns, 242'187u);
EXPECT_EQ(state.consumer_interval_ns, 20'875'000u);
EXPECT_EQ(state.consumer_safety_interval_ns, 21'843'748u);
EXPECT_EQ(state.next_refresh_interval_ns, 20'875'000u);
}
TEST(low_latency_strategy_test, allows_frequency_and_consumer_limits_to_be_cleared) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
EXPECT_EQ(strategy.set_frequency_hz(1000.0), Low_Latency_Test_Strategy::Control_Error::none);
EXPECT_EQ(strategy.set_consumer_feedback({20'000'000}), Low_Latency_Test_Strategy::Control_Error::none);
strategy.clear_consumer_feedback();
auto state = strategy.state();
EXPECT_FALSE(state.consumer_feedback_enabled);
EXPECT_EQ(state.consumer_safety_interval_ns, 0u);
EXPECT_EQ(state.consumer_interval_ns, 0u);
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited);
strategy.clear_frequency_limit();
state = strategy.state();
EXPECT_FALSE(state.frequency_limit_enabled);
EXPECT_TRUE(std::isnan(state.frequency_hz));
EXPECT_EQ(state.target_interval_ns, 0u);
EXPECT_EQ(state.next_refresh_interval_ns, 0u);
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::unlimited);
}
TEST(low_latency_strategy_test, unlimited_mode_runs_at_measured_render_bottleneck) {
Low_Latency_Test_Time_Source time_source;
Low_Latency_Test_Observer observer;
auto strategy = make_low_latency_test_strategy(time_source, observer);
strategy.clear_frequency_limit();
strategy.clear_consumer_feedback();
{
auto frame = strategy.acquire_painter();
time_source.advance_ns(2'000);
frame->value = 1;
}
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
time_source.advance_ns(5'000);
}
const auto state = strategy.state();
EXPECT_EQ(state.target_interval_ns, 0u);
EXPECT_EQ(state.consumer_interval_ns, 0u);
EXPECT_EQ(state.bottleneck_duration_ns, 5'000u);
EXPECT_EQ(state.next_refresh_interval_ns, 5'000u);
EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::render_limited);
strategy.on_real_time_data_update({
Real_Time_Data_Observation_Event::updated,
{nullptr, Real_Time_Data_Retention::latest, 1, 10, 1, 1}});
{ auto frame = strategy.acquire_painter(); frame->value = 1; }
strategy.on_real_time_data_update({
Real_Time_Data_Observation_Event::updated,
{nullptr, Real_Time_Data_Retention::latest, 2, 20, 2, 1}});
EXPECT_TRUE(strategy.discard_stale_latest_data_frame());
EXPECT_EQ(strategy.pending_frame_count(), 0U);
}
@@ -9,6 +9,9 @@
struct Low_Latency_Test_Frame {
int value{};
};
struct Low_Latency_Test_Scene {
using Frame = Low_Latency_Test_Frame;
};
struct Low_Latency_Test_Time_Source {
std::shared_ptr<std::atomic<std::uint64_t>> time_ns{std::make_shared<std::atomic<std::uint64_t>>(100)};
std::uint64_t now_ns() const noexcept {
@@ -82,7 +85,7 @@ struct Low_Latency_Test_Observer {
}
};
using Low_Latency_Test_Observer_State = Observer_State<Low_Latency_Test_Observer, Low_Latency_Test_Time_Source>;
using Low_Latency_Test_Strategy = Low_Latency_Strategy<Low_Latency_Test_Frame, std::mutex, Low_Latency_Test_Observer_State>;
using Low_Latency_Test_Strategy = Low_Latency_Strategy<Low_Latency_Test_Scene, std::mutex, Low_Latency_Test_Observer_State>;
inline Low_Latency_Test_Strategy make_low_latency_test_strategy(const Low_Latency_Test_Time_Source& time_source, const Low_Latency_Test_Observer& observer) {
return Low_Latency_Test_Strategy(Low_Latency_Test_Observer_State(observer, time_source));
}
@@ -1,106 +1,23 @@
#include <gtest/gtest.h>
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <vector>
#include "renderive/frame_control/Frame_Control.hpp"
struct Manual_Refresh_Test_Frame {
int value{};
};
using Manual_Refresh_Test_Strategy = Manual_Refresh_Strategy<Manual_Refresh_Test_Frame>;
#include <gtest/gtest.h>
struct Manual_Refresh_Test_Scene { struct Frame { int value{}; }; };
using Manual_Refresh_Test_Strategy = Manual_Refresh_Strategy<Manual_Refresh_Test_Scene>;
static_assert(Manual_Frame_Refresh_Strategy<Manual_Refresh_Test_Strategy>);
TEST(manual_refresh_strategy_test, publishes_only_after_manual_refresh) {
TEST(manual_refresh_strategy_test, publishes_only_after_refresh) {
Manual_Refresh_Test_Strategy strategy;
{
auto frame = strategy.acquire_painter();
ASSERT_TRUE(frame);
frame->value = 7;
}
{
auto frame = strategy.acquire_renderer();
EXPECT_FALSE(frame);
}
{ auto frame = strategy.acquire_painter(); frame->value = 7; }
EXPECT_FALSE(strategy.acquire_renderer());
EXPECT_EQ(strategy.refresh(), Manual_Refresh_Result::none);
{
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 7);
}
}
TEST(manual_refresh_strategy_test, keeps_latest_prepared_frame) {
Manual_Refresh_Test_Strategy strategy;
{
auto frame = strategy.acquire_painter();
frame->value = 1;
}
{
auto frame = strategy.acquire_painter();
frame->value = 2;
}
EXPECT_EQ(strategy.state().replaced_prepared_frame_count, 1);
ASSERT_EQ(strategy.refresh(), Manual_Refresh_Result::none);
auto frame = strategy.acquire_renderer();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->value, 2);
}
TEST(manual_refresh_strategy_test, reports_failed_refresh_without_pending_frame) {
Manual_Refresh_Test_Strategy strategy;
EXPECT_EQ(strategy.refresh(), Manual_Refresh_Result::no_pending_frame);
EXPECT_EQ(strategy.state().failed_refresh_count, 1);
}
struct Manual_Refresh_Reentrant_Observer_Data {
std::function<void(int)> callback;
};
struct Manual_Refresh_Reentrant_Observer {
static constexpr bool enabled = true;
std::shared_ptr<Manual_Refresh_Reentrant_Observer_Data> data{std::make_shared<Manual_Refresh_Reentrant_Observer_Data>()};
template <class Observation>
void observe(const Observation& observation) noexcept {
if (data->callback) {
data->callback(static_cast<int>(observation.event));
}
}
};
using Manual_Refresh_Reentrant_Observer_State = Observer_State<Manual_Refresh_Reentrant_Observer>;
using Manual_Refresh_Reentrant_Strategy = Manual_Refresh_Strategy<Manual_Refresh_Test_Frame, std::mutex, Manual_Refresh_Reentrant_Observer_State>;
TEST(manual_refresh_strategy_test, refresh_observer_can_acquire_renderer_without_deadlock) {
Manual_Refresh_Reentrant_Observer observer;
auto data = observer.data;
Manual_Refresh_Reentrant_Strategy strategy{Manual_Refresh_Reentrant_Observer_State(observer)};
{
auto frame = strategy.acquire_painter();
frame->value = 42;
}
std::atomic<int> observed_value{};
data->callback = [&](int event) {
if (event != static_cast<int>(Manual_Refresh_Reentrant_Strategy::Observation_Event::refresh_succeeded)) {
return;
}
auto frame = strategy.acquire_renderer();
if (frame) {
observed_value.store(frame->value, std::memory_order_release);
}
};
EXPECT_EQ(strategy.refresh(), Manual_Refresh_Result::none);
EXPECT_EQ(observed_value.load(std::memory_order_acquire), 42);
EXPECT_EQ(frame->value, 7);
}
TEST(manual_refresh_strategy_test, discard_is_counted_and_observed) {
Manual_Refresh_Reentrant_Observer observer;
auto data = observer.data;
Manual_Refresh_Reentrant_Strategy strategy{Manual_Refresh_Reentrant_Observer_State(observer)};
std::vector<int> events;
data->callback = [&events](int event) { events.push_back(event); };
{
auto frame = strategy.acquire_painter();
frame->value = 21;
}
ASSERT_TRUE(strategy.discard_pending_frame());
EXPECT_FALSE(strategy.discard_pending_frame());
const auto state = strategy.state();
EXPECT_FALSE(state.pending_frame);
EXPECT_EQ(state.discarded_prepared_frame_count, 1u);
ASSERT_EQ(events.size(), 2u);
EXPECT_EQ(events.back(), static_cast<int>(Manual_Refresh_Reentrant_Strategy::Observation_Event::manually_discarded));
TEST(manual_refresh_strategy_test, discard_removes_pending_frame) {
Manual_Refresh_Test_Strategy strategy;
{ auto frame = strategy.acquire_painter(); frame->value = 1; }
EXPECT_TRUE(strategy.discard_pending_frame());
EXPECT_EQ(strategy.refresh(), Manual_Refresh_Result::no_pending_frame);
}
@@ -1,175 +1,50 @@
#include <gtest/gtest.h>
#include "renderive/renderable/Inheritance.hpp"
#include <gtest/gtest.h>
#include <type_traits>
#include <vector>
namespace {
namespace chain = renderive::renderable_inheritance;
namespace renderable_chain = renderive::renderable_inheritance;
enum class Visit {
parent,
child
};
enum class Visit { parent, child };
struct Event {};
struct Impl_Base : renderive::inheritance::Type_Root {
explicit Impl_Base(std::vector<Visit>& visits) : visits(&visits) {}
std::vector<Visit>* visits;
};
struct Parent_Impl
: renderable_chain::Impl_Root<Parent_Impl, Impl_Base, Event> {
struct Parent_Impl : chain::Impl_Root<Parent_Impl, Impl_Base, Event> {
using Impl_Root::Impl_Root;
struct Observer : renderable_chain::Observer_Root<Parent_Impl> {
static void handle(Parent_Impl& impl, const Event&) {
impl.visits->push_back(Visit::parent);
}
};
void handle_observation(const Event&) {
visits->push_back(Visit::parent);
}
};
struct Child_Impl : renderable_chain::Impl_Node<Child_Impl, Parent_Impl> {
struct Child_Impl : chain::Impl_Node<Child_Impl, Parent_Impl> {
using Impl_Node::Impl_Node;
struct Observer
: renderable_chain::Observer_Node<Child_Impl,
Parent_Impl::Observer> {
static void handle(Child_Impl& impl, const Event&) {
impl.visits->push_back(Visit::child);
}
};
};
struct Parent_State : renderable_chain::State_Root<Parent_State> {};
struct Child_State
: renderable_chain::State_Node<Child_State, Parent_State> {};
struct Parent_Builder_Layer
: renderable_chain::Builder_Root<Parent_Builder_Layer> {};
struct Child_Builder_Layer
: renderable_chain::Builder_Node<Child_Builder_Layer,
Parent_Builder_Layer> {};
struct Business_Builder {
explicit Business_Builder(bool& initialized) : initialized(&initialized) {}
void build() const { *initialized = true; }
bool* initialized;
};
struct Final_Builder
: renderable_chain::Builder<Final_Builder, Child_Builder_Layer,
Business_Builder> {
using Builder::Builder;
};
struct Root_Base {
public:
explicit Root_Base(bool& constructed) : constructed(&constructed) {
constructed = true;
void handle_observation(const Event&) {
visits->push_back(Visit::child);
}
bool* constructed;
};
struct Parent_State : chain::State_Root<Parent_State> {};
struct Child_State : chain::State_Node<Child_State, Parent_State> {};
struct Parent_Builder : chain::Builder_Root<Parent_Builder> {};
struct Child_Builder : chain::Builder_Node<Child_Builder, Parent_Builder> {};
struct Parent_Observer : chain::Observer_Root<Parent_Observer> {};
struct Child_Observer : chain::Observer_Node<Child_Observer, Parent_Observer> {};
struct Renderable_Root
: renderive::renderable::render_base<Renderable_Root, Root_Base, true> {
using render_base::render_base;
struct Impl {};
};
struct Product_Properties {
bool enabled{};
};
template <class Product_Type, class Properties_Type>
struct Product_Business_Builder {
using Product = Product_Type;
using Properties = Properties_Type;
explicit Product_Business_Builder(bool& initialized)
: initialized(&initialized) {}
void build() const { *initialized = true; }
bool* initialized;
};
struct Visual_Control
: renderive::renderable::render_base<Visual_Control, Renderable_Root> {
using State = Product_Properties;
template <class Product, class Properties>
using Business_Builder = Product_Business_Builder<Product, Properties>;
using render_base::render_base;
protected:
template <class, auto Member, class Value>
void set_state(Value&& value) {
state.*Member = std::forward<Value>(value);
}
template <class, auto Member>
auto get_state() const {
return state.*Member;
}
private:
State state;
};
using Renderable_Product = renderive::renderable::attach<Visual_Control>;
TEST(renderable_inheritance_test, dispatches_nested_observers_parent_first) {
TEST(renderable_inheritance_test, dispatches_parent_before_child) {
std::vector<Visit> visits;
Child_Impl impl(visits);
impl.dispatch(Event{});
EXPECT_EQ(visits, (std::vector{Visit::parent, Visit::child}));
}
TEST(renderable_inheritance_test, preserves_business_builder_capabilities) {
bool initialized{};
Final_Builder builder(initialized);
builder.build();
EXPECT_TRUE(initialized);
static_assert(std::same_as<
renderive::inheritance::Type_At_Position_T<
typename Final_Builder::Inheritance_Registry,
renderable_chain::Builder_Chain,
Final_Builder::Inheritance_Position>,
Final_Builder>);
static_assert(std::same_as<
renderive::inheritance::Type_At_Position_T<
typename Child_State::Inheritance_Registry,
renderable_chain::State_Chain,
Child_State::Inheritance_Position>,
Child_State>);
TEST(renderable_inheritance_test, every_chain_registers_its_own_layer) {
static_assert(renderive::inheritance::Chain_Registration_Count_V<
typename Child_State::Inheritance_Registry, chain::State_Chain> == 2);
static_assert(renderive::inheritance::Chain_Registration_Count_V<
typename Child_Builder::Inheritance_Registry, chain::Builder_Chain> == 2);
static_assert(renderive::inheritance::Chain_Registration_Count_V<
typename Child_Observer::Inheritance_Registry, chain::Observer_Chain> == 2);
SUCCEED();
}
TEST(renderable_inheritance_test,
composes_root_control_and_product_without_business_boilerplate) {
bool constructed{};
Renderable_Product product(constructed);
EXPECT_TRUE(constructed);
bool initialized{};
Renderable_Product::Builder builder(initialized);
builder.build();
EXPECT_TRUE(initialized);
static_assert(std::same_as<Renderable_Product::Properties,
Product_Properties>);
static_assert(std::same_as<
typename Renderable_Product::Builder::Product,
Renderable_Product>);
}
} // namespace
+4 -9
View File
@@ -8,15 +8,6 @@ namespace renderive {
namespace detail {
struct LIB_DECL Afterglow : Renderable<Afterglow> {
using Base = Renderable<Afterglow>;
protected:
struct Builder : Base::template next_Builder<Builder> {};
public:
struct State;
private:
friend struct renderable::attach<Afterglow>;
Afterglow(const State& state, renderive_Owner<Frequency_Axis> frequency_axis,
renderive_Owner<Axis> power_axis);
public:
~Afterglow() override;
[[nodiscard]] std::size_t history_count() const;
[[nodiscard]] std::size_t latest_spectrum_point_count() const;
@@ -44,9 +35,13 @@ public:
std::size_t latest_spectrum_point_count{};
std::size_t rendered_cell_count{};
};
struct Builder : Base::template next_Builder<Builder> {};
private:
[[nodiscard]] Observer capture_observation() const;
struct Impl;
friend struct renderable::attach<Afterglow>;
Afterglow(const State& state, renderive_Owner<Frequency_Axis> frequency_axis,
renderive_Owner<Axis> power_axis);
};
}
using Afterglow = renderable::attach<detail::Afterglow>;
@@ -10,15 +10,6 @@ enum class Constellation_Diagram_Type : std::uint8_t {
namespace detail {
struct LIB_DECL Constellation_Diagram : Renderable<Constellation_Diagram> {
using Base = Renderable<Constellation_Diagram>;
protected:
struct Builder : Base::template next_Builder<Builder> {};
public:
struct State;
private:
friend struct renderable::attach<Constellation_Diagram>;
Constellation_Diagram(const State& state, renderive_Owner<Axis> i_axis,
renderive_Owner<Axis> q_axis);
public:
~Constellation_Diagram() override;
void append_point(PointF point);
[[nodiscard]] std::size_t point_count() const;
@@ -36,9 +27,13 @@ public:
State state;
std::size_t point_count{};
};
struct Builder : Base::template next_Builder<Builder> {};
private:
[[nodiscard]] Observer capture_observation() const;
struct Impl;
friend struct renderable::attach<Constellation_Diagram>;
Constellation_Diagram(const State& state, renderive_Owner<Axis> i_axis,
renderive_Owner<Axis> q_axis);
};
} // namespace detail
using Constellation_Diagram = renderable::attach<detail::Constellation_Diagram>;
@@ -5,31 +5,26 @@ namespace renderive {
namespace detail {
struct LIB_DECL Frequency_Trace : Renderable<Frequency_Trace> {
using Base = Renderable<Frequency_Trace>;
protected:
struct Builder : Base::template next_Builder<Builder> {};
public:
struct State;
private:
friend struct renderable::attach<Frequency_Trace>;
Frequency_Trace(const State& state, renderive_Owner<Time_Axis> time_axis,
renderive_Owner<Axis> value_axis);
public:
~Frequency_Trace() override;
void append_sample(int tick, double value);
void append_sample(Time_Of_Day time, double value);
[[nodiscard]] std::size_t sample_count() const;
[[nodiscard]] std::size_t rendered_point_count() const;
struct State : Base::template next_State<State> {
struct State : next_State<State> {
Pen pen{Color::yellow()};
};
struct Observer : Base::template next_Observer<Observer> {
struct Observer : next_Observer<Observer> {
State state;
std::size_t sample_count{};
std::size_t rendered_point_count{};
};
private:
struct Builder : next_Builder<Builder> {};
protected:
[[nodiscard]] Observer capture_observation() const;
struct Impl;
friend struct renderable::attach<Frequency_Trace>;
Frequency_Trace(const State& state, renderive_Owner<Time_Axis> time_axis,
renderive_Owner<Axis> value_axis);
};
}
using Frequency_Trace = renderable::attach<detail::Frequency_Trace>;
@@ -6,17 +6,7 @@ namespace detail {
struct LIB_DECL Selection_Rectangle_Overlay
: Renderable<Selection_Rectangle_Overlay>, Paint_Overlay {
using Base = Renderable<Selection_Rectangle_Overlay>;
protected:
struct Builder : Base::template next_Builder<Builder> {};
public:
struct State;
private:
friend struct renderable::attach<Selection_Rectangle_Overlay>;
Selection_Rectangle_Overlay(
const State& state, renderive_Owner<Abs_Axis> horizontal_axis,
renderive_Owner<Abs_Axis> vertical_axis);
public:
~Selection_Rectangle_Overlay() override;
[[nodiscard]] std::vector<RectF> selected_regions() const;
void clear_selected_regions();
struct State : Base::template next_State<State> {
@@ -25,15 +15,18 @@ public:
Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::Solid};
Pen selection_border_pen{Color::white(), 1.0, Line_Style::Dash};
};
private:
struct Impl;
public:
struct Observer : Base::template next_Observer<Observer> {
State state;
std::size_t selected_region_count{};
};
private:
struct Builder : Base::template next_Builder<Builder> {};
protected:
[[nodiscard]] Observer capture_observation() const;
struct Impl;
~Selection_Rectangle_Overlay() override;
Selection_Rectangle_Overlay(
const State& state, renderive_Owner<Abs_Axis> horizontal_axis,
renderive_Owner<Abs_Axis> vertical_axis);
};
}
using Selection_Rectangle_Overlay =
+31 -37
View File
@@ -33,9 +33,29 @@ struct Spectrum_Real_Time_Data_Strategy
};
struct LIB_DECL Spectrum : Renderable<Spectrum> {
using Base = Renderable<Spectrum>;
protected:
struct Builder : Base::template next_Builder<Builder> {};
public:
void update_samples(std::span<const double> values);
void update_samples(std::pmr::vector<double>&& values);
template <class Values>
void update_samples(const Values& values) {
update_samples(std::span<const double>(values.data(), values.size()));
}
[[nodiscard]] std::size_t sample_count() const;
[[nodiscard]] std::size_t rendered_point_count() const;
[[nodiscard]] double power_at(double frequency, bool& ok) const;
void add_custom_marker(double frequency);
void add_custom_line_marker(double frequency);
void remove_custom_marker(double frequency);
void remove_selected_marker();
void clear_custom_markers();
[[nodiscard]] int selectable_line_marker_count() const;
[[nodiscard]] int selected_marker_index() const;
void set_selected_marker_index(int index);
void select_next_marker();
void select_previous_marker();
void clear_marker_selection();
[[nodiscard]] double marker_frequency(int index) const;
void set_marker_frequency(int index, double frequency);
void set_current_marker_frequency(double frequency);
struct State : Base::template next_State<State>, Hover_Tooltip_Properties {
Nonnegative_Count frequency_point_size;
Render_Partition_Mode partition_mode{Render_Partition_Mode::Automatic};
@@ -65,47 +85,21 @@ public:
Brush_Style::Solid
};
};
private:
friend struct renderable::attach<Spectrum>;
Spectrum(const State& state,
renderive_Owner<Frequency_Axis> frequency_axis,
renderive_Owner<Axis> power_axis);
public:
~Spectrum() override;
void update_samples(std::span<const double> values);
void update_samples(std::pmr::vector<double>&& values);
template <class Values>
void update_samples(const Values& values) {
update_samples(std::span<const double>(values.data(), values.size()));
}
[[nodiscard]] std::size_t sample_count() const;
[[nodiscard]] std::size_t rendered_point_count() const;
[[nodiscard]] double power_at(double frequency, bool& ok) const;
void add_custom_marker(double frequency);
void add_custom_line_marker(double frequency);
void remove_custom_marker(double frequency);
void remove_selected_marker();
void clear_custom_markers();
[[nodiscard]] int selectable_line_marker_count() const;
[[nodiscard]] int selected_marker_index() const;
void set_selected_marker_index(int index);
void select_next_marker();
void select_previous_marker();
void clear_marker_selection();
[[nodiscard]] double marker_frequency(int index) const;
void set_marker_frequency(int index, double frequency);
void set_current_marker_frequency(double frequency);
private:
struct Impl;
public:
struct Observer : Base::template next_Observer<Observer> {
State state;
std::size_t sample_count{};
std::size_t rendered_point_count{};
std::size_t selectable_marker_count{};
};
private:
struct Builder : Base::template next_Builder<Builder> {};
protected:
struct Impl;
~Spectrum() override;
[[nodiscard]] Observer capture_observation() const;
friend struct renderable::attach<Spectrum>;
Spectrum(const State& state,
renderive_Owner<Frequency_Axis> frequency_axis,
renderive_Owner<Axis> power_axis);
};
}
using Spectrum = renderable::attach<detail::Spectrum>;
+4 -11
View File
@@ -7,15 +7,7 @@ namespace renderive {
namespace detail {
struct LIB_DECL Sweep_Spectrum : Renderable<Sweep_Spectrum> {
using Base = Renderable<Sweep_Spectrum>;
protected:
struct Builder : Base::template next_Builder<Builder> {};
public:
struct State;
private:
friend struct renderable::attach<Sweep_Spectrum>;
Sweep_Spectrum(const State& state, renderive_Owner<Axis> frequency_axis,
renderive_Owner<Axis> power_axis);
public:
~Sweep_Spectrum() override;
void append_block(std::span<const double> values);
void append_block(std::pmr::vector<double>&& values);
@@ -36,9 +28,6 @@ public:
Line_Interpolation_Mode interpolation_mode =
Line_Interpolation_Mode::Linear_Value;
};
private:
struct Impl;
public:
struct Observer : Base::template next_Observer<Observer> {
State state;
std::size_t stored_block_count{};
@@ -47,6 +36,10 @@ public:
};
private:
[[nodiscard]] Observer capture_observation() const;
friend struct renderable::attach<Sweep_Spectrum>;
Sweep_Spectrum(const State& state, renderive_Owner<Axis> frequency_axis,
renderive_Owner<Axis> power_axis);
struct Impl;
};
}
using Sweep_Spectrum = renderable::attach<detail::Sweep_Spectrum>;
+10 -17
View File
@@ -9,17 +9,6 @@ namespace renderive {
namespace detail {
struct LIB_DECL Waterfall : Renderable<Waterfall> {
using Base = Renderable<Waterfall>;
protected:
struct Builder : Base::template next_Builder<Builder> {};
public:
struct State;
private:
friend struct renderable::attach<Waterfall>;
Waterfall(const State& state,
renderive_Owner<Frequency_Axis> frequency_axis,
renderive_Owner<Time_Axis> time_axis);
public:
~Waterfall() override;
void append_row(int tick, std::span<const double> values);
void append_row(int tick, std::pmr::vector<double>&& values);
void append_row(Time_Of_Day time, std::span<const double> values);
@@ -35,7 +24,7 @@ public:
[[nodiscard]] std::size_t row_count() const;
[[nodiscard]] std::size_t stored_point_count() const;
[[nodiscard]] std::size_t rendered_cell_count() const;
struct State : Base::template next_State<State>, Hover_Tooltip_Properties {
struct State : next_State<State>, Hover_Tooltip_Properties {
Range frequency_range{0.0, 10.0};
Range power_range{0.0, 10.0};
Nonnegative_Count frequency_bin_count;
@@ -46,17 +35,21 @@ public:
Image_Interpolation_Mode::Nearest;
Color_Map color_map;
};
private:
struct Impl;
public:
struct Observer : Base::template next_Observer<Observer> {
struct Observer : next_Observer<Observer> {
State state;
std::size_t row_count{};
std::size_t stored_point_count{};
std::size_t rendered_cell_count{};
};
private:
struct Builder : next_Builder<Builder> {};
protected:
struct Impl;
friend struct renderable::attach<Waterfall>;
Waterfall(const State& state,
renderive_Owner<Frequency_Axis> frequency_axis,
renderive_Owner<Time_Axis> time_axis);
[[nodiscard]] Observer capture_observation() const;
~Waterfall() override;
};
}
using Waterfall = renderable::attach<detail::Waterfall>;
@@ -1,277 +1,53 @@
#include "../render_2D/export.h"
#include "render_2D/scene/Render_Scene_2D.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <limits>
#include <memory>
namespace renderive {
namespace {
TEST(Renderive_Core2_Frame_Pipeline, ManualLifecycleSeparatesPrepareRefreshAndRender) {
Render_Scene_2D plot({.frame_mode = Plot_Frame_Mode::Manual});
plot.init();
plot.set_viewport_size({96, 54});
ASSERT_TRUE(plot.prepare_frame());
auto snapshot = plot.frame_status();
EXPECT_EQ(snapshot.mode, Plot_Frame_Mode::Manual);
EXPECT_EQ(snapshot.last_event, "prepared");
EXPECT_EQ(snapshot.limit_state, "not_applicable");
EXPECT_EQ(snapshot.produced_frame_count, 1u);
EXPECT_EQ(snapshot.consumed_frame_count, 0u);
EXPECT_EQ(snapshot.pending_frame_count, 1u);
EXPECT_EQ(snapshot.latest_sequence, 1u);
ASSERT_TRUE(plot.refresh_manual_frame());
snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "refresh_succeeded");
EXPECT_EQ(snapshot.produced_frame_count, 1u);
EXPECT_EQ(snapshot.consumed_frame_count, 0u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.latest_sequence, 1u);
ASSERT_TRUE(plot.render_prepared_frame());
snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "rendered");
EXPECT_EQ(snapshot.produced_frame_count, 1u);
EXPECT_EQ(snapshot.consumed_frame_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.latest_sequence, 1u);
EXPECT_EQ(plot.diagnostics().refresh.frame_count, 1u);
std::unique_ptr<Render_Scene_2D> make_scene(
Render_Scene_2D::State state = {}) {
auto strategy =
std::make_shared<Low_Latency_Render_Scene_2D_Strategy>();
return Render_Scene_2D::Builder{state}.build(std::move(strategy));
}
TEST(Renderive_Core2_Frame_Pipeline, HighFrequencyLowLatencyLifecycleExposesKernelTiming) {
Render_Scene_2D plot;
plot.init();
plot.set_viewport_size({96, 54});
EXPECT_TRUE(plot.set_max_render_fps(1'000'000'000.0));
ASSERT_TRUE(plot.prepare_frame());
auto snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "published");
EXPECT_EQ(snapshot.limit_state, "frequency_limited");
EXPECT_DOUBLE_EQ(snapshot.frequency_hz, 1'000'000'000.0);
EXPECT_EQ(snapshot.target_interval_ns, 1u);
EXPECT_EQ(snapshot.produced_frame_count, 1u);
EXPECT_EQ(snapshot.consumed_frame_count, 0u);
EXPECT_EQ(snapshot.pending_frame_count, 1u);
ASSERT_TRUE(plot.render_prepared_frame());
snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "lifecycle_completed");
EXPECT_DOUBLE_EQ(snapshot.frequency_hz, 1'000'000'000.0);
EXPECT_EQ(snapshot.target_interval_ns, 1u);
EXPECT_EQ(snapshot.produced_frame_count, 1u);
EXPECT_EQ(snapshot.consumed_frame_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.bottleneck_duration_ns,
std::max(snapshot.paint_duration_ns, snapshot.render_duration_ns));
EXPECT_EQ(snapshot.next_refresh_interval_ns,
std::max(snapshot.target_interval_ns, snapshot.bottleneck_duration_ns));
EXPECT_GE(snapshot.end_to_end_ns, snapshot.paint_duration_ns);
EXPECT_GE(snapshot.end_to_end_ns, snapshot.render_duration_ns);
const auto refresh = plot.refresh_feedback_snapshot();
EXPECT_DOUBLE_EQ(refresh.frequency_hz, snapshot.frequency_hz);
EXPECT_EQ(refresh.frame_count, snapshot.consumed_frame_count);
EXPECT_EQ(refresh.next_refresh_interval_ns, snapshot.next_refresh_interval_ns);
TEST(RenderScene2DFramePipeline, BuilderOwnsConstructionAndState) {
Render_Scene_2D::State state;
state.viewport = {96, 54};
state.background = Color{8, 16, 24, 255};
auto scene = make_scene(state);
ASSERT_TRUE(scene);
EXPECT_EQ(scene->root_renderable()->object_name(), "root");
}
TEST(Renderive_Core2_Frame_Pipeline, LowLatencyConsumerFeedbackFlowsThroughCore2IntoKernelScheduling) {
Render_Scene_2D plot;
plot.init();
plot.set_viewport_size({96, 54});
EXPECT_TRUE(plot.set_max_render_fps(100.0));
EXPECT_TRUE(plot.set_consumer_feedback({40'000'000}));
ASSERT_TRUE(plot.prepare_frame());
ASSERT_TRUE(plot.render_prepared_frame());
const auto snapshot = plot.frame_status();
EXPECT_TRUE(snapshot.frequency_limit_enabled);
EXPECT_TRUE(snapshot.consumer_feedback_enabled);
EXPECT_EQ(snapshot.limit_state, "consumer_limited");
EXPECT_EQ(snapshot.target_interval_ns, 10'000'000u);
EXPECT_EQ(snapshot.consumer_sample_interval_ns, 40'000'000u);
EXPECT_EQ(snapshot.consumer_smoothed_interval_ns, 40'000'000u);
EXPECT_EQ(snapshot.consumer_variation_ns, 0u);
EXPECT_EQ(snapshot.consumer_safety_interval_ns, 40'000'000u);
EXPECT_EQ(snapshot.consumer_interval_ns, 40'000'000u);
EXPECT_EQ(snapshot.next_refresh_interval_ns, 40'000'000u);
EXPECT_EQ(plot.refresh_feedback_snapshot().next_refresh_interval_ns, 40'000'000u);
TEST(RenderScene2DFramePipeline, ActiveDirtyAndForceGatesAreOrthogonal) {
Render_Scene_2D::State state;
state.viewport = {80, 45};
auto scene = make_scene(state);
scene->publish_frame_state();
EXPECT_EQ(scene->observer().state, state);
EXPECT_EQ(scene->render_frame().value(), Plot_Render_Status::view_inactive);
scene->activate_view();
EXPECT_EQ(scene->render_frame().value(), Plot_Render_Status::rendered);
EXPECT_EQ(scene->render_frame().value(), Plot_Render_Status::model_unchanged);
EXPECT_EQ(scene->render_frame(true).value(), Plot_Render_Status::rendered);
EXPECT_EQ(scene->wait_for_render(), Scene_Render_Result::none);
}
TEST(Renderive_Core2_Frame_Pipeline, LowLatencyLimitsCanBeClearedIndependently) {
Render_Scene_2D plot;
plot.init();
plot.set_viewport_size({96, 54});
EXPECT_TRUE(plot.set_max_render_fps(100.0));
EXPECT_TRUE(plot.set_consumer_feedback({40'000'000}));
ASSERT_TRUE(plot.clear_consumer_feedback());
ASSERT_TRUE(plot.clear_max_render_fps());
ASSERT_TRUE(plot.prepare_frame());
ASSERT_TRUE(plot.render_prepared_frame());
const auto snapshot = plot.frame_status();
EXPECT_FALSE(snapshot.frequency_limit_enabled);
EXPECT_FALSE(snapshot.consumer_feedback_enabled);
EXPECT_EQ(snapshot.target_interval_ns, 0u);
EXPECT_EQ(snapshot.consumer_interval_ns, 0u);
EXPECT_EQ(snapshot.next_refresh_interval_ns, snapshot.bottleneck_duration_ns);
EXPECT_TRUE(snapshot.limit_state == "paint_limited" || snapshot.limit_state == "render_limited");
}
TEST(Renderive_Core2_Frame_Pipeline, LowLatencyReplacementRetiresOldFrameWithoutPhantomPendingFrame) {
Render_Scene_2D plot;
plot.init();
plot.set_viewport_size({64, 40});
ASSERT_TRUE(plot.prepare_frame());
ASSERT_TRUE(plot.prepare_frame());
auto snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "published");
EXPECT_EQ(snapshot.produced_frame_count, 2u);
EXPECT_EQ(snapshot.dropped_frame_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 1u);
EXPECT_EQ(snapshot.latest_sequence, 2u);
ASSERT_TRUE(plot.render_prepared_frame());
snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "lifecycle_completed");
EXPECT_EQ(snapshot.produced_frame_count, 2u);
EXPECT_EQ(snapshot.consumed_frame_count, 1u);
EXPECT_EQ(snapshot.dropped_frame_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.latest_sequence, 2u);
}
TEST(Renderive_Core2_Frame_Pipeline, LowLatencyDiscardAndEmptyRenderRemainObservable) {
Render_Scene_2D plot;
plot.init();
plot.set_viewport_size({64, 40});
ASSERT_TRUE(plot.prepare_frame());
ASSERT_TRUE(plot.discard_pending_frame());
auto snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "manually_discarded");
EXPECT_EQ(snapshot.produced_frame_count, 1u);
EXPECT_EQ(snapshot.consumed_frame_count, 0u);
EXPECT_EQ(snapshot.dropped_frame_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.latest_sequence, 1u);
const auto empty_render = plot.render_prepared_frame();
ASSERT_FALSE(empty_render);
EXPECT_EQ(empty_render.error(),
Plot_Render_Prepared_Frame_Error::renderer_unavailable);
snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "swap_failed");
EXPECT_EQ(snapshot.failed_operation_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.latest_sequence, 1u);
}
TEST(Renderive_Core2_Frame_Pipeline, ManualDiscardAndFailedRefreshUpdateObserverState) {
Render_Scene_2D plot({.frame_mode = Plot_Frame_Mode::Manual});
plot.init();
plot.set_viewport_size({64, 40});
ASSERT_TRUE(plot.prepare_frame());
ASSERT_TRUE(plot.discard_pending_frame());
auto snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "manually_discarded");
EXPECT_EQ(snapshot.produced_frame_count, 1u);
EXPECT_EQ(snapshot.dropped_frame_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.latest_sequence, 1u);
const auto last_paint_duration_ns = snapshot.paint_duration_ns;
const auto failed_refresh = plot.refresh_manual_frame();
ASSERT_FALSE(failed_refresh);
EXPECT_EQ(failed_refresh.error(),
Plot_Refresh_Manual_Frame_Error::no_pending_frame);
snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "refresh_failed");
EXPECT_EQ(snapshot.failed_operation_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.latest_sequence, 1u);
EXPECT_EQ(snapshot.paint_duration_ns, last_paint_duration_ns);
}
TEST(Renderive_Core2_Frame_Pipeline, PlaybackPreservesQueueDepthAndEmptyAcquireFailure) {
Render_Scene_2D plot({.frame_mode = Plot_Frame_Mode::Playback});
plot.init();
plot.set_viewport_size({64, 40});
ASSERT_TRUE(plot.prepare_frame());
ASSERT_TRUE(plot.prepare_frame());
ASSERT_TRUE(plot.prepare_frame());
auto snapshot = plot.frame_status();
EXPECT_EQ(snapshot.produced_frame_count, 3u);
EXPECT_EQ(snapshot.pending_frame_count, 3u);
EXPECT_EQ(snapshot.latest_sequence, 3u);
ASSERT_TRUE(plot.render_prepared_frame());
ASSERT_TRUE(plot.render_prepared_frame());
ASSERT_TRUE(plot.render_prepared_frame());
snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "rendered");
EXPECT_EQ(snapshot.consumed_frame_count, 3u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.latest_sequence, 3u);
const auto last_paint_duration_ns = snapshot.paint_duration_ns;
const auto last_render_duration_ns = snapshot.render_duration_ns;
const auto last_queue_wait_ns = snapshot.queue_wait_ns;
EXPECT_FALSE(plot.render_prepared_frame());
snapshot = plot.frame_status();
EXPECT_EQ(snapshot.last_event, "queue_empty");
EXPECT_EQ(snapshot.failed_operation_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 0u);
EXPECT_EQ(snapshot.latest_sequence, 3u);
EXPECT_EQ(snapshot.paint_duration_ns, last_paint_duration_ns);
EXPECT_EQ(snapshot.render_duration_ns, last_render_duration_ns);
EXPECT_EQ(snapshot.queue_wait_ns, last_queue_wait_ns);
}
TEST(Renderive_Core2_Frame_Pipeline, PlaybackFullCyclePreservesPreviouslyQueuedFrames) {
Render_Scene_2D plot({.frame_mode = Plot_Frame_Mode::Playback});
plot.init();
plot.set_viewport_size({96, 54});
ASSERT_TRUE(plot.prepare_frame());
ASSERT_TRUE(plot.prepare_frame());
ASSERT_TRUE(plot.prepare_frame());
auto snapshot = plot.frame_status();
ASSERT_EQ(snapshot.pending_frame_count, 3u);
ASSERT_EQ(snapshot.produced_frame_count, 3u);
ASSERT_EQ(plot.render_frame(true).value(), Plot_Render_Status::rendered);
snapshot = plot.frame_status();
EXPECT_EQ(snapshot.mode, Plot_Frame_Mode::Playback);
EXPECT_EQ(snapshot.last_event, "rendered");
EXPECT_EQ(snapshot.produced_frame_count, 4u);
EXPECT_EQ(snapshot.consumed_frame_count, 1u);
EXPECT_EQ(snapshot.pending_frame_count, 3u);
// The cycle enqueues sequence 4 but must consume the FIFO head (sequence 1).
EXPECT_EQ(snapshot.latest_sequence, 1u);
EXPECT_FALSE(plot.discard_pending_frame());
EXPECT_EQ(plot.frame_status().pending_frame_count, 3u);
}
TEST(Renderive_Core2_Frame_Pipeline, EveryModeHonorsActiveDirtyAndForceRenderGates) {
const auto verify = [](Plot_Frame_Mode mode) {
Render_Scene_2D scene({.frame_mode = mode});
scene.init();
scene.set_viewport_size({80, 45});
EXPECT_FALSE(scene.view_active());
EXPECT_NE(scene.render_frame(false).value(), Plot_Render_Status::rendered);
EXPECT_EQ(scene.frame_status().observation_count, 0u);
scene.activate_view();
ASSERT_EQ(scene.render_frame(false).value(), Plot_Render_Status::rendered);
const auto completed = scene.frame_status();
EXPECT_EQ(completed.mode, mode);
EXPECT_EQ(completed.consumed_frame_count, 1u);
EXPECT_NE(scene.render_frame(false).value(), Plot_Render_Status::rendered);
EXPECT_EQ(scene.frame_status().consumed_frame_count, 1u);
scene.deactivate_view();
scene.set_background_color(Color{8, 16, 24, 255});
EXPECT_NE(scene.render_frame(false).value(), Plot_Render_Status::rendered);
ASSERT_EQ(scene.render_frame(true).value(), Plot_Render_Status::rendered);
EXPECT_EQ(scene.frame_status().consumed_frame_count, 2u);
};
verify(Plot_Frame_Mode::Manual);
verify(Plot_Frame_Mode::Low_Latency);
verify(Plot_Frame_Mode::Playback);
}
TEST(Renderive_Core2_Frame_Pipeline, FrequencyConfigurationRejectsInvalidValuesAndWrongModes) {
Render_Scene_2D low_latency;
EXPECT_EQ(low_latency.set_max_render_fps(0.0).error(), Plot_Set_Max_Render_Fps_Error::invalid_fps);
EXPECT_EQ(low_latency.set_max_render_fps(-1.0).error(), Plot_Set_Max_Render_Fps_Error::invalid_fps);
EXPECT_EQ(low_latency.set_max_render_fps(std::numeric_limits<double>::quiet_NaN()).error(),
Plot_Set_Max_Render_Fps_Error::invalid_fps);
EXPECT_EQ(low_latency.set_max_render_fps(std::numeric_limits<double>::infinity()).error(),
Plot_Set_Max_Render_Fps_Error::invalid_fps);
EXPECT_TRUE(low_latency.set_max_render_fps(1'000.0));
EXPECT_DOUBLE_EQ(low_latency.max_render_fps(), 1'000.0);
Render_Scene_2D manual({.frame_mode = Plot_Frame_Mode::Manual});
Render_Scene_2D playback({.frame_mode = Plot_Frame_Mode::Playback});
EXPECT_EQ(manual.set_max_render_fps(60.0).error(), Plot_Set_Max_Render_Fps_Error::unsupported_frame_mode);
EXPECT_EQ(playback.set_max_render_fps(60.0).error(), Plot_Set_Max_Render_Fps_Error::unsupported_frame_mode);
const auto unsupported_refresh = playback.refresh_manual_frame();
ASSERT_FALSE(unsupported_refresh);
EXPECT_EQ(unsupported_refresh.error(),
Plot_Refresh_Manual_Frame_Error::unsupported_frame_mode);
EXPECT_FALSE(playback.discard_pending_frame());
TEST(RenderScene2DFramePipeline, ObserverReadsPublishedDoubleBufferedState) {
Render_Scene_2D::State state;
state.viewport = {32, 24};
auto scene = make_scene(state);
scene->activate_view();
ASSERT_EQ(scene->render_frame(true).value(), Plot_Render_Status::rendered);
ASSERT_EQ(scene->wait_for_render(), Scene_Render_Result::none);
const auto observation = scene->observer();
EXPECT_EQ(observation.state, state);
EXPECT_EQ(observation.renderable_count, 1U);
EXPECT_TRUE(observation.view_active);
EXPECT_GT(observation.frequency_hz, 0.0);
}
} // namespace
} // namespace renderive
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -60,7 +60,7 @@ struct Point_Visual::Impl : Base::next_Impl<Impl>, Point_Buffer_Strategy {
}
[[nodiscard]] Visual_Data_Error edit_points(const std::function<void(std::vector<Point>&)>& edit) {
if (!edit) return Visual_Data_Error::empty_edit;
const auto& cached = cache_buffer_value<Point_Payload_Tag>();
const auto cached = latest_snapshot<Point_Payload_Tag>();
auto next = cached ? *cached : std::vector<Point>{};
edit(next);
return update_points(std::move(next));
@@ -68,8 +68,8 @@ struct Point_Visual::Impl : Base::next_Impl<Impl>, Point_Buffer_Strategy {
[[nodiscard]] const Point_Payload& points() const noexcept {
return render_buffer_value<Point_Payload_Tag>();
}
[[nodiscard]] std::size_t point_count() const noexcept {
const auto& cached = cache_buffer_value<Point_Payload_Tag>();
[[nodiscard]] std::size_t point_count() const {
const auto cached = latest_snapshot<Point_Payload_Tag>();
return cached ? cached->size() : 0U;
}
[[nodiscard]] std::uint64_t dependent_state_revision() const noexcept {
+5 -5
View File
@@ -148,15 +148,15 @@ public:
}
[[nodiscard]] Visual_Data_Error edit_items(const std::function<void(std::vector<Item>&)>& edit) {
if (!edit) return Visual_Data_Error::empty_edit;
const auto& cached = this->template d_func<Impl>()
.template cache_buffer_value<Visual_Data_Tag<Spec>>();
const auto cached = this->template d_func<Impl>()
.template latest_snapshot<Visual_Data_Tag<Spec>>();
auto next = cached ? *cached : std::vector<Item>{};
edit(next);
return update_items(std::move(next));
}
[[nodiscard]] std::size_t item_count() const noexcept {
const auto& cached = this->template d_func<Impl>()
.template cache_buffer_value<Visual_Data_Tag<Spec>>();
[[nodiscard]] std::size_t item_count() const {
const auto cached = this->template d_func<Impl>()
.template latest_snapshot<Visual_Data_Tag<Spec>>();
return cached ? cached->size() : 0U;
}
[[nodiscard]] std::uint64_t data_revision() const noexcept {
+17 -82
View File
@@ -1,89 +1,22 @@
#include "render_3D/Datoviz_Visuals.h"
#include "render_3D/Scene_Context.hpp"
#include <renderive/scene/Scene.hpp>
#include <gtest/gtest.h>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <vector>
namespace renderive::render_3d {
namespace {
template <class Visual>
struct Visual_Frame_Test_Scene final : Scene3D_Context<>, Scene_Renderer {
using Frame_Data = typename Visual::Frame_Data;
explicit Visual_Frame_Test_Scene(
const std::shared_ptr<Visual>& visual) : owner(visual->renderable_id()) {
auto builder = attach_builder();
builder.attach(renderive_Owner<Visual>(visual));
}
~Visual_Frame_Test_Scene() override {
shutdown();
}
void render_frame() {
publish_frame_state();
render();
wait_for_render();
}
Node_Execution_Result render_scene(
const Scene_Render_Context& context) override {
prepared = context.template prepared<Frame_Data>(owner);
if (!prepared)
throw std::logic_error(
"Datoviz visual did not publish frame-local data");
return Node_Execution_Result::completed();
}
Renderable_Id owner{};
std::shared_ptr<const Frame_Data> prepared;
};
template <class Visual, class Item>
void exercise(Item item) {
static_assert(std::derived_from<Visual, Renderable>);
auto visual = typename Visual::Builder{}.build(std::vector<Item>{item});
ASSERT_NE(visual, nullptr);
ASSERT_TRUE(visual);
EXPECT_EQ(visual->item_count(), 1U);
EXPECT_EQ(visual->observation().event,
Renderable_Observer_Event::Data_Updated);
EXPECT_EQ(visual->observation().data_revision,
visual->data_revision());
const auto initial_revision = visual->data_revision();
visual->edit_items([](auto& items) {
items.push_back(items.front());
});
ASSERT_EQ(visual->edit_items(
[](auto& items) { items.push_back(items.front()); }),
Visual_Data_Error::none);
EXPECT_EQ(visual->item_count(), 2U);
EXPECT_GT(visual->data_revision(), initial_revision);
EXPECT_EQ(visual->observation().event,
Renderable_Observer_Event::Data_Updated);
EXPECT_EQ(visual->observation().item_count, 2U);
EXPECT_EQ(visual->observation().data_revision,
visual->data_revision());
Visual_Frame_Test_Scene<Visual> scene(visual);
scene.render_frame();
ASSERT_TRUE(scene.prepared);
ASSERT_TRUE(scene.prepared->items);
EXPECT_EQ(scene.prepared->items->size(), 2U);
EXPECT_EQ(visual->observation().event,
Renderable_Observer_Event::Published);
const auto first_frame = scene.prepared;
const auto first_published_revision = first_frame->data_revision;
const auto revision = visual->data_revision();
visual->update_items(std::vector<Item>{item, item, item});
EXPECT_EQ(visual->item_count(), 3U);
EXPECT_GT(visual->data_revision(), revision);
EXPECT_EQ(visual->observation().event,
Renderable_Observer_Event::Data_Updated);
EXPECT_EQ(visual->observation().item_count, 3U);
EXPECT_EQ(visual->observation().data_revision,
visual->data_revision());
ASSERT_TRUE(first_frame->items);
EXPECT_EQ(first_frame->items->size(), 2U);
scene.render_frame();
ASSERT_TRUE(scene.prepared);
ASSERT_TRUE(scene.prepared->items);
EXPECT_EQ(scene.prepared->items->size(), 3U);
EXPECT_GT(scene.prepared->data_revision, first_published_revision);
EXPECT_EQ(visual->observation().event,
Renderable_Observer_Event::Published);
}
TEST(DatovizVisualFamilies, EveryPublicFamilyUsesRenderableStorage) {
TEST(DatovizVisualFamilies, EveryPublicFamilyUsesBuilderOwnedStorage) {
exercise<Splat_Visual>(Splat{});
exercise<Pixel_Visual>(Pixel{});
exercise<Marker_Visual>(Marker{});
@@ -99,18 +32,20 @@ TEST(DatovizVisualFamilies, EveryPublicFamilyUsesRenderableStorage) {
exercise<Text_Visual>(Text_Label{.text = "Renderive"});
exercise<Volume_Visual>(Voxel{});
}
TEST(DatovizVisualFamilies, StateValidationAndBuilderPropertiesAreShared) {
TEST(DatovizVisualFamilies, BuilderValidationUsesStateProperties) {
auto volume = Volume_Visual::Builder{}
.set<&Volume_State::field_width>(4U)
.set<&Volume_State::field_height>(4U)
.set<&Volume_State::field_depth>(4U)
.build(std::vector<Voxel>(64));
ASSERT_NE(volume, nullptr);
.set<&Volume_State::field_width>(4U)
.set<&Volume_State::field_height>(4U)
.set<&Volume_State::field_depth>(4U)
.build(std::vector<Voxel>(64));
ASSERT_TRUE(volume);
EXPECT_EQ(volume->get<&Volume_State::field_depth>(), 4U);
EXPECT_THROW((Volume_Visual::Builder{}
.set<&Volume_State::field_width>(4U)
.build()),
std::invalid_argument);
}
} // namespace
} // namespace renderive::render_3d
@@ -1,195 +1,54 @@
#include "render_3D/Render_Scene_3D.h"
#include <renderive/scene/base/Scene_Base.hpp>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <memory>
#include <string>
#include <vector>
namespace renderive::render_3d {
namespace {
struct Test_Point_Scene {
std::shared_ptr<Point_Visual> visual;
std::unique_ptr<Render_Scene_3D> scene;
};
Test_Point_Scene make_test_scene(Scene_Options options) {
std::vector<Point> points{
{{-0.72F, -0.38F, 0.15F}, {245, 70, 78, 255}, 42.0F},
{{0.00F, 0.48F, -0.05F}, {65, 220, 132, 255}, 48.0F},
{{0.72F, 0.0F, 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_State state;
state.style = {{12, 16, 24, 255}, 2.0F, Point_Aspect::Outline};
auto visual = Point_Visual::Builder{state}.build(std::move(points));
auto scene = std::make_unique<Render_Scene_3D>(options, visual);
return {std::move(visual), std::move(scene)};
std::unique_ptr<Render_Scene_3D> make_scene(
Render_Scene_3D::State state,
const std::shared_ptr<Point_Visual>& visual) {
return Render_Scene_3D::Builder{state}.build(
visual, std::make_shared<Low_Latency_Render_Scene_3D_Strategy>());
}
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<const std::uint8_t*>(frame.rgba8.data());
const std::array<std::uint8_t, 4> 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) {
TEST(PointRenderIntegration, BuilderConstructsSceneFromStateAndStrategy) {
auto visual = Point_Visual::Builder{}.build(std::vector<Point>{
{{0.0F, 0.0F, 0.0F}, {255, 0, 0, 255}, 8.0F}});
Render_Scene_3D::State state;
state.viewport = {320, 200};
try {
auto demo = make_test_scene(Scene_Options{.viewport = {320, 200}});
ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none);
ASSERT_EQ(demo.scene->render_scene().wait_for_render(),
Scene_Render_Result::none);
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_EQ(demo.scene->request_frame(), Frame_Request_Result::none);
ASSERT_EQ(demo.scene->render_scene().wait_for_render(),
Scene_Render_Result::none);
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);
auto scene = make_scene(state, visual);
ASSERT_TRUE(scene);
EXPECT_EQ(scene->renderable_count(), 1U);
}
catch (const std::exception& error) {
GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what();
}
}
TEST(PointRenderIntegration, KernelEventsReachDatovizArcballOnItsDomain) {
TEST(PointRenderIntegration, ObserverPublishesSceneAndVisualState) {
std::vector<Point> points(3);
for (auto& point : points) point.diameter_px = 1.0F;
auto visual = Point_Visual::Builder{}.build(std::move(points));
Render_Scene_3D::State state;
state.viewport = {160, 90};
try {
auto demo = make_test_scene(Scene_Options{.viewport = {320, 200}});
ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none);
ASSERT_EQ(demo.scene->render_scene().wait_for_render(),
Scene_Render_Result::none);
const auto before = demo.scene->latest_frame()->rgba8;
::renderive::Basic_Pointer_Event<Event_Point> 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<Event_Point> 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<Event_Point> release(
::renderive::Event_Type::Pointer_Release);
release.position = move.position;
release.button = ::renderive::Mouse_Button::Left;
demo.scene->dispatch(release);
ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none);
ASSERT_EQ(demo.scene->render_scene().wait_for_render(),
Scene_Render_Result::none);
const auto after_drag = demo.scene->latest_frame();
ASSERT_NE(after_drag, nullptr);
EXPECT_NE(after_drag->rgba8, before);
::renderive::Basic_Wheel_Event<Event_Point> wheel;
wheel.position = {160.0F, 100.0F};
wheel.pixel_delta_y = 180.0;
wheel.angle_delta_y = -216.0;
demo.scene->dispatch(wheel);
ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none);
ASSERT_EQ(demo.scene->render_scene().wait_for_render(),
Scene_Render_Result::none);
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();
}
}
TEST(PointRenderIntegration,
CapturesExternalGpuLifetimeAndSeparatedBackendPhases) {
try {
auto demo = make_test_scene(Scene_Options{.viewport = {320, 200}});
auto& kernel_scene = demo.scene->render_scene();
const auto request = kernel_scene.capture_next_frame();
ASSERT_TRUE(request);
const Capture_Session_Id capture = request.session_id;
ASSERT_EQ(demo.scene->request_frame(), Frame_Request_Result::none);
ASSERT_EQ(kernel_scene.wait_for_render(), Scene_Render_Result::none);
const auto session = kernel_scene.capture_session(capture);
ASSERT_TRUE(session);
ASSERT_EQ(session->captured_count(), 1U);
const auto& snapshot = *session->frames.front().snapshot;
const auto plan = kernel_scene.find_render_plan(
snapshot.render_plan_version);
ASSERT_TRUE(plan);
const auto render_node = std::find_if(
plan->graph.nodes.begin(), plan->graph.nodes.end(),
[](const Render_Node& node) {
return node.kind == Render_Node_Kind::render;
});
ASSERT_NE(render_node, plan->graph.nodes.end());
const auto& execution = snapshot.node_executions.at(
render_node->execution_index);
EXPECT_EQ(execution.status, Node_Execution_Status::complete);
EXPECT_GT(execution.external_start_time_ns, 0U);
EXPECT_GE(execution.external_end_time_ns,
execution.external_start_time_ns);
EXPECT_GE(execution.duration_ns(), execution.cpu_duration_ns());
EXPECT_TRUE(execution.metrics.contains(
Node_Metric_Kind::queue_wait_ns));
EXPECT_TRUE(execution.metrics.contains(
Node_Metric_Kind::backend_execute_duration_ns));
EXPECT_TRUE(execution.metrics.contains(
Node_Metric_Kind::gpu_fence_wait_ns));
EXPECT_TRUE(execution.metrics.contains(
Node_Metric_Kind::readback_duration_ns));
const auto artifact_bytes = execution.metrics.get(
Node_Metric_Kind::backend_artifact_json_bytes);
EXPECT_GT(artifact_bytes, 0U);
const auto artifact = std::find_if(
execution.attachments.begin(), execution.attachments.end(),
[](const Node_Diagnostic_Attachment& attachment) {
return attachment.type == "datoviz.drp2.artifact.json";
});
ASSERT_NE(artifact, execution.attachments.end());
EXPECT_EQ(artifact->content.size(), artifact_bytes);
EXPECT_FALSE(artifact->content.empty());
const bool gpu_timing = execution.metrics.contains(
Node_Metric_Kind::gpu_render_duration_ns);
EXPECT_EQ(gpu_timing, execution.metrics.contains(
Node_Metric_Kind::gpu_transition_duration_ns));
EXPECT_EQ(gpu_timing, execution.metrics.contains(
Node_Metric_Kind::gpu_copy_duration_ns));
EXPECT_EQ(gpu_timing, execution.metrics.contains(
Node_Metric_Kind::gpu_total_duration_ns));
if (gpu_timing) {
const auto total = execution.metrics.get(
Node_Metric_Kind::gpu_total_duration_ns);
EXPECT_GE(total, execution.metrics.get(
Node_Metric_Kind::gpu_render_duration_ns));
EXPECT_GE(total, execution.metrics.get(
Node_Metric_Kind::gpu_transition_duration_ns));
EXPECT_GE(total, execution.metrics.get(
Node_Metric_Kind::gpu_copy_duration_ns));
}
auto scene = make_scene(state, visual);
scene->publish_frame_state();
ASSERT_EQ(scene->render(), Scene_Render_Result::none);
ASSERT_EQ(scene->wait_for_render(), Scene_Render_Result::none);
const auto observation = scene->observer();
EXPECT_EQ(observation.state, state);
EXPECT_EQ(observation.renderable_count, 1U);
EXPECT_EQ(observation.point_count, visual->point_count());
EXPECT_EQ(observation.point_count, 3U);
}
catch (const std::exception& error) {
GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what();
}
}
} // namespace
} // namespace renderive::render_3d
+19 -122
View File
@@ -1,114 +1,34 @@
#include "render_3D/Point_Visual.h"
#include "render_3D/detail/Point_Core.h"
#include <renderive/frame_control/Frame_Control.hpp>
#include "render_3D/Scene_Context.hpp"
#include <renderive/scene/Scene.hpp>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <memory>
#include <stdexcept>
#include <thread>
#include <vector>
namespace renderive::render_3d {
namespace {
using Test_Frame_Control = Low_Latency_Strategy<Scene3D_Frame_Data>;
struct Test_Scene final
: Scene3D_Context<Test_Frame_Control, detail::Scene_State>,
Scene_Renderer {
explicit Test_Scene(const std::shared_ptr<Point_Visual>& visual) {
Scene_State_Strategy::set<&detail::Scene_State::viewport>(
Extent{320, 180});
point_id = visual->renderable_id();
auto builder = attach_builder();
if (builder.attach(renderive_Owner<Point_Visual>(visual)) !=
Scene_Edit_Error::none)
throw std::logic_error("test visual attachment failed");
}
~Test_Scene() override {
shutdown();
}
void render_frame() {
{
auto painter = frame_control.acquire_painter();
ASSERT_TRUE(painter);
publish_frame_state();
}
auto renderer = frame_control.acquire_renderer();
ASSERT_TRUE(renderer);
ASSERT_EQ(Scene_Base::render(*renderer), Scene_Render_Result::none);
ASSERT_EQ(wait_for_render(), Scene_Render_Result::none);
}
Node_Execution_Result render_scene(
const Scene_Render_Context& context) override {
prepared = context.prepared<detail::Prepared_Point>(point_id);
if (!prepared) throw std::logic_error("Point_Visual did not publish prepared data");
EXPECT_EQ(context.frame.scene_state<detail::Scene_State>().viewport,
(Extent{320, 180}));
return Node_Execution_Result::completed();
}
Renderable_Id point_id{};
std::shared_ptr<const detail::Prepared_Point> prepared;
};
static_assert(std::derived_from<Point_Visual, Renderable>);
static_assert(std::derived_from<Point_Visual, ::Renderable_Base>);
TEST(PointState, PublishesStateAndBulkDataIntoFrameLocalPreparedOutput) {
auto visual = Point_Visual::Builder{}.build(
TEST(PointState, BuilderAndStateMutationShareOneAuthoritativeState) {
Point_State initial;
initial.style.stroke_width_px = 2.0F;
auto visual = Point_Visual::Builder{initial}.build(
std::vector<Point>{{{1.0F, 2.0F, 3.0F}, {1, 2, 3, 255}, 7.0F}});
ASSERT_TRUE(visual);
Point_State configured;
configured.visible = false;
configured.style.stroke_width_px = 3.0F;
visual->set<&Point_State::visible>(configured.visible);
visual->set<&Point_State::style>(configured.style);
Test_Scene scene(visual);
scene.render_frame();
ASSERT_TRUE(scene.prepared);
EXPECT_EQ(scene.prepared->state, configured);
ASSERT_EQ(scene.prepared->positions.size(), 1U);
EXPECT_EQ(scene.prepared->positions.front(),
(std::array<float, 3>{1.0F, 2.0F, 3.0F}));
EXPECT_EQ(scene.prepared->data_revision, 1U);
EXPECT_EQ(visual->get<&Point_State::style>(), initial.style);
auto changed = initial.style;
changed.stroke_width_px = 3.0F;
visual->set<&Point_State::style>(changed);
EXPECT_EQ(visual->get<&Point_State::style>(), changed);
}
TEST(PointState, PublishesImmutableBulkPayloadsAcrossConcurrentUpdates) {
TEST(PointState, BulkUpdatesUseOwnedStorage) {
auto visual = Point_Visual::Builder{}.build();
ASSERT_TRUE(visual);
Test_Scene scene(visual);
constexpr int update_count = 200;
std::atomic<bool> done{};
std::thread writer([&] {
for (int value = 1; value <= update_count; ++value) {
std::vector<Point> points(static_cast<std::size_t>(value % 31 + 1));
for (auto& point : points) {
point.position.x = static_cast<float>(value);
point.diameter_px = static_cast<float>(value % 12 + 1);
}
visual->update_points(std::move(points));
}
done.store(true, std::memory_order_release);
});
do {
scene.render_frame();
const auto prepared = scene.prepared;
ASSERT_TRUE(prepared);
if (!prepared->positions.empty()) {
const float expected = prepared->positions.front()[0];
for (const auto& point : prepared->positions)
EXPECT_EQ(point[0], expected);
}
}
while (!done.load(std::memory_order_acquire));
writer.join();
scene.render_frame();
ASSERT_TRUE(scene.prepared);
ASSERT_FALSE(scene.prepared->positions.empty());
EXPECT_EQ(scene.prepared->positions.front()[0],
static_cast<float>(update_count));
EXPECT_EQ(visual->update_points(std::vector<Point>(4)),
Visual_Data_Error::none);
EXPECT_EQ(visual->point_count(), 4U);
}
TEST(PointState, RejectsInvalidStateAndPayloadAtTheOwningBoundary) {
TEST(PointState, RejectsInvalidStateAndPayloadAtOwningBoundary) {
auto visual = Point_Visual::Builder{}.build();
ASSERT_TRUE(visual);
Point_State invalid;
invalid.style.stroke_width_px = -1.0F;
EXPECT_THROW(visual->set<&Point_State::style>(invalid.style),
@@ -116,29 +36,6 @@ TEST(PointState, RejectsInvalidStateAndPayloadAtTheOwningBoundary) {
EXPECT_EQ(visual->update_points(std::vector<Point>{{{}, {}, 0.0F}}),
Visual_Data_Error::invalid_data);
}
TEST(PointRenderPlan, OrdersParallelPreparationBeforeSingleSceneRenderNode) {
auto visual = Point_Visual::Builder{}.build(
std::vector<Point>{{{}, {}, 8.0F}});
Test_Scene scene(visual);
scene.render_frame();
const auto plan = scene.render_plan_snapshot();
ASSERT_TRUE(plan);
const auto prepare = std::find_if(
plan->graph.nodes.begin(), plan->graph.nodes.end(),
[&](const Render_Node& node) {
return node.owner_id == visual->renderable_id() &&
node.kind == Render_Node_Kind::prepare;
});
const auto render = std::find_if(
plan->graph.nodes.begin(), plan->graph.nodes.end(),
[](const Render_Node& node) {
return node.kind == Render_Node_Kind::render;
});
ASSERT_NE(prepare, plan->graph.nodes.end());
ASSERT_NE(render, plan->graph.nodes.end());
EXPECT_NE(std::find(plan->graph.edges.begin(), plan->graph.edges.end(),
Render_Edge{prepare->node_id, render->node_id}),
plan->graph.edges.end());
}
} // namespace
} // namespace renderive::render_3d