Files
Renderive/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp
T
2026-08-12 09:45:18 +08:00

287 lines
12 KiB
C++

#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 {
explicit Scene_Base_Test_Renderable(Scene_Base& scene) : Renderable_Base(scene) {}
void render(const Scene_Render_Context& context) override {
render_sequence = context.render_sequence;
++render_count;
}
std::uint64_t render_sequence{};
int render_count{};
};
TEST(scene_base_test, publishes_renderable_collection_before_background_render) {
Scene2D_Context<> scene;
auto renderable = std::make_shared<Scene_Base_Test_Renderable>(scene);
scene.attach_renderable(renderable);
scene.render();
scene.wait_for_render();
EXPECT_EQ(renderable->render_count, 1);
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);
const auto execution = scene.task_execution_snapshot();
EXPECT_EQ(execution.state, Task_Execution_State::Failed);
EXPECT_EQ(execution.graph_node_count, 3u);
EXPECT_EQ(execution.failed_count, 1u);
EXPECT_EQ(execution.running_count, 0u);
EXPECT_GT(execution.duration_ns, 0u);
}
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, display_graph_rejects_cycles) {
Scene2D_Context<> scene;
auto first = std::make_shared<Scene_Base_Test_Renderable>(scene);
auto second = std::make_shared<Scene_Base_Test_Renderable>(scene);
auto third = std::make_shared<Scene_Base_Test_Renderable>(scene);
scene.attach_renderable(first);
scene.attach_renderable(second);
scene.attach_renderable(third);
scene.set_display_parent(*second, first.get());
scene.add_display_parent(*third, *second);
EXPECT_THROW(scene.add_display_parent(*first, *third), std::invalid_argument);
}
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);
}
struct Scene_Base_Throw_Once_Renderable : Renderable_Base {
explicit Scene_Base_Throw_Once_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {}
void render(const Scene_Render_Context&) override {
if (render_count.fetch_add(1, std::memory_order_acq_rel) == 0) {
throw std::runtime_error("first render failed");
}
}
std::atomic<int> render_count{};
};
TEST(scene_base_test, unobserved_render_failure_survives_next_successful_submission) {
Scene2D_Context<> scene;
auto renderable = std::make_shared<Scene_Base_Throw_Once_Renderable>(scene);
scene.attach_renderable(renderable);
scene.render();
scene.render();
EXPECT_THROW(scene.wait_for_render(), std::runtime_error);
EXPECT_EQ(renderable->render_count.load(std::memory_order_acquire), 2);
scene.render();
EXPECT_NO_THROW(scene.wait_for_render());
EXPECT_EQ(renderable->render_count.load(std::memory_order_acquire), 3);
}
struct Scene_Base_Render_Execution_Renderable : Renderable_Base {
explicit Scene_Base_Render_Execution_Renderable(Scene_Base& scene) : Renderable_Base(scene, {.cache_enabled = false}) {}
void render(const Scene_Render_Context& context) override {
context.scene->wait_for_render();
wait_returned.store(true, std::memory_order_release);
try {
context.scene->render();
} catch (const std::logic_error&) {
nested_render_rejected.store(true, std::memory_order_release);
}
try {
context.scene->set_renderable_configuration(*this, {.cache_enabled = true});
} catch (const std::logic_error&) {
mutation_rejected.store(true, std::memory_order_release);
}
}
std::atomic<bool> wait_returned{};
std::atomic<bool> nested_render_rejected{};
std::atomic<bool> mutation_rejected{};
};
TEST(scene_base_test, taskflow_execution_thread_cannot_wait_on_or_mutate_current_scene) {
Scene2D_Context<> scene;
auto renderable = std::make_shared<Scene_Base_Render_Execution_Renderable>(scene);
scene.attach_renderable(renderable);
scene.render();
scene.wait_for_render();
EXPECT_TRUE(renderable->wait_returned.load(std::memory_order_acquire));
EXPECT_TRUE(renderable->nested_render_rejected.load(std::memory_order_acquire));
EXPECT_TRUE(renderable->mutation_rejected.load(std::memory_order_acquire));
}