内核优化

This commit is contained in:
2026-08-10 13:50:17 +08:00
parent 2abdba699d
commit 302b1dcdfd
46 changed files with 1955 additions and 515 deletions
@@ -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);
}
@@ -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);
}