Files
Renderive/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp
T

260 lines
10 KiB
C++

#include <gtest/gtest.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <thread>
#include "renderive/renderable/Renderable.hpp"
#include "renderive/scene/Scene.hpp"
#include "renderive/scene/Scene_Test_Helpers.hpp"
namespace {
template <class Predicate>
bool wait_until(Predicate&& predicate) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (!predicate()) {
if (std::chrono::steady_clock::now() >= deadline)
return false;
std::this_thread::yield();
}
return true;
}
}
struct Scene_Base_Test_Renderable : Renderable_Base {
Scene_Base_Test_Renderable() = default;
void prepare(const Prepare_Render_Context& context) override {
render_sequence = context.frame.render_sequence;
++render_count;
}
std::uint64_t render_sequence{};
int render_count{};
};
struct Scene_Base_Throwing_Renderable : Renderable_Base {
Scene_Base_Throwing_Renderable() : Renderable_Base({.cache_enabled = false}) {}
void prepare(const Prepare_Render_Context&) override {
throw std::runtime_error("render failed");
}
};
struct Scene_Base_Blocking_Renderable : Renderable_Base {
Scene_Base_Blocking_Renderable() : Renderable_Base({.cache_enabled = false}) {}
void prepare(const Prepare_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, attach_builder_is_synchronous_before_runtime) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Scene_Base_Test_Renderable>::make();
{
auto builder = scene.attach_builder();
builder.attach(renderable);
EXPECT_EQ(scene.renderable_count(), 1u);
}
scene.render();
scene.wait_for_render();
EXPECT_EQ(renderable->render_count, 1);
EXPECT_EQ(renderable->render_sequence, 1);
}
TEST(scene_base_test, attach_builder_can_build_complete_initial_topology) {
Scene2D_Context<> scene;
auto parent = renderive_Owner<Scene_Base_Test_Renderable>::make();
auto child = renderive_Owner<Scene_Base_Test_Renderable>::make();
{
auto builder = scene.attach_builder();
builder.attach(parent);
builder.attach(child);
builder.set_parent(Scene_Base::Relationship::display, child, parent);
builder.set_parent(Scene_Base::Relationship::dependency, child, parent);
}
const auto topology = scene.topology_snapshot();
EXPECT_EQ(topology.renderables.size(), 2u);
EXPECT_TRUE(std::ranges::any_of(topology.display, [&](const auto& relation) {
return relation.child.get() == child.get() && relation.parent.get() == parent.get();
}));
EXPECT_TRUE(std::ranges::any_of(topology.dependency, [&](const auto& relation) {
return relation.child.get() == child.get() && relation.parent.get() == parent.get();
}));
}
TEST(scene_base_test, attach_builder_rejects_null_renderable) {
Scene2D_Context<> scene;
auto builder = scene.attach_builder();
EXPECT_THROW(builder.attach({}), std::invalid_argument);
}
TEST(scene_base_test, attach_builder_is_rejected_after_runtime_starts) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Scene_Base_Test_Renderable>::make();
attach_initial(scene, renderable);
scene.render();
scene.wait_for_render();
EXPECT_THROW(scene.attach_builder(), std::logic_error);
}
TEST(scene_base_test, edit_renderables_waits_for_inflight_frame) {
Scene2D_Context<> scene;
auto blocking = renderive_Owner<Scene_Base_Blocking_Renderable>::make();
auto added = renderive_Owner<Scene_Base_Test_Renderable>::make();
attach_initial(scene, blocking);
scene.render();
blocking->wait_rendering();
std::atomic<bool> edited{};
scene.edit_renderables([added, &edited](auto& editor) {
editor.attach(added);
edited.store(true, std::memory_order_release);
});
EXPECT_FALSE(edited.load(std::memory_order_acquire));
blocking->release();
scene.wait_for_render();
EXPECT_TRUE(wait_until([&] { return edited.load(std::memory_order_acquire); }));
EXPECT_EQ(added->render_count, 0);
scene.render();
scene.wait_for_render();
EXPECT_EQ(added->render_count, 1);
}
TEST(scene_base_test, one_runtime_edit_can_rewire_and_detach_without_internal_repair) {
Scene2D_Context<> scene;
auto first_parent = renderive_Owner<Scene_Base_Test_Renderable>::make();
auto second_parent = renderive_Owner<Scene_Base_Test_Renderable>::make();
auto child = renderive_Owner<Scene_Base_Test_Renderable>::make();
with_attach_builder(scene, [&](auto& builder) {
builder.attach(first_parent);
builder.attach(second_parent);
builder.attach(child);
builder.set_parent(Scene_Base::Relationship::display, child, first_parent);
builder.set_parent(Scene_Base::Relationship::dependency, child, first_parent);
});
wait_renderable_edit(scene, [first_parent, second_parent, child](auto& editor) {
editor.set_parent(Scene_Base::Relationship::display, child, second_parent);
editor.set_parent(Scene_Base::Relationship::dependency, child, second_parent);
editor.detach(first_parent);
});
const auto topology = scene.topology_snapshot();
EXPECT_EQ(topology.renderables.size(), 2u);
EXPECT_TRUE(std::ranges::any_of(topology.display, [&](const auto& relation) {
return relation.child.get() == child.get() && relation.parent.get() == second_parent.get();
}));
}
TEST(scene_base_test, detach_releases_scene_ownership_after_edit_applies) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Scene_Base_Test_Renderable>::make();
std::weak_ptr<Scene_Base_Test_Renderable> weak = renderable.share();
attach_initial(scene, renderable);
wait_renderable_edit(scene, [renderable](auto& editor) {
editor.detach(renderable);
});
renderable.reset();
EXPECT_TRUE(weak.expired());
EXPECT_EQ(scene.renderable_count(), 0u);
}
TEST(scene_base_test, topology_snapshot_retains_renderable_lifetime) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Scene_Base_Test_Renderable>::make();
std::weak_ptr<Scene_Base_Test_Renderable> weak = renderable.share();
attach_initial(scene, renderable);
const auto snapshot = scene.topology_snapshot();
wait_renderable_edit(scene, [renderable](auto& editor) {
editor.detach(renderable);
});
renderable.reset();
EXPECT_FALSE(weak.expired());
EXPECT_EQ(snapshot.renderables.size(), 1u);
EXPECT_EQ(snapshot.renderables.front().get(), weak.lock().get());
}
TEST(scene_base_test, non_structural_configuration_update_does_not_wait_for_render) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Scene_Base_Blocking_Renderable>::make();
attach_initial(scene, renderable);
scene.render();
renderable->wait_rendering();
std::atomic<bool> updated{};
std::thread writer([&] {
renderable->set_configuration({.cache_enabled = true});
updated.store(true, std::memory_order_release);
});
EXPECT_TRUE(wait_until([&] { return updated.load(std::memory_order_acquire); }));
renderable->release();
writer.join();
scene.wait_for_render();
EXPECT_TRUE(renderable->configuration().cache_enabled);
}
TEST(scene_base_test, concurrent_render_and_runtime_structure_edits_are_serialized_by_worker) {
Scene2D_Context<> scene;
auto parent = renderive_Owner<Scene_Base_Test_Renderable>::make();
auto child = renderive_Owner<Scene_Base_Test_Renderable>::make();
attach_initial(scene, parent, child);
std::thread renderer([&] {
for (int index = 0; index < 50; ++index) {
scene.render();
scene.wait_for_render();
}
});
std::thread editor([&] {
for (int index = 0; index < 50; ++index) {
scene.edit_renderables([parent, child, index](auto& edit) {
if (index % 2 == 0) {
edit.set_parent(Scene_Base::Relationship::dependency, child, parent);
edit.set_parent(Scene_Base::Relationship::display, child, parent);
} else {
edit.clear_parents(Scene_Base::Relationship::dependency, child);
edit.clear_parents(Scene_Base::Relationship::display, child);
}
});
}
});
renderer.join();
editor.join();
scene.render();
scene.wait_for_render();
EXPECT_EQ(scene.renderable_count(), 2u);
}
TEST(scene_base_test, wait_for_render_propagates_background_render_failure) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Scene_Base_Throwing_Renderable>::make();
attach_initial(scene, 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 = renderive_Owner<Scene_Base_Throwing_Renderable>::make();
attach_initial(scene, renderable);
scene.render();
}
EXPECT_TRUE(true);
}
TEST(scene_base_test, all_waiters_receive_the_same_render_failure) {
Scene2D_Context<> scene;
auto renderable = renderive_Owner<Scene_Base_Throwing_Renderable>::make();
attach_initial(scene, 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);
}