补充测试
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+10
-236
@@ -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());
|
||||
}
|
||||
|
||||
+12
-237
@@ -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());
|
||||
}
|
||||
|
||||
+27
-224
@@ -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);
|
||||
}
|
||||
|
||||
+4
-1
@@ -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));
|
||||
}
|
||||
|
||||
+14
-97
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user