内核优化
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include "renderive/base/node/Node.hpp"
|
||||
struct Multiway_Node_Test_Owner {};
|
||||
struct Multiway_Node_Test_Tag {};
|
||||
using Multiway_Node_Test_Type = Multiway_Node<Multiway_Node_Test_Owner, Multiway_Node_Test_Tag>;
|
||||
static_assert(Multiway_Node_Type<Multiway_Node_Test_Type, Multiway_Node_Test_Owner, Multiway_Node_Test_Tag>);
|
||||
static_assert(!std::same_as<Multiway_Node<Multiway_Node_Test_Owner>, Multiway_Node_Test_Type>);
|
||||
static_assert(std::same_as<decltype(std::declval<const Multiway_Node_Test_Type&>().child(0)), const Multiway_Node_Test_Type&>);
|
||||
TEST(multiway_node_test, reparents_child_and_preserves_order) {
|
||||
Multiway_Node_Test_Owner first_owner;
|
||||
Multiway_Node_Test_Owner second_owner;
|
||||
@@ -17,12 +19,12 @@ TEST(multiway_node_test, reparents_child_and_preserves_order) {
|
||||
root.append_child(first);
|
||||
root.append_child(second);
|
||||
first.append_child(third);
|
||||
EXPECT_EQ(root.children.size(), 2);
|
||||
EXPECT_EQ(first.children.front(), &third);
|
||||
EXPECT_EQ(third.parent, &first);
|
||||
EXPECT_EQ(root.child_count(), 2);
|
||||
EXPECT_EQ(&first.child(0), &third);
|
||||
EXPECT_EQ(third.parent(), &first);
|
||||
second.append_child(third);
|
||||
EXPECT_TRUE(first.children.empty());
|
||||
EXPECT_EQ(third.parent, &second);
|
||||
EXPECT_TRUE(first.children_empty());
|
||||
EXPECT_EQ(third.parent(), &second);
|
||||
}
|
||||
TEST(multiway_node_test, rejects_cycles) {
|
||||
Multiway_Node_Test_Type root;
|
||||
@@ -30,3 +32,22 @@ TEST(multiway_node_test, rejects_cycles) {
|
||||
root.append_child(child);
|
||||
EXPECT_THROW(child.append_child(root), std::invalid_argument);
|
||||
}
|
||||
TEST(multiway_node_test, reorders_existing_child_without_invalid_iterator) {
|
||||
Multiway_Node_Test_Type root;
|
||||
Multiway_Node_Test_Type first;
|
||||
Multiway_Node_Test_Type second;
|
||||
root.append_child(first);
|
||||
root.append_child(second);
|
||||
root.append_child(first);
|
||||
EXPECT_EQ(root.child_count(), 2);
|
||||
EXPECT_EQ(&root.child(0), &second);
|
||||
EXPECT_EQ(&root.child(1), &first);
|
||||
root.insert_child(0, first);
|
||||
EXPECT_EQ(&root.child(0), &first);
|
||||
EXPECT_EQ(&root.child(1), &second);
|
||||
}
|
||||
TEST(multiway_node_test, rejects_child_index_past_end) {
|
||||
Multiway_Node_Test_Type root;
|
||||
Multiway_Node_Test_Type child;
|
||||
EXPECT_THROW(root.insert_child(1, child), std::out_of_range);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "renderive/frame_control/Frame_Control.hpp"
|
||||
struct Flow_Refresh_Test_Frame {
|
||||
int value{};
|
||||
@@ -26,3 +33,150 @@ TEST(flow_refresh_strategy_test, returns_empty_lease_when_queue_is_empty) {
|
||||
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;
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST(flow_refresh_strategy_test, concurrent_consumer_does_not_underflow_pending_count) {
|
||||
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);
|
||||
}
|
||||
|
||||
+26
@@ -1,4 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
#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;
|
||||
@@ -98,3 +100,27 @@ TEST(low_latency_strategy_test, detects_render_limited_state) {
|
||||
}
|
||||
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_THROW(strategy.set_frequency_hz(std::numeric_limits<double>::quiet_NaN()), std::invalid_argument);
|
||||
EXPECT_THROW(strategy.set_frequency_hz(std::numeric_limits<double>::infinity()), std::invalid_argument);
|
||||
}
|
||||
TEST(low_latency_strategy_test, clamps_extremely_small_frequency_interval_without_overflow) {
|
||||
Low_Latency_Test_Time_Source time_source;
|
||||
Low_Latency_Test_Observer observer;
|
||||
auto strategy = make_low_latency_test_strategy(time_source, observer);
|
||||
strategy.set_frequency_hz(1e-300);
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "renderive/real_time_data/Real_Time_Data.hpp"
|
||||
#include "renderive/renderable/Renderable.hpp"
|
||||
@@ -45,15 +46,15 @@ TEST(history_real_time_data_test, retains_all_values_until_explicit_discard) {
|
||||
}
|
||||
TEST(real_time_data_attachment_test, binds_updates_to_renderable_frame_strategy) {
|
||||
Scene2D_Context<> scene;
|
||||
Real_Time_Data_Test_Latest latest;
|
||||
Real_Time_Data_Test_History history;
|
||||
auto latest = std::make_shared<Real_Time_Data_Test_Latest>();
|
||||
auto history = std::make_shared<Real_Time_Data_Test_History>();
|
||||
auto renderable = std::make_shared<Real_Time_Data_Test_Attachment>(With_Real_Time_Data(latest, history), scene);
|
||||
renderable->discard_stale_frame_on_latest_data_update = true;
|
||||
{
|
||||
auto frame = scene.frame_control.acquire_painter();
|
||||
ASSERT_TRUE(frame);
|
||||
}
|
||||
latest.update(1);
|
||||
latest->update(1);
|
||||
EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1);
|
||||
auto frame = scene.frame_control.acquire_renderer();
|
||||
EXPECT_FALSE(frame);
|
||||
@@ -61,16 +62,130 @@ TEST(real_time_data_attachment_test, binds_updates_to_renderable_frame_strategy)
|
||||
TEST(real_time_data_attachment_test, discards_history_older_than_one_frame_interval) {
|
||||
using Strategy = Low_Latency_Strategy<Scene2D_Frame_Data>;
|
||||
Scene2D_Context<Strategy> scene(Observer_State<>{}, Strategy::Configuration{100'000'000.0});
|
||||
Real_Time_Data_Test_Latest latest;
|
||||
Real_Time_Data_Test_History history;
|
||||
auto latest = std::make_shared<Real_Time_Data_Test_Latest>();
|
||||
auto history = std::make_shared<Real_Time_Data_Test_History>();
|
||||
auto renderable = std::make_shared<Real_Time_Data_Test_Attachment>(With_Real_Time_Data(latest, history), scene);
|
||||
renderable->real_time_data_discard_mode = Real_Time_Data_Discard_Mode::retain_frame_interval;
|
||||
history.update(1);
|
||||
history.update(2);
|
||||
history.update(3);
|
||||
history->update(1);
|
||||
history->update(2);
|
||||
history->update(3);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(history.snapshot(), (std::vector<int>{2, 3}));
|
||||
EXPECT_EQ(history->snapshot(), (std::vector<int>{2, 3}));
|
||||
EXPECT_EQ(renderable->render_count.load(), 1);
|
||||
}
|
||||
using Real_Time_Data_Test_Latest_Attachment = Attach_Real_Time_Data<Real_Time_Data_Test_Renderable, Real_Time_Data_Test_Latest>;
|
||||
TEST(real_time_data_attachment_test, supports_multiple_renderables_and_unbinds_destroyed_renderable) {
|
||||
Scene2D_Context<> first_scene;
|
||||
Scene2D_Context<> second_scene;
|
||||
auto latest = std::make_shared<Real_Time_Data_Test_Latest>();
|
||||
auto first = std::make_shared<Real_Time_Data_Test_Latest_Attachment>(With_Real_Time_Data(latest), first_scene);
|
||||
auto second = std::make_shared<Real_Time_Data_Test_Latest_Attachment>(With_Real_Time_Data(latest), second_scene);
|
||||
first->discard_stale_frame_on_latest_data_update = true;
|
||||
second->discard_stale_frame_on_latest_data_update = true;
|
||||
{
|
||||
auto frame = first_scene.frame_control.acquire_painter();
|
||||
ASSERT_TRUE(frame);
|
||||
}
|
||||
{
|
||||
auto frame = second_scene.frame_control.acquire_painter();
|
||||
ASSERT_TRUE(frame);
|
||||
}
|
||||
latest->update(1);
|
||||
EXPECT_EQ(first_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1);
|
||||
EXPECT_EQ(second_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1);
|
||||
first.reset();
|
||||
{
|
||||
auto frame = second_scene.frame_control.acquire_painter();
|
||||
ASSERT_TRUE(frame);
|
||||
}
|
||||
latest->update(2);
|
||||
EXPECT_EQ(first_scene.frame_control.counter_statistics().manually_discarded_frame_count, 1);
|
||||
EXPECT_EQ(second_scene.frame_control.counter_statistics().manually_discarded_frame_count, 2);
|
||||
}
|
||||
TEST(real_time_data_attachment_test, unbind_is_synchronized_with_concurrent_updates) {
|
||||
Scene2D_Context<> scene;
|
||||
auto latest = std::make_shared<Real_Time_Data_Test_Latest>();
|
||||
auto renderable = std::make_shared<Real_Time_Data_Test_Latest_Attachment>(With_Real_Time_Data(latest), scene);
|
||||
std::atomic<bool> running{true};
|
||||
std::atomic<bool> updated{};
|
||||
std::thread updater([&] {
|
||||
int value{};
|
||||
while (running.load(std::memory_order_acquire)) {
|
||||
latest->update(++value);
|
||||
updated.store(true, std::memory_order_release);
|
||||
}
|
||||
});
|
||||
while (!updated.load(std::memory_order_acquire)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
renderable.reset();
|
||||
running.store(false, std::memory_order_release);
|
||||
updater.join();
|
||||
EXPECT_TRUE(latest->revision() > 0);
|
||||
}
|
||||
TEST(real_time_data_attachment_test, runtime_discard_configuration_is_safe_during_updates_and_rendering) {
|
||||
Scene2D_Context<> scene;
|
||||
auto latest = std::make_shared<Real_Time_Data_Test_Latest>();
|
||||
auto history = std::make_shared<Real_Time_Data_Test_History>();
|
||||
auto renderable = std::make_shared<Real_Time_Data_Test_Attachment>(With_Real_Time_Data(latest, history), scene);
|
||||
scene.attach_renderable(renderable);
|
||||
std::thread updater([&] {
|
||||
for (int value = 0; value < 1000; ++value) {
|
||||
latest->update(value);
|
||||
}
|
||||
});
|
||||
std::thread configurator([&] {
|
||||
for (int index = 0; index < 1000; ++index) {
|
||||
renderable->discard_stale_frame_on_latest_data_update = index % 2 != 0;
|
||||
renderable->real_time_data_discard_mode = index % 2 == 0 ? Real_Time_Data_Discard_Mode::retain_all : Real_Time_Data_Discard_Mode::retain_frame_interval;
|
||||
}
|
||||
});
|
||||
for (int index = 0; index < 20; ++index) {
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
}
|
||||
updater.join();
|
||||
configurator.join();
|
||||
EXPECT_EQ(latest->revision(), 1000);
|
||||
}
|
||||
TEST(real_time_data_attachment_test, ignores_updates_after_bound_scene_is_destroyed) {
|
||||
auto latest = std::make_shared<Real_Time_Data_Test_Latest>();
|
||||
std::shared_ptr<Real_Time_Data_Test_Latest_Attachment> renderable;
|
||||
{
|
||||
Scene2D_Context<> scene;
|
||||
renderable = std::make_shared<Real_Time_Data_Test_Latest_Attachment>(With_Real_Time_Data(latest), scene);
|
||||
latest->update(1);
|
||||
EXPECT_EQ(scene.frame_control.state().real_time_data_update_sequence, 1);
|
||||
}
|
||||
EXPECT_NO_THROW(latest->update(2));
|
||||
EXPECT_THROW(renderable->scene(), std::logic_error);
|
||||
renderable.reset();
|
||||
}
|
||||
TEST(real_time_data_attachment_test, notifies_each_scene_once_for_one_source_update) {
|
||||
Scene2D_Context<> scene;
|
||||
auto latest = std::make_shared<Real_Time_Data_Test_Latest>();
|
||||
auto first = std::make_shared<Real_Time_Data_Test_Latest_Attachment>(With_Real_Time_Data(latest), scene);
|
||||
auto second = std::make_shared<Real_Time_Data_Test_Latest_Attachment>(With_Real_Time_Data(latest), scene);
|
||||
first->discard_stale_frame_on_latest_data_update = true;
|
||||
second->discard_stale_frame_on_latest_data_update = true;
|
||||
{
|
||||
auto frame = scene.frame_control.acquire_painter();
|
||||
ASSERT_TRUE(frame);
|
||||
}
|
||||
latest->update(1);
|
||||
EXPECT_EQ(scene.frame_control.state().real_time_data_update_sequence, 1);
|
||||
EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1);
|
||||
}
|
||||
TEST(real_time_data_attachment_test, attachment_owns_real_time_data_sources) {
|
||||
Scene2D_Context<> scene;
|
||||
auto latest = std::make_shared<Real_Time_Data_Test_Latest>();
|
||||
std::weak_ptr<Real_Time_Data_Test_Latest> weak = latest;
|
||||
auto renderable = std::make_shared<Real_Time_Data_Test_Latest_Attachment>(With_Real_Time_Data(latest), scene);
|
||||
latest.reset();
|
||||
EXPECT_FALSE(weak.expired());
|
||||
renderable->real_time_data<Real_Time_Data_Test_Latest>().update(1);
|
||||
renderable.reset();
|
||||
EXPECT_TRUE(weak.expired());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "renderive/renderable/Renderable.hpp"
|
||||
@@ -25,14 +26,83 @@ TEST(renderable_task_graph_test, combines_internal_and_external_dependencies) {
|
||||
auto dependent = std::make_shared<Renderable_Task_Graph_Test_Renderable>(scene, order, 10);
|
||||
scene.attach_renderable(dependency);
|
||||
scene.attach_renderable(dependent);
|
||||
dependency->dependency_node.append_child(dependent->dependency_node);
|
||||
scene.set_dependency_parent(*dependent, dependency.get());
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(order, (std::vector<int>{1, 2, 11, 12}));
|
||||
}
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
struct Renderable_Task_Graph_Branch_Test_Renderable : Renderable_Base {
|
||||
Renderable_Task_Graph_Branch_Test_Renderable(Scene_Base& scene, std::atomic<unsigned>& state, std::atomic<bool>& invalid)
|
||||
: Renderable_Base(scene, {.cache_enabled = false}), state(&state), invalid(&invalid) {}
|
||||
void build_task_graph(Renderable_Task_Graph& graph) override {
|
||||
auto first = graph.emplace([this](const Scene_Render_Context&) {
|
||||
state->fetch_or(1u, std::memory_order_release);
|
||||
}, "first");
|
||||
auto second = graph.emplace([this](const Scene_Render_Context&) {
|
||||
state->fetch_or(2u, std::memory_order_release);
|
||||
}, "second");
|
||||
auto join = graph.emplace([this](const Scene_Render_Context&) {
|
||||
if ((state->load(std::memory_order_acquire) & 3u) != 3u) {
|
||||
invalid->store(true, std::memory_order_release);
|
||||
}
|
||||
state->fetch_or(4u, std::memory_order_release);
|
||||
}, "join");
|
||||
graph.precede(first, join);
|
||||
graph.precede(second, join);
|
||||
}
|
||||
std::atomic<unsigned>* state;
|
||||
std::atomic<bool>* invalid;
|
||||
};
|
||||
TEST(renderable_task_graph_test, preserves_internal_fan_in_dependencies) {
|
||||
Scene2D_Context<> scene;
|
||||
std::atomic<unsigned> state{};
|
||||
std::atomic<bool> invalid{};
|
||||
auto renderable = std::make_shared<Renderable_Task_Graph_Branch_Test_Renderable>(scene, state, invalid);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_FALSE(invalid.load(std::memory_order_acquire));
|
||||
EXPECT_EQ(state.load(std::memory_order_acquire), 7u);
|
||||
}
|
||||
struct Renderable_Task_Graph_Parent_Test_Renderable : Renderable_Base {
|
||||
Renderable_Task_Graph_Parent_Test_Renderable(Scene_Base& scene, std::atomic<unsigned>& state)
|
||||
: Renderable_Base(scene, {.cache_enabled = false}), state(&state) {}
|
||||
void build_task_graph(Renderable_Task_Graph& graph) override {
|
||||
graph.emplace([this](const Scene_Render_Context&) {
|
||||
state->fetch_or(1u, std::memory_order_release);
|
||||
}, "parent_a");
|
||||
graph.emplace([this](const Scene_Render_Context&) {
|
||||
state->fetch_or(2u, std::memory_order_release);
|
||||
}, "parent_b");
|
||||
}
|
||||
std::atomic<unsigned>* state;
|
||||
};
|
||||
struct Renderable_Task_Graph_Child_Test_Renderable : Renderable_Base {
|
||||
Renderable_Task_Graph_Child_Test_Renderable(Scene_Base& scene, std::atomic<unsigned>& state, std::atomic<bool>& invalid)
|
||||
: Renderable_Base(scene, {.cache_enabled = false}), state(&state), invalid(&invalid) {}
|
||||
void render(const Scene_Render_Context&) override {
|
||||
if ((state->load(std::memory_order_acquire) & 3u) != 3u) {
|
||||
invalid->store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
std::atomic<unsigned>* state;
|
||||
std::atomic<bool>* invalid;
|
||||
};
|
||||
TEST(renderable_task_graph_test, waits_for_all_parent_terminal_tasks_before_dependent_module) {
|
||||
Scene2D_Context<> scene;
|
||||
std::atomic<unsigned> state{};
|
||||
std::atomic<bool> invalid{};
|
||||
auto parent = std::make_shared<Renderable_Task_Graph_Parent_Test_Renderable>(scene, state);
|
||||
auto child = std::make_shared<Renderable_Task_Graph_Child_Test_Renderable>(scene, state, invalid);
|
||||
scene.attach_renderable(parent);
|
||||
scene.attach_renderable(child);
|
||||
scene.set_dependency_parent(*child, parent.get());
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_FALSE(invalid.load(std::memory_order_acquire));
|
||||
EXPECT_EQ(state.load(std::memory_order_acquire), 3u);
|
||||
}
|
||||
#ifndef RENDERIVE_TASKFLOW_MOCK
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
struct Renderable_Task_Graph_Concurrency_Test_Renderable : Renderable_Base {
|
||||
@@ -52,7 +122,7 @@ struct Renderable_Task_Graph_Concurrency_Test_Renderable : Renderable_Base {
|
||||
std::atomic<int>* active;
|
||||
std::atomic<int>* maximum;
|
||||
};
|
||||
TEST(renderable_task_graph_concurrency_test, DISABLED_runs_independent_internal_tasks_concurrently) {
|
||||
TEST(renderable_task_graph_concurrency_test, runs_independent_internal_tasks_concurrently) {
|
||||
Scene2D_Context<> scene;
|
||||
std::atomic<int> active{};
|
||||
std::atomic<int> maximum{};
|
||||
@@ -62,3 +132,27 @@ TEST(renderable_task_graph_concurrency_test, DISABLED_runs_independent_internal_
|
||||
scene.wait_for_render();
|
||||
EXPECT_GE(maximum.load(std::memory_order_acquire), 2);
|
||||
}
|
||||
#endif
|
||||
TEST(renderable_task_graph_test, rejects_tasks_from_different_graphs) {
|
||||
Renderable_Task_Graph first;
|
||||
Renderable_Task_Graph second;
|
||||
auto first_task = first.emplace([](const Scene_Render_Context&) {});
|
||||
auto second_task = second.emplace([](const Scene_Render_Context&) {});
|
||||
EXPECT_THROW(first.precede(first_task, second_task), std::invalid_argument);
|
||||
}
|
||||
TEST(renderable_task_graph_test, rejects_stale_task_handles_after_clear) {
|
||||
Renderable_Task_Graph graph;
|
||||
auto stale = graph.emplace([](const Scene_Render_Context&) {});
|
||||
graph.clear();
|
||||
auto current = graph.emplace([](const Scene_Render_Context&) {});
|
||||
EXPECT_THROW(graph.precede(stale, current), std::invalid_argument);
|
||||
}
|
||||
TEST(renderable_task_graph_test, rejects_cycles_when_dependency_is_added) {
|
||||
Renderable_Task_Graph graph;
|
||||
auto first = graph.emplace([](const Scene_Render_Context&) {});
|
||||
auto second = graph.emplace([](const Scene_Render_Context&) {});
|
||||
auto third = graph.emplace([](const Scene_Render_Context&) {});
|
||||
graph.precede(first, second);
|
||||
graph.precede(second, third);
|
||||
EXPECT_THROW(graph.precede(third, first), std::invalid_argument);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include "renderive/renderable/Renderable.hpp"
|
||||
#include "renderive/scene/Scene.hpp"
|
||||
struct Renderable_Base_Test_Renderable : Renderable_Base {
|
||||
@@ -10,15 +13,15 @@ struct Renderable_Base_Test_Renderable : Renderable_Base {
|
||||
}
|
||||
int render_count{};
|
||||
};
|
||||
TEST(renderable_base_test, exposes_scene_and_strategy_members_directly) {
|
||||
TEST(renderable_base_test, exposes_scene_and_strategy_through_accessors) {
|
||||
Scene2D_Context<> low_latency_scene;
|
||||
Scene2D_Context<Manual_Refresh_Strategy<Scene2D_Frame_Data>> manual_scene;
|
||||
Renderable_Base_Test_Renderable low_latency_renderable(low_latency_scene, true);
|
||||
Renderable_Base_Test_Renderable manual_renderable(manual_scene, true);
|
||||
EXPECT_EQ(low_latency_renderable.scene, &low_latency_scene);
|
||||
EXPECT_EQ(low_latency_renderable.scene->frame_control_strategy, &low_latency_scene.frame_control);
|
||||
EXPECT_DOUBLE_EQ(low_latency_renderable.scene->frame_control_strategy->frequency_hz(), 60.0);
|
||||
EXPECT_TRUE(std::isnan(manual_renderable.scene->frame_control_strategy->frequency_hz()));
|
||||
EXPECT_EQ(&low_latency_renderable.scene(), &low_latency_scene);
|
||||
EXPECT_EQ(&low_latency_renderable.scene().frame_control_strategy(), &low_latency_scene.frame_control);
|
||||
EXPECT_DOUBLE_EQ(low_latency_renderable.scene().frame_control_strategy().frequency_hz(), 60.0);
|
||||
EXPECT_TRUE(std::isnan(manual_renderable.scene().frame_control_strategy().frequency_hz()));
|
||||
}
|
||||
TEST(renderable_base_test, controls_cache_with_configuration) {
|
||||
Scene2D_Context<> scene;
|
||||
@@ -38,3 +41,149 @@ TEST(renderable_base_test, controls_cache_with_configuration) {
|
||||
EXPECT_EQ(cached->render_count, 2);
|
||||
EXPECT_EQ(uncached->render_count, 3);
|
||||
}
|
||||
TEST(renderable_base_test, changes_configuration_through_scene) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Renderable_Base_Test_Renderable>(scene, false);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
scene.set_renderable_configuration(*renderable, {.cache_enabled = true});
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_TRUE(renderable->configuration().cache_enabled);
|
||||
EXPECT_EQ(renderable->render_count, 1);
|
||||
}
|
||||
struct Renderable_Base_Cache_Invalidation_Test_Renderable : Renderable_Base {
|
||||
explicit Renderable_Base_Cache_Invalidation_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = true}) {}
|
||||
void render(const Scene_Render_Context&) override {
|
||||
const int count = render_count.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||
if (count != 1) {
|
||||
return;
|
||||
}
|
||||
std::unique_lock lock(mutex);
|
||||
started = true;
|
||||
condition.notify_all();
|
||||
condition.wait(lock, [this] {
|
||||
return released;
|
||||
});
|
||||
}
|
||||
void wait_started() {
|
||||
std::unique_lock lock(mutex);
|
||||
condition.wait(lock, [this] {
|
||||
return started;
|
||||
});
|
||||
}
|
||||
void release() {
|
||||
std::lock_guard lock(mutex);
|
||||
released = true;
|
||||
condition.notify_all();
|
||||
}
|
||||
std::atomic<int> render_count{};
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
bool started{};
|
||||
bool released{};
|
||||
};
|
||||
TEST(renderable_base_test, preserves_cache_invalidation_that_happens_during_render) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Renderable_Base_Cache_Invalidation_Test_Renderable>(scene);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
renderable->wait_started();
|
||||
renderable->invalidate_cache();
|
||||
renderable->release();
|
||||
scene.wait_for_render();
|
||||
EXPECT_FALSE(renderable->cache_valid());
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(renderable->render_count.load(std::memory_order_acquire), 2);
|
||||
EXPECT_TRUE(renderable->cache_valid());
|
||||
}
|
||||
struct Renderable_Base_Task_Graph_Rebuild_Test_Renderable : Renderable_Base {
|
||||
explicit Renderable_Base_Task_Graph_Rebuild_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {}
|
||||
void build_task_graph(Renderable_Task_Graph& graph) override {
|
||||
const int count = build_count.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||
if (count == 1) {
|
||||
std::unique_lock lock(mutex);
|
||||
build_started = true;
|
||||
condition.notify_all();
|
||||
condition.wait(lock, [this] {
|
||||
return release_build;
|
||||
});
|
||||
}
|
||||
graph.emplace([](const Scene_Render_Context&) {}, "graph_rebuild_test");
|
||||
}
|
||||
void wait_build_started() {
|
||||
std::unique_lock lock(mutex);
|
||||
condition.wait(lock, [this] {
|
||||
return build_started;
|
||||
});
|
||||
}
|
||||
void release() {
|
||||
std::lock_guard lock(mutex);
|
||||
release_build = true;
|
||||
condition.notify_all();
|
||||
}
|
||||
std::atomic<int> build_count{};
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
bool build_started{};
|
||||
bool release_build{};
|
||||
};
|
||||
TEST(renderable_base_test, preserves_rebuild_request_that_arrives_during_task_graph_build) {
|
||||
Scene2D_Context<> scene;
|
||||
Renderable_Base_Task_Graph_Rebuild_Test_Renderable renderable(scene);
|
||||
std::shared_ptr<const Renderable_Task_Graph> first_graph;
|
||||
std::thread builder([&] {
|
||||
first_graph = renderable.task_graph();
|
||||
});
|
||||
renderable.wait_build_started();
|
||||
std::thread rebuilder([&] {
|
||||
renderable.rebuild_task_graph();
|
||||
});
|
||||
renderable.release();
|
||||
builder.join();
|
||||
rebuilder.join();
|
||||
const auto second_graph = renderable.task_graph();
|
||||
EXPECT_EQ(renderable.build_count.load(std::memory_order_acquire), 2);
|
||||
EXPECT_NE(first_graph.get(), second_graph.get());
|
||||
}
|
||||
TEST(renderable_base_test, task_graph_snapshots_remain_valid_during_concurrent_rebuilds) {
|
||||
Scene2D_Context<> scene;
|
||||
Renderable_Base_Task_Graph_Rebuild_Test_Renderable renderable(scene);
|
||||
renderable.release();
|
||||
std::atomic<bool> running{true};
|
||||
std::thread rebuilder([&] {
|
||||
for (int index = 0; index < 500; ++index) {
|
||||
renderable.rebuild_task_graph();
|
||||
}
|
||||
running.store(false, std::memory_order_release);
|
||||
});
|
||||
while (running.load(std::memory_order_acquire)) {
|
||||
const auto graph = renderable.task_graph();
|
||||
ASSERT_TRUE(graph);
|
||||
EXPECT_EQ(graph->nodes().size(), 1);
|
||||
}
|
||||
rebuilder.join();
|
||||
const auto graph = renderable.task_graph();
|
||||
ASSERT_TRUE(graph);
|
||||
EXPECT_EQ(graph->nodes().size(), 1);
|
||||
}
|
||||
TEST(renderable_base_test, configuration_snapshot_is_safe_during_scene_updates) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Renderable_Base_Test_Renderable>(scene, false);
|
||||
scene.attach_renderable(renderable);
|
||||
std::atomic<bool> running{true};
|
||||
std::thread writer([&] {
|
||||
for (int index = 0; index < 1000; ++index) {
|
||||
scene.set_renderable_configuration(*renderable, {.cache_enabled = index % 2 != 0});
|
||||
}
|
||||
running.store(false, std::memory_order_release);
|
||||
});
|
||||
while (running.load(std::memory_order_acquire)) {
|
||||
const auto configuration = renderable->configuration();
|
||||
static_cast<void>(configuration.cache_enabled);
|
||||
}
|
||||
writer.join();
|
||||
EXPECT_TRUE(renderable->configuration().cache_enabled);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,9 @@ struct Renderable_Concept_Test : Renderable_Base {
|
||||
};
|
||||
static_assert(Renderable<Renderable_Concept_Test>);
|
||||
static_assert(!std::same_as<Renderable_Base::Layer_Node, Renderable_Base::Dependency_Node>);
|
||||
TEST(renderable_concept_test, exposes_tree_nodes_scene_and_configuration) {
|
||||
TEST(renderable_concept_test, exposes_scene_and_configuration_state) {
|
||||
Scene2D_Context<> scene;
|
||||
Renderable_Concept_Test renderable(scene);
|
||||
EXPECT_EQ(renderable.scene, &scene);
|
||||
EXPECT_EQ(renderable.layer_node.owner, &renderable);
|
||||
EXPECT_EQ(renderable.dependency_node.owner, &renderable);
|
||||
EXPECT_FALSE(renderable.configuration.cache_enabled);
|
||||
EXPECT_EQ(&renderable.scene(), &scene);
|
||||
EXPECT_FALSE(renderable.configuration().cache_enabled);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include "renderive/renderable/Renderable.hpp"
|
||||
#include "renderive/scene/Scene.hpp"
|
||||
static_assert(std::same_as<decltype(std::declval<const Scene2D_Context<>&>().topology_snapshot()), Scene_Base::Topology_Snapshot>);
|
||||
struct Scene2D_Context_Test_Renderable : Renderable_Base {
|
||||
explicit Scene2D_Context_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {}
|
||||
void render(const Scene_Render_Context& context) override {
|
||||
@@ -32,5 +34,98 @@ TEST(scene2d_context_test, swaps_renderable_cache_and_renders_in_background) {
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(first->render_count.load(), 1);
|
||||
EXPECT_EQ(second->render_count.load(), 1);
|
||||
EXPECT_EQ(scene.frame_control_strategy->frame_control_state().publish_revision, 2);
|
||||
EXPECT_EQ(scene.frame_control_strategy().frame_control_state().publish_revision, 2);
|
||||
}
|
||||
struct Scene2D_Final_Cache_Blocking_Renderable : Renderable_Base {
|
||||
explicit Scene2D_Final_Cache_Blocking_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {}
|
||||
void render(const Scene_Render_Context&) override {
|
||||
while (!release.load(std::memory_order_acquire)) {
|
||||
started.store(true, std::memory_order_release);
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
std::atomic<bool> started{};
|
||||
std::atomic<bool> release{};
|
||||
};
|
||||
TEST(scene2d_context_test, final_color_cache_access_waits_for_render_completion) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Scene2D_Final_Cache_Blocking_Renderable>(scene);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
while (!renderable->started.load(std::memory_order_acquire)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
std::atomic<bool> access_started{};
|
||||
std::atomic<bool> cache_accessed{};
|
||||
std::thread reader([&] {
|
||||
access_started.store(true, std::memory_order_release);
|
||||
scene.with_final_color_cache([&](const Recording_Color_Cache&) {
|
||||
cache_accessed.store(true, std::memory_order_release);
|
||||
});
|
||||
});
|
||||
while (!access_started.load(std::memory_order_acquire)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
EXPECT_FALSE(cache_accessed.load(std::memory_order_acquire));
|
||||
renderable->release.store(true, std::memory_order_release);
|
||||
reader.join();
|
||||
EXPECT_TRUE(cache_accessed.load(std::memory_order_acquire));
|
||||
}
|
||||
struct Scene2D_Cached_Renderable : Renderable_Base {
|
||||
Scene2D_Cached_Renderable(Scene_Base& scene, std::uint64_t value) : Renderable_Base(scene, {.cache_enabled = true}), value(value) {}
|
||||
void render(const Scene_Render_Context& context) override {
|
||||
++render_count;
|
||||
dynamic_cast<Recording_Color_Cache&>(*context.color_cache).append(value);
|
||||
}
|
||||
std::uint64_t value;
|
||||
int render_count{};
|
||||
};
|
||||
TEST(scene2d_context_test, reattached_cached_renderable_rebuilds_removed_color_cache) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Scene2D_Cached_Renderable>(scene, 7);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(renderable->render_count, 1);
|
||||
scene.detach_renderable(*renderable);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(renderable->render_count, 2);
|
||||
scene.with_final_color_cache([](const Recording_Color_Cache& cache) {
|
||||
EXPECT_EQ(cache.values.size(), 1);
|
||||
EXPECT_EQ(cache.values.at(0), 7);
|
||||
});
|
||||
}
|
||||
TEST(scene2d_context_test, dependency_reparent_invalidates_cached_child) {
|
||||
Scene2D_Context<> scene;
|
||||
auto first_parent = std::make_shared<Scene2D_Cached_Renderable>(scene, 1);
|
||||
auto second_parent = std::make_shared<Scene2D_Cached_Renderable>(scene, 2);
|
||||
auto child = std::make_shared<Scene2D_Cached_Renderable>(scene, 3);
|
||||
scene.attach_renderable(first_parent);
|
||||
scene.attach_renderable(second_parent);
|
||||
scene.attach_renderable(child);
|
||||
scene.set_dependency_parent(*child, first_parent.get());
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(first_parent->render_count, 1);
|
||||
EXPECT_EQ(second_parent->render_count, 1);
|
||||
EXPECT_EQ(child->render_count, 1);
|
||||
scene.set_dependency_parent(*child, second_parent.get());
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(first_parent->render_count, 1);
|
||||
EXPECT_EQ(second_parent->render_count, 1);
|
||||
EXPECT_EQ(child->render_count, 2);
|
||||
}
|
||||
TEST(scene2d_context_test, final_color_cache_callback_can_reenter_scene_control_api) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Scene2D_Cached_Renderable>(scene, 9);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
scene.with_final_color_cache([&](const Recording_Color_Cache&) {
|
||||
scene.set_renderable_configuration(*renderable, {.cache_enabled = false});
|
||||
});
|
||||
EXPECT_FALSE(renderable->configuration().cache_enabled);
|
||||
}
|
||||
|
||||
@@ -24,12 +24,16 @@ TEST(scene2d_render_order_test, separates_dependency_order_from_display_order) {
|
||||
auto axis = std::make_shared<Scene2D_Render_Order_Renderable>(scene, state, 1);
|
||||
scene.attach_renderable(spectrum);
|
||||
scene.attach_renderable(axis);
|
||||
axis->dependency_node.append_child(spectrum->dependency_node);
|
||||
spectrum->layer_node.append_child(axis->layer_node);
|
||||
scene.set_dependency_parent(*spectrum, axis.get());
|
||||
scene.set_display_parent(*axis, spectrum.get());
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(state.render_order, (std::vector<std::uint64_t>{1, 2}));
|
||||
EXPECT_EQ(scene.final_color_cache.values.size(), 2);
|
||||
EXPECT_EQ(scene.final_color_cache.values.at(0), 2);
|
||||
EXPECT_EQ(scene.final_color_cache.values.at(1), 1);
|
||||
std::vector<std::uint64_t> final_values;
|
||||
scene.with_final_color_cache([&final_values](const Recording_Color_Cache& cache) {
|
||||
final_values.assign(cache.values.begin(), cache.values.end());
|
||||
});
|
||||
EXPECT_EQ(final_values.size(), 2);
|
||||
EXPECT_EQ(final_values.at(0), 2);
|
||||
EXPECT_EQ(final_values.at(1), 1);
|
||||
}
|
||||
|
||||
@@ -21,3 +21,32 @@ TEST(scene3d_context_test, uses_scene_3d_base_contract) {
|
||||
EXPECT_EQ(renderable->render_count.load(), 1);
|
||||
EXPECT_TRUE(renderable->scene_is_3d);
|
||||
}
|
||||
struct Scene3D_Dependency_Cache_Test_Renderable : Renderable_Base {
|
||||
explicit Scene3D_Dependency_Cache_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = true}) {}
|
||||
void render(const Scene_Render_Context&) override {
|
||||
++render_count;
|
||||
}
|
||||
int render_count{};
|
||||
};
|
||||
TEST(scene3d_context_test, propagates_cache_invalidation_through_unsorted_dependency_chain) {
|
||||
Scene3D_Context<> scene;
|
||||
auto leaf = std::make_shared<Scene3D_Dependency_Cache_Test_Renderable>(scene);
|
||||
auto middle = std::make_shared<Scene3D_Dependency_Cache_Test_Renderable>(scene);
|
||||
auto root = std::make_shared<Scene3D_Dependency_Cache_Test_Renderable>(scene);
|
||||
scene.attach_renderable(leaf);
|
||||
scene.attach_renderable(middle);
|
||||
scene.attach_renderable(root);
|
||||
scene.set_dependency_parent(*leaf, middle.get());
|
||||
scene.set_dependency_parent(*middle, root.get());
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(root->render_count, 1);
|
||||
EXPECT_EQ(middle->render_count, 1);
|
||||
EXPECT_EQ(leaf->render_count, 1);
|
||||
root->invalidate_cache();
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_EQ(root->render_count, 2);
|
||||
EXPECT_EQ(middle->render_count, 2);
|
||||
EXPECT_EQ(leaf->render_count, 2);
|
||||
}
|
||||
|
||||
@@ -96,3 +96,17 @@ TEST(scene_memory_resource_test, supports_independent_resource_aware_components)
|
||||
EXPECT_EQ(unique_product->value, 8);
|
||||
EXPECT_GT(memory_resource.allocation_count(), 0);
|
||||
}
|
||||
TEST(scene_memory_resource_test, keeps_independent_renderable_memory_domain_alive_until_renderable_release) {
|
||||
Scene_Memory_Resource_Test_Resource upstream;
|
||||
std::shared_ptr<Scene_Memory_Resource_Test_Renderable> renderable;
|
||||
{
|
||||
Scene2D_Context<> scene(upstream);
|
||||
renderable = std::make_shared<Scene_Memory_Resource_Test_Renderable>(scene);
|
||||
renderable->task_graph();
|
||||
}
|
||||
auto& memory_resource = renderable->memory_resource();
|
||||
void* storage = memory_resource.allocate(32, alignof(std::max_align_t));
|
||||
memory_resource.deallocate(storage, 32, alignof(std::max_align_t));
|
||||
renderable.reset();
|
||||
EXPECT_GT(upstream.deallocation_count(), 0);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "renderive/scene/Scene.hpp"
|
||||
@@ -34,3 +36,32 @@ TEST(scene_state_observer_test, scene_uses_triple_state_and_observes_lifecycle)
|
||||
EXPECT_EQ(scene.Scene_State_Strategy::render_use_state().value, 9);
|
||||
EXPECT_EQ(recorder.data->events, (std::vector<Scene_Base::Observation_Event>{Scene_Base::Observation_Event::render_submitted, Scene_Base::Observation_Event::render_started, Scene_Base::Observation_Event::render_completed}));
|
||||
}
|
||||
struct Scene_Reentrant_Observer_Data {
|
||||
std::function<void(Scene_Base::Observation_Event)> callback;
|
||||
};
|
||||
struct Scene_Reentrant_Observer {
|
||||
static constexpr bool enabled = true;
|
||||
std::shared_ptr<Scene_Reentrant_Observer_Data> data{std::make_shared<Scene_Reentrant_Observer_Data>()};
|
||||
void observe(const Scene_Base::Observation& observation) noexcept {
|
||||
if (data->callback) {
|
||||
data->callback(observation.event);
|
||||
}
|
||||
}
|
||||
};
|
||||
using Scene_Reentrant_Observer_State = Observer_State<Scene_Reentrant_Observer, Scene_State_Observer_Test_Time_Source>;
|
||||
TEST(scene_state_observer_test, render_worker_observer_can_wait_for_current_render_without_deadlock) {
|
||||
Scene_Reentrant_Observer recorder;
|
||||
auto data = recorder.data;
|
||||
using Scene = Scene2D_Context<Low_Latency_Strategy<Scene2D_Frame_Data>, Recording_Color_Cache, Scene_State_Observer_Test_State, Scene_State_Observer_Test_State_Observer, Scene_Reentrant_Observer_State>;
|
||||
Scene scene(With_Observer(Scene_State_Observer_Test_State_Observer{}), With_Observer(Scene_Reentrant_Observer_State(recorder, Scene_State_Observer_Test_Time_Source{})));
|
||||
std::atomic<bool> reentered{};
|
||||
data->callback = [&](Scene_Base::Observation_Event event) {
|
||||
if (event == Scene_Base::Observation_Event::render_started) {
|
||||
scene.wait_for_render();
|
||||
reentered.store(true, std::memory_order_release);
|
||||
}
|
||||
};
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
EXPECT_TRUE(reentered.load(std::memory_order_acquire));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include "renderive/renderable/Renderable.hpp"
|
||||
#include "renderive/scene/Scene.hpp"
|
||||
struct Scene_Base_Test_Renderable : Renderable_Base {
|
||||
@@ -21,3 +26,192 @@ TEST(scene_base_test, publishes_renderable_collection_before_background_render)
|
||||
EXPECT_EQ(renderable->render_sequence, 1);
|
||||
EXPECT_EQ(scene.renderable_count(), 1);
|
||||
}
|
||||
struct Scene_Base_Throwing_Renderable : Renderable_Base {
|
||||
explicit Scene_Base_Throwing_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {}
|
||||
void render(const Scene_Render_Context&) override {
|
||||
throw std::runtime_error("render failed");
|
||||
}
|
||||
};
|
||||
TEST(scene_base_test, wait_for_render_propagates_background_render_failure) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Scene_Base_Throwing_Renderable>(scene);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
EXPECT_THROW(scene.wait_for_render(), std::runtime_error);
|
||||
}
|
||||
TEST(scene_base_test, scene_destruction_does_not_throw_after_background_render_failure) {
|
||||
{
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Scene_Base_Throwing_Renderable>(scene);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
}
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
struct Scene_Base_Blocking_Renderable : Renderable_Base {
|
||||
explicit Scene_Base_Blocking_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {}
|
||||
void render(const Scene_Render_Context&) override {
|
||||
std::unique_lock lock(mutex);
|
||||
rendering = true;
|
||||
condition.notify_all();
|
||||
condition.wait(lock, [this] {
|
||||
return released;
|
||||
});
|
||||
}
|
||||
void wait_rendering() {
|
||||
std::unique_lock lock(mutex);
|
||||
condition.wait(lock, [this] {
|
||||
return rendering;
|
||||
});
|
||||
}
|
||||
void release() {
|
||||
std::lock_guard lock(mutex);
|
||||
released = true;
|
||||
condition.notify_all();
|
||||
}
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
bool rendering{};
|
||||
bool released{};
|
||||
};
|
||||
TEST(scene_base_test, scene_mutation_waits_until_background_render_is_idle) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Scene_Base_Blocking_Renderable>(scene);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
renderable->wait_rendering();
|
||||
std::atomic<bool> mutation_finished{};
|
||||
std::thread mutation([&] {
|
||||
scene.set_renderable_configuration(*renderable, {.cache_enabled = true});
|
||||
mutation_finished.store(true, std::memory_order_release);
|
||||
});
|
||||
std::this_thread::yield();
|
||||
EXPECT_FALSE(mutation_finished.load(std::memory_order_acquire));
|
||||
renderable->release();
|
||||
mutation.join();
|
||||
EXPECT_TRUE(mutation_finished.load(std::memory_order_acquire));
|
||||
EXPECT_TRUE(renderable->configuration().cache_enabled);
|
||||
}
|
||||
TEST(scene_base_test, concurrent_render_and_topology_updates_remain_serialized) {
|
||||
Scene2D_Context<> scene;
|
||||
auto parent = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
auto child = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
scene.attach_renderable(parent);
|
||||
scene.attach_renderable(child);
|
||||
std::thread renderer([&] {
|
||||
for (int index = 0; index < 100; ++index) {
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
}
|
||||
});
|
||||
std::thread mutator([&] {
|
||||
for (int index = 0; index < 100; ++index) {
|
||||
Renderable_Base* value = index % 2 == 0 ? parent.get() : nullptr;
|
||||
scene.set_dependency_parent(*child, value);
|
||||
scene.set_display_parent(*child, value);
|
||||
scene.set_renderable_configuration(*child, {.cache_enabled = index % 2 == 0});
|
||||
}
|
||||
});
|
||||
renderer.join();
|
||||
mutator.join();
|
||||
EXPECT_EQ(scene.renderable_count(), 2);
|
||||
}
|
||||
TEST(scene_base_test, detach_releases_renderable_from_both_scene_buffers) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
std::weak_ptr<Scene_Base_Test_Renderable> weak = renderable;
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
scene.wait_for_render();
|
||||
scene.detach_renderable(*renderable);
|
||||
renderable.reset();
|
||||
EXPECT_TRUE(weak.expired());
|
||||
EXPECT_EQ(scene.renderable_count(), 0);
|
||||
}
|
||||
TEST(scene_base_test, attach_rejects_null_renderable) {
|
||||
Scene2D_Context<> scene;
|
||||
EXPECT_THROW(scene.attach_renderable({}), std::invalid_argument);
|
||||
}
|
||||
TEST(scene_base_test, topology_rejects_unattached_renderables) {
|
||||
Scene2D_Context<> scene;
|
||||
auto parent = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
auto child = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
scene.attach_renderable(child);
|
||||
EXPECT_THROW(scene.set_dependency_parent(*child, parent.get()), std::invalid_argument);
|
||||
EXPECT_THROW(scene.set_display_parent(*child, parent.get()), std::invalid_argument);
|
||||
scene.attach_renderable(parent);
|
||||
auto detached_child = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
EXPECT_THROW(scene.set_dependency_parent(*detached_child, parent.get()), std::invalid_argument);
|
||||
}
|
||||
TEST(scene_base_test, setting_same_topology_parent_is_a_noop) {
|
||||
Scene2D_Context<> scene;
|
||||
auto parent = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
auto child = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
scene.attach_renderable(parent);
|
||||
scene.attach_renderable(child);
|
||||
scene.set_dependency_parent(*child, parent.get());
|
||||
const auto revision = child->cache_revision();
|
||||
EXPECT_NO_THROW(scene.set_dependency_parent(*child, parent.get()));
|
||||
EXPECT_EQ(child->cache_revision(), revision);
|
||||
const auto topology = scene.topology_snapshot();
|
||||
EXPECT_EQ(topology.dependency.size(), 2);
|
||||
bool found{};
|
||||
for (const auto& relation : topology.dependency) {
|
||||
if (relation.child.get() == child.get()) {
|
||||
EXPECT_EQ(relation.parent.get(), parent.get());
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found);
|
||||
}
|
||||
TEST(scene_base_test, topology_snapshot_retains_renderable_lifetime) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
std::weak_ptr<Scene_Base_Test_Renderable> weak = renderable;
|
||||
scene.attach_renderable(renderable);
|
||||
const auto snapshot = scene.topology_snapshot();
|
||||
scene.detach_renderable(*renderable);
|
||||
renderable.reset();
|
||||
EXPECT_FALSE(weak.expired());
|
||||
EXPECT_EQ(snapshot.renderables.size(), 1);
|
||||
EXPECT_EQ(snapshot.renderables.front().get(), weak.lock().get());
|
||||
}
|
||||
TEST(scene_base_test, topology_snapshot_is_safe_during_topology_updates) {
|
||||
Scene2D_Context<> scene;
|
||||
auto parent = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
auto child = std::make_shared<Scene_Base_Test_Renderable>(scene);
|
||||
scene.attach_renderable(parent);
|
||||
scene.attach_renderable(child);
|
||||
std::thread writer([&] {
|
||||
for (int index = 0; index < 500; ++index) {
|
||||
scene.set_dependency_parent(*child, index % 2 == 0 ? parent.get() : nullptr);
|
||||
scene.set_display_parent(*child, index % 2 == 0 ? parent.get() : nullptr);
|
||||
}
|
||||
});
|
||||
for (int index = 0; index < 500; ++index) {
|
||||
const auto snapshot = scene.topology_snapshot();
|
||||
EXPECT_EQ(snapshot.renderables.size(), 2);
|
||||
EXPECT_EQ(snapshot.display.size(), 2);
|
||||
EXPECT_EQ(snapshot.dependency.size(), 2);
|
||||
}
|
||||
writer.join();
|
||||
}
|
||||
TEST(scene_base_test, all_waiters_receive_the_same_render_failure) {
|
||||
Scene2D_Context<> scene;
|
||||
auto renderable = std::make_shared<Scene_Base_Throwing_Renderable>(scene);
|
||||
scene.attach_renderable(renderable);
|
||||
scene.render();
|
||||
std::atomic<int> failures{};
|
||||
auto waiter = [&] {
|
||||
try {
|
||||
scene.wait_for_render();
|
||||
} catch (const std::runtime_error&) {
|
||||
failures.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
std::thread first(waiter);
|
||||
std::thread second(waiter);
|
||||
first.join();
|
||||
second.join();
|
||||
EXPECT_EQ(failures.load(), 2);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@ TEST(double_state_strategy_test, keeps_cache_separate_until_publish) {
|
||||
product->publish();
|
||||
EXPECT_EQ(product->render_use_state().percent.get(), 60);
|
||||
}
|
||||
TEST(double_state_strategy_test, returns_render_state_snapshot) {
|
||||
State_Plain_Strategy strategy(State_Plain_Value{.count = 1, .name = "snapshot"});
|
||||
const auto snapshot = strategy.render_use_state();
|
||||
strategy.set<&State_Plain_Value::count>(2);
|
||||
strategy.publish();
|
||||
EXPECT_EQ(snapshot.count, 1);
|
||||
EXPECT_EQ(strategy.render_use_state().count, 2);
|
||||
}
|
||||
TEST(double_state_strategy_test, keeps_cached_value_when_runtime_validation_fails) {
|
||||
auto product = State_Checked_Product::Builder{}.set<&State_Checked_Value::percent>(20).set<&State_Checked_Value::batch_size>(4).build(1, "state");
|
||||
EXPECT_THROW((product->set<&State_Checked_Value::percent>(200)), std::out_of_range);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include "renderive/state/State_Strategy.hpp"
|
||||
struct Triple_State_Test_Base {};
|
||||
struct Triple_State_Test_State {
|
||||
@@ -14,6 +16,15 @@ TEST(triple_state_strategy_test, publishes_then_acquires_render_state) {
|
||||
EXPECT_EQ(strategy.acquire_render_state(), 1);
|
||||
EXPECT_EQ(strategy.render_use_state().value, 7);
|
||||
}
|
||||
TEST(triple_state_strategy_test, returns_render_state_snapshot) {
|
||||
Triple_State_Strategy<Triple_State_Test_Base, Triple_State_Test_State> strategy;
|
||||
const auto snapshot = strategy.render_use_state();
|
||||
strategy.set<&Triple_State_Test_State::value>(7);
|
||||
strategy.publish();
|
||||
strategy.acquire_render_state();
|
||||
EXPECT_EQ(snapshot.value, 0);
|
||||
EXPECT_EQ(strategy.render_use_state().value, 7);
|
||||
}
|
||||
struct Multi_State_A_Base {};
|
||||
struct Multi_State_B_Base {};
|
||||
struct Multi_State_A {
|
||||
@@ -34,3 +45,24 @@ TEST(double_state_strategy_test, supports_named_base_access_in_multiple_inherita
|
||||
EXPECT_EQ(product.Multi_State_A_Strategy::render_use_state().value, 3);
|
||||
EXPECT_EQ(product.Multi_State_B_Strategy::render_use_state().value, 8);
|
||||
}
|
||||
TEST(triple_state_strategy_test, concurrently_acquires_render_revision_without_data_race) {
|
||||
Triple_State_Strategy<Triple_State_Test_Base, Triple_State_Test_State> strategy;
|
||||
std::atomic<bool> finished{};
|
||||
auto acquire = [&] {
|
||||
while (!finished.load(std::memory_order_acquire)) {
|
||||
strategy.acquire_render_state();
|
||||
}
|
||||
strategy.acquire_render_state();
|
||||
};
|
||||
std::thread first(acquire);
|
||||
std::thread second(acquire);
|
||||
for (int value = 1; value <= 1000; ++value) {
|
||||
strategy.set<&Triple_State_Test_State::value>(value);
|
||||
strategy.publish();
|
||||
}
|
||||
finished.store(true, std::memory_order_release);
|
||||
first.join();
|
||||
second.join();
|
||||
EXPECT_EQ(strategy.acquire_render_state(), 1000);
|
||||
EXPECT_EQ(strategy.render_use_state().value, 1000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user