修改成异步的接口

This commit is contained in:
2026-08-13 08:45:18 +08:00
parent 6d2cf825ae
commit b964dcdcd9
23 changed files with 553 additions and 227 deletions
@@ -51,14 +51,14 @@ public:
}
for (std::size_t index = 0; index < entries->size(); ++index) {
const auto& entry = (*entries)[index];
if (!entry->active.load(std::memory_order_acquire) || !entry->target->renderable_alive.load(std::memory_order_acquire)) {
if (!entry->active.load(std::memory_order_acquire) || !entry->target->renderable_alive.load(std::memory_order_acquire) || !entry->target->attached.load(std::memory_order_acquire)) {
continue;
}
const auto& lifetime = entry->target->scene_lifetime;
bool first_for_scene = true;
for (std::size_t previous = 0; previous < index; ++previous) {
const auto& previous_entry = (*entries)[previous];
if (previous_entry->active.load(std::memory_order_acquire) && previous_entry->target->renderable_alive.load(std::memory_order_acquire) && previous_entry->target->scene_lifetime.get() == lifetime.get()) {
if (previous_entry->active.load(std::memory_order_acquire) && previous_entry->target->renderable_alive.load(std::memory_order_acquire) && previous_entry->target->attached.load(std::memory_order_acquire) && previous_entry->target->scene_lifetime.get() == lifetime.get()) {
first_for_scene = false;
break;
}
@@ -69,7 +69,7 @@ public:
bool discard_stale_frame{};
if (observation.state.retention == Real_Time_Data_Retention::latest) {
for (const auto& value : *entries) {
if (value->active.load(std::memory_order_acquire) && value->target->renderable_alive.load(std::memory_order_acquire) && value->target->scene_lifetime.get() == lifetime.get() && value->target->discard_stale_frame_on_latest_data_update.load(std::memory_order_acquire)) {
if (value->active.load(std::memory_order_acquire) && value->target->renderable_alive.load(std::memory_order_acquire) && value->target->attached.load(std::memory_order_acquire) && value->target->scene_lifetime.get() == lifetime.get() && value->target->discard_stale_frame_on_latest_data_update.load(std::memory_order_acquire)) {
discard_stale_frame = true;
break;
}
@@ -8,5 +8,5 @@ public:
virtual ~Render_Frame_Completion() = default;
private:
friend class Scene_Base;
virtual void render_frame_completed(std::uint64_t target_interval_ns) noexcept = 0;
virtual void render_frame_completed(std::uint64_t target_interval_ns) = 0;
};
@@ -16,6 +16,7 @@ void advance_revision(std::atomic<std::uint64_t>& revision) noexcept {
Renderable_Base::Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration)
: memory_domain_(scene.memory_domain_), real_time_data_state_(std::allocate_shared<Real_Time_Data_State>(Scene_Memory_Allocator<Real_Time_Data_State>{memory_domain_}, scene.scene_lifetime_)), discard_stale_frame_on_latest_data_update(real_time_data_state_->discard_stale_frame_on_latest_data_update), renderable_id_(scene.allocate_renderable_id()), composite_node_id_(scene.allocate_node_id()), configuration_(configuration) {}
Renderable_Base::~Renderable_Base() {
real_time_data_state_->attached.store(false, std::memory_order_release);
real_time_data_state_->renderable_alive.store(false, std::memory_order_release);
}
void Renderable_Base::invalidate_prepare() noexcept {
@@ -48,7 +49,10 @@ bool Renderable_Base::paint_cache_valid() const noexcept {
painted_revision() == revision &&
painted_prepare_revision_.load(std::memory_order_acquire) == prepared_revision();
}
void Renderable_Base::rebuild_render_graph() noexcept {
void Renderable_Base::rebuild_render_graph() {
scene().request_render_graph_rebuild(*this);
}
void Renderable_Base::reset_render_graph() noexcept {
{
std::lock_guard lock(render_graph_mutex_);
render_graph_.reset();
@@ -24,6 +24,7 @@ private:
explicit Real_Time_Data_State(std::shared_ptr<Scene_Lifetime> scene_lifetime) : scene_lifetime(std::move(scene_lifetime)) {}
std::shared_ptr<Scene_Lifetime> scene_lifetime;
std::atomic<bool> renderable_alive{true};
std::atomic<bool> attached{};
std::atomic<bool> discard_stale_frame_on_latest_data_update{};
};
std::shared_ptr<Scene_Memory_Domain> memory_domain_;
@@ -50,7 +51,7 @@ protected:
virtual void build_prepare_graph(Renderable_Graph_Builder& builder);
virtual void build_paint_graph(Renderable_Graph_Builder& builder);
virtual void prepare(const Prepare_Render_Context& context);
void rebuild_render_graph() noexcept;
void rebuild_render_graph();
[[nodiscard]] std::shared_ptr<const Renderable_Graph> render_graph();
[[nodiscard]] static Render_State_View render_state_view() noexcept;
[[nodiscard]] Real_Time_Data_Binding bind_real_time_data(Real_Time_Data_Base& data);
@@ -64,6 +65,7 @@ private:
void register_real_time_data(Real_Time_Data_Base& data);
void unregister_real_time_data(Real_Time_Data_Base& data) noexcept;
void publish_real_time_data();
void reset_render_graph() noexcept;
const Renderable_Id renderable_id_;
const Render_Node_Id composite_node_id_;
mutable std::mutex configuration_mutex_;
+317 -143
View File
@@ -312,6 +312,7 @@ Scene_Base::Scene_Base(std::pmr::memory_resource& upstream_memory_resource)
dependency_topology_(memory_domain_->resource()),
color_caches_(&memory_domain_->resource()),
task_(memory_domain_->resource()),
mutation_queue_(&memory_domain_->resource()),
composite_begin_node_id_(allocate_node_id()) {
worker_ = std::thread([this] { render_loop(); });
}
@@ -319,7 +320,63 @@ Scene_Base::Scene_Base(std::pmr::memory_resource& upstream_memory_resource)
Scene_Base::~Scene_Base() {
shutdown();
}
void Scene_Base::Mutation::wait() const {
if (!state_)
return;
std::unique_lock lock(state_->mutex);
if (!state_->completed && state_->scene && state_->scene->is_render_worker_thread())
throw std::logic_error("scene mutation wait is not allowed from render execution");
state_->condition.wait(lock, [this] { return state_->completed; });
if (state_->exception)
std::rethrow_exception(state_->exception);
}
bool Scene_Base::Mutation::ready() const {
if (!state_)
return true;
std::lock_guard lock(state_->mutex);
return state_->completed;
}
Scene_Base::Attach_Builder::Attach_Builder(Scene_Base& scene)
: scene_(&scene), task_lock_(scene.task_mutex_) {
if (scene.runtime_started_)
throw std::logic_error("scene attach builder is only available before runtime starts");
}
void Scene_Base::Attach_Builder::attach_renderable(Renderable renderable,
Attach_Relationships relationships) {
std::lock_guard lock(scene_->model_mutex_);
scene_->apply_attach_renderable_locked(std::move(renderable), std::move(relationships));
}
void Scene_Base::Attach_Builder::set_display_parent(const Renderable& child,
const Renderable& parent) {
std::lock_guard lock(scene_->model_mutex_);
scene_->apply_set_display_parent_locked(child, parent);
}
void Scene_Base::Attach_Builder::add_display_parent(const Renderable& child,
const Renderable& parent) {
std::lock_guard lock(scene_->model_mutex_);
scene_->apply_add_display_parent_locked(child, parent);
}
void Scene_Base::Attach_Builder::clear_display_parent(const Renderable& child) {
std::lock_guard lock(scene_->model_mutex_);
scene_->apply_clear_display_parent_locked(child);
}
void Scene_Base::Attach_Builder::set_dependency_parent(const Renderable& child,
const Renderable& parent) {
std::lock_guard lock(scene_->model_mutex_);
scene_->apply_set_dependency_parent_locked(child, parent);
}
void Scene_Base::Attach_Builder::add_dependency_parent(const Renderable& child,
const Renderable& parent) {
std::lock_guard lock(scene_->model_mutex_);
scene_->apply_add_dependency_parent_locked(child, parent);
}
void Scene_Base::Attach_Builder::clear_dependency_parent(const Renderable& child) {
std::lock_guard lock(scene_->model_mutex_);
scene_->apply_clear_dependency_parent_locked(child);
}
Scene_Base::Attach_Builder Scene_Base::attach_builder() {
return Attach_Builder(*this);
}
void Scene_Base::render() {
submit_render(nullptr);
}
@@ -335,6 +392,7 @@ void Scene_Base::submit_render(Abstract_Frame* frame) {
return;
}
auto task_lock = lock_render_idle();
runtime_started_ = true;
if (pending_exception_observed_) {
pending_exception_ = nullptr;
pending_exception_observed_ = false;
@@ -410,6 +468,14 @@ void Scene_Base::wait_for_render() {
if (exception)
std::rethrow_exception(exception);
}
void Scene_Base::wait_for_mutations() {
if (is_render_worker_thread())
throw std::logic_error("scene mutation wait is not allowed from render execution");
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
render_completed_.wait(lock, [this] {
return mutation_queue_.empty() && !mutating_;
});
}
std::vector<Renderable_Id> Scene_Base::relationship_ids_locked(
const std::vector<Renderable>& renderables) const {
@@ -424,184 +490,244 @@ std::vector<Renderable_Id> Scene_Base::relationship_ids_locked(
}
return ids;
}
void Scene_Base::attach_renderable(Renderable renderable,
Attach_Relationships relationships) {
Scene_Base::Mutation Scene_Base::enqueue_mutation(std::function<void()> operation,
Mutation_Callback callback) {
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
return enqueue_mutation_locked(std::move(operation), std::move(callback));
}
Scene_Base::Mutation Scene_Base::enqueue_mutation_locked(std::function<void()> operation,
Mutation_Callback callback) {
if (stop_)
throw std::logic_error("scene is shutting down");
runtime_started_ = true;
auto completion = std::make_shared<Mutation::State>(*this);
mutation_queue_.push_back({std::move(operation), std::move(callback), completion});
task_ready_.notify_one();
return Mutation(std::move(completion));
}
void Scene_Base::complete_mutation(Mutation_Command& command,
std::exception_ptr exception) noexcept {
{
std::lock_guard lock(command.completion->mutex);
command.completion->exception = exception;
command.completion->completed = true;
command.completion->scene = nullptr;
}
command.completion->condition.notify_all();
}
void Scene_Base::apply_attach_renderable_locked(Renderable renderable,
Attach_Relationships relationships) {
if (!renderable)
throw std::invalid_argument("renderable is null");
validate_renderable_scene(*renderable);
{
std::lock_guard lock(model_mutex_);
if (is_renderable_attached_locked(renderable))
return;
const auto display_parents =
relationship_ids_locked(relationships.display_parents);
const auto display_children =
relationship_ids_locked(relationships.display_children);
const auto dependency_parents =
relationship_ids_locked(relationships.dependency_parents);
auto cache = make_renderable_color_cache();
const Renderable_Id id = renderable->renderable_id();
bool display_attached{};
bool dependency_attached{};
bool renderable_inserted{};
try {
dependency_topology_.attach(id, dependency_parents);
dependency_attached = true;
display_topology_.attach(id, display_parents, display_children);
display_attached = true;
if (!renderables_.try_emplace(id, renderable).second)
throw std::logic_error("renderable id is already attached");
renderable_inserted = true;
if (cache && !color_caches_.try_emplace(id, std::move(cache)).second)
throw std::logic_error("renderable color cache is already attached");
} catch (...) {
color_caches_.erase(id);
if (renderable_inserted)
renderables_.erase(id);
if (display_attached)
display_topology_.erase(id);
if (dependency_attached)
dependency_topology_.erase(id);
throw;
}
renderable->invalidate_prepare();
if (is_renderable_attached_locked(renderable))
return;
const auto display_parents = relationship_ids_locked(relationships.display_parents);
const auto display_children = relationship_ids_locked(relationships.display_children);
const auto dependency_parents = relationship_ids_locked(relationships.dependency_parents);
auto cache = make_renderable_color_cache();
const Renderable_Id id = renderable->renderable_id();
bool display_attached{};
bool dependency_attached{};
bool renderable_inserted{};
try {
dependency_topology_.attach(id, dependency_parents);
dependency_attached = true;
display_topology_.attach(id, display_parents, display_children);
display_attached = true;
if (!renderables_.try_emplace(id, renderable).second)
throw std::logic_error("renderable id is already attached");
renderable_inserted = true;
if (cache && !color_caches_.try_emplace(id, std::move(cache)).second)
throw std::logic_error("renderable color cache is already attached");
} catch (...) {
color_caches_.erase(id);
if (renderable_inserted)
renderables_.erase(id);
if (display_attached)
display_topology_.erase(id);
if (dependency_attached)
dependency_topology_.erase(id);
throw;
}
renderable->real_time_data_state_->attached.store(true, std::memory_order_release);
renderable->invalidate_prepare();
notify_model_dirty();
}
void Scene_Base::detach_renderable(const Renderable& renderable) {
void Scene_Base::apply_detach_renderable_locked(const Renderable& renderable) {
if (!renderable)
throw std::invalid_argument("renderable is null");
validate_renderable_scene(*renderable);
bool detached{};
{
std::lock_guard lock(model_mutex_);
if (!is_renderable_attached_locked(renderable))
return;
const Renderable_Id id = renderable->renderable_id();
display_topology_.detach_and_promote(id);
const auto dependency_children =
dependency_topology_.detach_and_promote(id);
for (const Renderable_Id child : dependency_children) {
if (const auto iterator = renderables_.find(child);
iterator != renderables_.end())
iterator->second->invalidate_prepare();
}
color_caches_.erase(id);
renderables_.erase(id);
detached = true;
if (!is_renderable_attached_locked(renderable))
return;
const Renderable_Id id = renderable->renderable_id();
renderable->real_time_data_state_->attached.store(false, std::memory_order_release);
display_topology_.detach_and_promote(id);
const auto dependency_children = dependency_topology_.detach_and_promote(id);
for (const Renderable_Id child : dependency_children) {
if (const auto iterator = renderables_.find(child); iterator != renderables_.end())
iterator->second->invalidate_prepare();
}
if (detached)
notify_model_dirty();
color_caches_.erase(id);
renderables_.erase(id);
notify_model_dirty();
}
void Scene_Base::set_display_parent(const Renderable& child,
const Renderable& parent) {
void Scene_Base::apply_set_display_parent_locked(const Renderable& child,
const Renderable& parent) {
if (!child || !parent)
throw std::invalid_argument("display relationship endpoint is null");
validate_renderable_scene(*child);
validate_renderable_scene(*parent);
bool changed{};
{
std::lock_guard lock(model_mutex_);
validate_renderable_attached_locked(child);
validate_renderable_attached_locked(parent);
changed = display_topology_.replace_parents(
child->renderable_id(), {parent->renderable_id()});
}
if (changed)
validate_renderable_attached_locked(child);
validate_renderable_attached_locked(parent);
if (display_topology_.replace_parents(child->renderable_id(), {parent->renderable_id()}))
notify_model_dirty();
}
void Scene_Base::add_display_parent(const Renderable& child,
const Renderable& parent) {
void Scene_Base::apply_add_display_parent_locked(const Renderable& child,
const Renderable& parent) {
if (!child || !parent)
throw std::invalid_argument("display relationship endpoint is null");
validate_renderable_scene(*child);
validate_renderable_scene(*parent);
bool changed{};
{
std::lock_guard lock(model_mutex_);
validate_renderable_attached_locked(child);
validate_renderable_attached_locked(parent);
changed = display_topology_.add_parent(child->renderable_id(),
parent->renderable_id());
}
if (changed)
validate_renderable_attached_locked(child);
validate_renderable_attached_locked(parent);
if (display_topology_.add_parent(child->renderable_id(), parent->renderable_id()))
notify_model_dirty();
}
void Scene_Base::clear_display_parent(const Renderable& child) {
void Scene_Base::apply_clear_display_parent_locked(const Renderable& child) {
if (!child)
throw std::invalid_argument("display child is null");
validate_renderable_scene(*child);
bool changed{};
{
std::lock_guard lock(model_mutex_);
validate_renderable_attached_locked(child);
changed = display_topology_.clear_parents(child->renderable_id());
}
if (changed)
validate_renderable_attached_locked(child);
if (display_topology_.clear_parents(child->renderable_id()))
notify_model_dirty();
}
void Scene_Base::set_dependency_parent(const Renderable& child,
const Renderable& parent) {
void Scene_Base::apply_set_dependency_parent_locked(const Renderable& child,
const Renderable& parent) {
if (!child || !parent)
throw std::invalid_argument("dependency relationship endpoint is null");
validate_renderable_scene(*child);
validate_renderable_scene(*parent);
bool changed{};
{
std::lock_guard lock(model_mutex_);
validate_renderable_attached_locked(child);
validate_renderable_attached_locked(parent);
changed = dependency_topology_.replace_parents(
child->renderable_id(), {parent->renderable_id()});
if (changed)
child->invalidate_prepare();
}
if (changed)
notify_model_dirty();
validate_renderable_attached_locked(child);
validate_renderable_attached_locked(parent);
if (!dependency_topology_.replace_parents(child->renderable_id(), {parent->renderable_id()}))
return;
child->invalidate_prepare();
notify_model_dirty();
}
void Scene_Base::add_dependency_parent(const Renderable& child,
const Renderable& parent) {
void Scene_Base::apply_add_dependency_parent_locked(const Renderable& child,
const Renderable& parent) {
if (!child || !parent)
throw std::invalid_argument("dependency relationship endpoint is null");
validate_renderable_scene(*child);
validate_renderable_scene(*parent);
bool changed{};
{
std::lock_guard lock(model_mutex_);
validate_renderable_attached_locked(child);
validate_renderable_attached_locked(parent);
changed = dependency_topology_.add_parent(child->renderable_id(),
parent->renderable_id());
if (changed)
child->invalidate_prepare();
}
if (changed)
notify_model_dirty();
validate_renderable_attached_locked(child);
validate_renderable_attached_locked(parent);
if (!dependency_topology_.add_parent(child->renderable_id(), parent->renderable_id()))
return;
child->invalidate_prepare();
notify_model_dirty();
}
void Scene_Base::clear_dependency_parent(const Renderable& child) {
void Scene_Base::apply_clear_dependency_parent_locked(const Renderable& child) {
if (!child)
throw std::invalid_argument("dependency child is null");
validate_renderable_scene(*child);
bool changed{};
{
std::lock_guard lock(model_mutex_);
validate_renderable_attached_locked(child);
changed = dependency_topology_.clear_parents(child->renderable_id());
if (changed)
child->invalidate_prepare();
}
if (changed)
notify_model_dirty();
validate_renderable_attached_locked(child);
if (!dependency_topology_.clear_parents(child->renderable_id()))
return;
child->invalidate_prepare();
notify_model_dirty();
}
Scene_Base::Mutation Scene_Base::attach_renderable(Renderable renderable,
Attach_Relationships relationships,
Mutation_Callback callback) {
if (!renderable)
throw std::invalid_argument("renderable is null");
validate_renderable_scene(*renderable);
return enqueue_mutation(
[this, renderable = std::move(renderable), relationships = std::move(relationships)]() mutable {
apply_attach_renderable_locked(std::move(renderable), std::move(relationships));
},
std::move(callback));
}
Scene_Base::Mutation Scene_Base::detach_renderable(const Renderable& renderable,
Mutation_Callback callback) {
if (!renderable)
throw std::invalid_argument("renderable is null");
validate_renderable_scene(*renderable);
return enqueue_mutation([this, renderable] { apply_detach_renderable_locked(renderable); },
std::move(callback));
}
Scene_Base::Mutation Scene_Base::set_display_parent(const Renderable& child,
const Renderable& parent,
Mutation_Callback callback) {
if (!child || !parent)
throw std::invalid_argument("display relationship endpoint is null");
validate_renderable_scene(*child);
validate_renderable_scene(*parent);
return enqueue_mutation([this, child, parent] { apply_set_display_parent_locked(child, parent); },
std::move(callback));
}
Scene_Base::Mutation Scene_Base::add_display_parent(const Renderable& child,
const Renderable& parent,
Mutation_Callback callback) {
if (!child || !parent)
throw std::invalid_argument("display relationship endpoint is null");
validate_renderable_scene(*child);
validate_renderable_scene(*parent);
return enqueue_mutation([this, child, parent] { apply_add_display_parent_locked(child, parent); },
std::move(callback));
}
Scene_Base::Mutation Scene_Base::clear_display_parent(const Renderable& child,
Mutation_Callback callback) {
if (!child)
throw std::invalid_argument("display child is null");
validate_renderable_scene(*child);
return enqueue_mutation([this, child] { apply_clear_display_parent_locked(child); },
std::move(callback));
}
Scene_Base::Mutation Scene_Base::set_dependency_parent(const Renderable& child,
const Renderable& parent,
Mutation_Callback callback) {
if (!child || !parent)
throw std::invalid_argument("dependency relationship endpoint is null");
validate_renderable_scene(*child);
validate_renderable_scene(*parent);
return enqueue_mutation([this, child, parent] { apply_set_dependency_parent_locked(child, parent); },
std::move(callback));
}
Scene_Base::Mutation Scene_Base::add_dependency_parent(const Renderable& child,
const Renderable& parent,
Mutation_Callback callback) {
if (!child || !parent)
throw std::invalid_argument("dependency relationship endpoint is null");
validate_renderable_scene(*child);
validate_renderable_scene(*parent);
return enqueue_mutation([this, child, parent] { apply_add_dependency_parent_locked(child, parent); },
std::move(callback));
}
Scene_Base::Mutation Scene_Base::clear_dependency_parent(const Renderable& child,
Mutation_Callback callback) {
if (!child)
throw std::invalid_argument("dependency child is null");
validate_renderable_scene(*child);
return enqueue_mutation([this, child] { apply_clear_dependency_parent_locked(child); },
std::move(callback));
}
void Scene_Base::request_render_graph_rebuild(Renderable_Base& renderable) {
validate_renderable_scene(renderable);
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
if (!runtime_started_) {
renderable.reset_render_graph();
notify_model_dirty();
return;
}
auto owner = renderable.shared_from_this();
static_cast<void>(enqueue_mutation_locked([this, owner = std::move(owner)] {
owner->reset_render_graph();
notify_model_dirty();
}, {}));
}
void Scene_Base::publish_frame_state() {
auto task_lock = lock_render_idle();
if (auto* state = dynamic_cast<State_Strategy_Base*>(this))
@@ -867,7 +993,7 @@ std::unique_lock<std::recursive_mutex> Scene_Base::lock_render_idle() {
"render-idle operation is not allowed during render submission observation");
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
render_completed_.wait(lock, [this] {
return !task_pending_ && !rendering_;
return !task_pending_ && !rendering_ && mutation_queue_.empty() && !mutating_;
});
return lock;
}
@@ -886,27 +1012,75 @@ void Scene_Base::shutdown() noexcept {
{
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
render_completed_.wait(lock, [this] {
return !task_pending_ && !rendering_;
return !task_pending_ && !rendering_ && mutation_queue_.empty() && !mutating_;
});
stop_ = true;
}
task_ready_.notify_one();
if (worker_.joinable())
worker_.join();
{
std::lock_guard lock(model_mutex_);
for (const auto& [id, renderable] : renderables_) {
static_cast<void>(id);
renderable->real_time_data_state_->attached.store(false, std::memory_order_release);
}
}
scene_lifetime_->invalidate();
}
void Scene_Base::render_loop() {
for (;;) {
Render_Task task(memory_resource());
std::vector<Mutation_Command> mutations;
{
std::unique_lock<std::recursive_mutex> lock(task_mutex_);
task_ready_.wait(lock, [this] { return stop_ || task_pending_; });
task_ready_.wait(lock, [this] {
return stop_ || task_pending_ || !mutation_queue_.empty();
});
if (stop_)
return;
task = std::move(task_);
task_pending_ = false;
rendering_ = true;
if (task_pending_) {
task = std::move(task_);
task_pending_ = false;
rendering_ = true;
} else {
mutating_ = true;
mutations.reserve(mutation_queue_.size());
while (!mutation_queue_.empty()) {
mutations.push_back(std::move(mutation_queue_.front()));
mutation_queue_.pop_front();
}
}
}
if (!mutations.empty()) {
std::vector<std::exception_ptr> exceptions(mutations.size());
{
std::lock_guard lock(model_mutex_);
for (std::size_t index = 0; index < mutations.size(); ++index) {
try {
mutations[index].operation();
} catch (...) {
exceptions[index] = std::current_exception();
}
}
}
for (std::size_t index = 0; index < mutations.size(); ++index)
complete_mutation(mutations[index], exceptions[index]);
{
std::lock_guard<std::recursive_mutex> lock(task_mutex_);
mutating_ = false;
}
render_completed_.notify_all();
for (std::size_t index = 0; index < mutations.size(); ++index) {
if (mutations[index].callback) {
try {
mutations[index].callback(exceptions[index]);
} catch (...) {
}
}
}
continue;
}
const auto& snapshot = *task.snapshot;
observe_scene({Observation_Event::render_started, observer_now_ns(),
+90 -9
View File
@@ -4,6 +4,7 @@
#include <condition_variable>
#include <concepts>
#include <cstdint>
#include <deque>
#include <exception>
#include <functional>
#include <memory>
@@ -43,6 +44,7 @@ private:
public:
using Renderable = std::shared_ptr<Renderable_Base>;
using Const_Renderable = std::shared_ptr<const Renderable_Base>;
using Mutation_Callback = std::function<void(std::exception_ptr)>;
struct Attach_Relationships {
std::vector<Renderable> display_parents;
@@ -50,6 +52,46 @@ public:
std::vector<Renderable> dependency_parents;
};
class Mutation {
public:
Mutation() = default;
void wait() const;
[[nodiscard]] bool ready() const;
private:
struct State {
explicit State(Scene_Base& scene) : scene(&scene) {}
Scene_Base* scene{};
mutable std::mutex mutex;
std::condition_variable condition;
std::exception_ptr exception;
bool completed{};
};
explicit Mutation(std::shared_ptr<State> state) : state_(std::move(state)) {}
std::shared_ptr<State> state_;
friend class Scene_Base;
};
class Attach_Builder {
public:
Attach_Builder(const Attach_Builder&) = delete;
Attach_Builder& operator=(const Attach_Builder&) = delete;
Attach_Builder(Attach_Builder&&) noexcept = default;
Attach_Builder& operator=(Attach_Builder&&) noexcept = default;
void attach_renderable(Renderable renderable,
Attach_Relationships relationships = {});
void set_display_parent(const Renderable& child, const Renderable& parent);
void add_display_parent(const Renderable& child, const Renderable& parent);
void clear_display_parent(const Renderable& child);
void set_dependency_parent(const Renderable& child, const Renderable& parent);
void add_dependency_parent(const Renderable& child, const Renderable& parent);
void clear_dependency_parent(const Renderable& child);
private:
explicit Attach_Builder(Scene_Base& scene);
Scene_Base* scene_{};
std::unique_lock<std::recursive_mutex> task_lock_;
friend class Scene_Base;
};
enum class Observation_Event {
render_submitted,
render_started,
@@ -87,18 +129,28 @@ public:
void render();
void render(Abstract_Frame& frame);
void wait_for_render();
void wait_for_mutations();
void publish_frame_state();
void notify_model_dirty() noexcept;
void attach_renderable(Renderable renderable,
Attach_Relationships relationships = {});
void detach_renderable(const Renderable& renderable);
void set_display_parent(const Renderable& child, const Renderable& parent);
void add_display_parent(const Renderable& child, const Renderable& parent);
void clear_display_parent(const Renderable& child);
void set_dependency_parent(const Renderable& child, const Renderable& parent);
void add_dependency_parent(const Renderable& child, const Renderable& parent);
void clear_dependency_parent(const Renderable& child);
Attach_Builder attach_builder();
Mutation attach_renderable(Renderable renderable,
Attach_Relationships relationships = {},
Mutation_Callback callback = {});
Mutation detach_renderable(const Renderable& renderable,
Mutation_Callback callback = {});
Mutation set_display_parent(const Renderable& child, const Renderable& parent,
Mutation_Callback callback = {});
Mutation add_display_parent(const Renderable& child, const Renderable& parent,
Mutation_Callback callback = {});
Mutation clear_display_parent(const Renderable& child,
Mutation_Callback callback = {});
Mutation set_dependency_parent(const Renderable& child, const Renderable& parent,
Mutation_Callback callback = {});
Mutation add_dependency_parent(const Renderable& child, const Renderable& parent,
Mutation_Callback callback = {});
Mutation clear_dependency_parent(const Renderable& child,
Mutation_Callback callback = {});
void set_renderable_configuration(const Renderable& renderable,
Renderable_Configuration configuration);
@@ -211,6 +263,12 @@ private:
std::shared_ptr<Render_Completion> completion;
};
struct Mutation_Command {
std::function<void()> operation;
Mutation_Callback callback;
std::shared_ptr<Mutation::State> completion;
};
class Execution_Context;
class Render_Execution_Scope;
@@ -221,6 +279,26 @@ private:
};
void submit_render(Abstract_Frame* frame);
[[nodiscard]] Mutation enqueue_mutation(std::function<void()> operation,
Mutation_Callback callback);
[[nodiscard]] Mutation enqueue_mutation_locked(std::function<void()> operation,
Mutation_Callback callback);
void complete_mutation(Mutation_Command& command,
std::exception_ptr exception) noexcept;
void apply_attach_renderable_locked(Renderable renderable,
Attach_Relationships relationships);
void apply_detach_renderable_locked(const Renderable& renderable);
void apply_set_display_parent_locked(const Renderable& child,
const Renderable& parent);
void apply_add_display_parent_locked(const Renderable& child,
const Renderable& parent);
void apply_clear_display_parent_locked(const Renderable& child);
void apply_set_dependency_parent_locked(const Renderable& child,
const Renderable& parent);
void apply_add_dependency_parent_locked(const Renderable& child,
const Renderable& parent);
void apply_clear_dependency_parent_locked(const Renderable& child);
void request_render_graph_rebuild(Renderable_Base& renderable);
[[nodiscard]] std::shared_ptr<Frame_Render_Snapshot> snapshot_live_model();
std::shared_ptr<const Render_Plan> compile_render_plan(
const Frame_Render_Snapshot& snapshot, Render_Task& task);
@@ -252,6 +330,7 @@ private:
std::condition_variable_any render_completed_;
std::thread worker_;
Render_Task task_;
std::pmr::deque<Mutation_Command> mutation_queue_;
std::shared_ptr<Render_Completion> current_completion_;
std::exception_ptr pending_exception_;
bool pending_exception_observed_{};
@@ -259,6 +338,8 @@ private:
std::uint64_t render_sequence_{};
bool task_pending_{};
bool rendering_{};
bool mutating_{};
bool runtime_started_{};
bool stop_{};
std::atomic<Renderable_Id> next_renderable_id_{1};
@@ -95,6 +95,7 @@ TEST(real_time_data_attachment_test, binds_updates_to_renderable_frame_strategy)
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;
scene.attach_renderable(renderable).wait();
{
auto frame = scene.frame_control.acquire_painter();
ASSERT_TRUE(frame);
@@ -133,6 +134,8 @@ TEST(real_time_data_attachment_test, supports_multiple_renderables_and_unbinds_d
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;
first_scene.attach_renderable(first).wait();
second_scene.attach_renderable(second).wait();
{
auto frame = first_scene.frame_control.acquire_painter();
ASSERT_TRUE(frame);
@@ -144,6 +147,7 @@ TEST(real_time_data_attachment_test, supports_multiple_renderables_and_unbinds_d
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_scene.detach_renderable(first).wait();
first.reset();
{
auto frame = second_scene.frame_control.acquire_painter();
@@ -153,6 +157,26 @@ TEST(real_time_data_attachment_test, supports_multiple_renderables_and_unbinds_d
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, detached_renderable_no_longer_drives_frame_strategy) {
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);
renderable->discard_stale_frame_on_latest_data_update = true;
scene.attach_renderable(renderable).wait();
{
auto frame = scene.frame_control.acquire_painter();
ASSERT_TRUE(frame);
}
latest->update(1);
EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1);
scene.detach_renderable(renderable).wait();
{
auto frame = scene.frame_control.acquire_painter();
ASSERT_TRUE(frame);
}
latest->update(2);
EXPECT_EQ(scene.frame_control.counter_statistics().manually_discarded_frame_count, 1);
}
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>();
@@ -205,6 +229,7 @@ TEST(real_time_data_attachment_test, ignores_updates_after_bound_scene_is_destro
{
Scene2D_Context<> scene;
renderable = std::make_shared<Real_Time_Data_Test_Latest_Attachment>(With_Real_Time_Data(latest), scene);
scene.attach_renderable(renderable).wait();
latest->update(1);
EXPECT_EQ(scene.frame_control.state().real_time_data_update_sequence, 1);
}
@@ -219,6 +244,8 @@ TEST(real_time_data_attachment_test, notifies_each_scene_once_for_one_source_upd
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;
scene.attach_renderable(first).wait();
scene.attach_renderable(second).wait();
{
auto frame = scene.frame_control.acquire_painter();
ASSERT_TRUE(frame);
@@ -261,6 +288,7 @@ TEST(real_time_data_attachment_test, frame_strategy_observer_can_reacquire_scene
Scene2D_Context<Real_Time_Data_Reentrant_Strategy> scene(std::move(observer_state), configuration);
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);
scene.attach_renderable(renderable).wait();
std::atomic<bool> reacquired{};
observer_data->callback = [&] {
reacquired.store(&renderable->scene() == &scene, std::memory_order_release);
@@ -128,7 +128,7 @@ struct Renderable_Base_Render_Graph_Rebuild_Test_Renderable : Renderable_Base {
std::shared_ptr<const Renderable_Graph> graph_snapshot() {
return render_graph();
}
void request_graph_rebuild() noexcept {
void request_graph_rebuild() {
rebuild_render_graph();
}
std::atomic<int> build_count{};
@@ -95,57 +95,50 @@ private:
};
TEST(dynamic_renderable_lifecycle_test,
attach_during_render_is_non_blocking_and_visible_on_the_next_frame) {
attach_during_render_is_queued_until_the_current_frame_finishes) {
Scene2D_Context<> scene;
auto gate = std::make_shared<Gate>();
auto current = std::make_shared<Blocking_Renderable>(scene, gate);
auto attached = std::make_shared<Counting_Renderable>(scene);
scene.attach_renderable(current);
scene.attach_renderable(current).wait();
scene.render();
gate->wait_until_arrived();
std::atomic<bool> attach_finished{};
std::thread controller([&] {
scene.attach_renderable(attached);
attach_finished.store(true, std::memory_order_release);
std::atomic<bool> callback_called{};
auto mutation = scene.attach_renderable(attached, {}, [&](std::exception_ptr exception) {
EXPECT_FALSE(exception);
callback_called.store(true, std::memory_order_release);
});
EXPECT_TRUE(wait_until([&] { return attach_finished.load(std::memory_order_acquire); }));
EXPECT_FALSE(mutation.ready());
EXPECT_FALSE(callback_called.load(std::memory_order_acquire));
EXPECT_EQ(attached->prepare_count.load(std::memory_order_acquire), 0);
gate->open();
controller.join();
scene.wait_for_render();
mutation.wait();
EXPECT_TRUE(wait_until([&] { return callback_called.load(std::memory_order_acquire); }));
EXPECT_EQ(attached->prepare_count.load(std::memory_order_acquire), 0);
scene.render();
scene.wait_for_render();
EXPECT_EQ(attached->prepare_count.load(std::memory_order_acquire), 1);
}
TEST(dynamic_renderable_lifecycle_test,
detach_during_render_keeps_the_frame_owner_alive_until_execution_finishes) {
detach_during_render_waits_for_the_frame_before_releasing_scene_ownership) {
Scene2D_Context<> scene;
auto gate = std::make_shared<Gate>();
std::atomic<bool> destroyed{};
auto renderable = std::make_shared<Blocking_Renderable>(scene, gate, 1, &destroyed);
std::weak_ptr<Blocking_Renderable> weak = renderable;
scene.attach_renderable(renderable);
scene.attach_renderable(renderable).wait();
scene.render();
gate->wait_until_arrived();
std::atomic<bool> detach_finished{};
std::thread controller([&] {
scene.detach_renderable(renderable);
detach_finished.store(true, std::memory_order_release);
});
EXPECT_TRUE(wait_until([&] { return detach_finished.load(std::memory_order_acquire); }));
controller.join();
auto mutation = scene.detach_renderable(renderable);
EXPECT_FALSE(mutation.ready());
renderable.reset();
EXPECT_FALSE(destroyed.load(std::memory_order_acquire));
EXPECT_FALSE(weak.expired());
gate->open();
scene.wait_for_render();
mutation.wait();
EXPECT_TRUE(wait_until([&] { return destroyed.load(std::memory_order_acquire); }));
EXPECT_TRUE(weak.expired());
EXPECT_EQ(scene.renderable_count(), 0u);
@@ -164,15 +157,16 @@ TEST(dynamic_renderable_lifecycle_test,
scene.attach_renderable(child, {
.dependency_parents = {first_parent}
});
scene.wait_for_mutations();
scene.render();
first_gate->wait_until_arrived();
scene.set_dependency_parent(child, second_parent);
auto reparent = scene.set_dependency_parent(child, second_parent);
EXPECT_FALSE(reparent.ready());
EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 0);
first_gate->open();
scene.wait_for_render();
reparent.wait();
EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 1);
scene.render();
second_gate->wait_until_arrived();
EXPECT_EQ(child->prepare_count.load(std::memory_order_acquire), 1);
@@ -258,9 +252,9 @@ TEST(dynamic_renderable_lifecycle_test,
scene.render();
scene.wait_for_render();
scene.wait_for_mutations();
EXPECT_EQ(renderable->prepare_count.load(std::memory_order_acquire), 1);
EXPECT_EQ(scene.renderable_count(), 0u);
scene.render();
scene.wait_for_render();
EXPECT_EQ(renderable->prepare_count.load(std::memory_order_acquire), 1);
@@ -327,21 +321,25 @@ TEST(dynamic_renderable_lifecycle_test,
Scene2D_Context<Low_Latency_Strategy<Scene2D_Frame_Data>, Tracking_Color_Cache> scene;
auto gate = std::make_shared<Gate>();
auto renderable = std::make_shared<Blocking_Painted_Renderable>(scene, gate);
scene.attach_renderable(renderable);
scene.attach_renderable(renderable).wait();
ASSERT_EQ(Tracking_Color_Cache::next_instance_id.load(), 2);
Tracking_Color_Cache::watched_instance.store(2, std::memory_order_release);
scene.render();
gate->wait_until_arrived();
scene.detach_renderable(renderable);
auto detach = scene.detach_renderable(renderable);
auto attach = scene.attach_renderable(renderable);
EXPECT_FALSE(detach.ready());
EXPECT_FALSE(attach.ready());
EXPECT_FALSE(Tracking_Color_Cache::watched_destroyed.load(std::memory_order_acquire));
scene.attach_renderable(renderable);
ASSERT_EQ(Tracking_Color_Cache::next_instance_id.load(), 3);
EXPECT_EQ(Tracking_Color_Cache::next_instance_id.load(), 2);
gate->open();
scene.wait_for_render();
detach.wait();
attach.wait();
EXPECT_TRUE(wait_until([] {
return Tracking_Color_Cache::watched_destroyed.load(std::memory_order_acquire);
}));
ASSERT_EQ(Tracking_Color_Cache::next_instance_id.load(), 3);
scene.with_final_color_cache([](const Tracking_Color_Cache& cache) {
EXPECT_EQ(cache.samples, std::vector<int>({2}));
});
@@ -446,7 +444,7 @@ TEST(dynamic_renderable_lifecycle_test,
controller.join();
controls_done.store(true, std::memory_order_release);
renderer.join();
scene.wait_for_mutations();
EXPECT_EQ(failures.load(std::memory_order_acquire), 0);
EXPECT_EQ(violations.load(std::memory_order_acquire), 0);
EXPECT_EQ(scene.renderable_count(), 2u);
@@ -248,18 +248,21 @@ TEST(render_plan_execution_test, logical_node_ids_are_stable_and_retired_ids_are
const Render_Node_Id retired_chunk = first->graph.nodes[2].node_id;
renderable->set_chunk_count(2);
scene.wait_for_mutations();
const auto unchanged = renderable->graph_snapshot();
EXPECT_EQ(unchanged->graph.nodes[0].node_id, root);
EXPECT_EQ(unchanged->graph.nodes[1].node_id, first_chunk);
EXPECT_EQ(unchanged->graph.nodes[2].node_id, retired_chunk);
renderable->set_chunk_count(1);
scene.wait_for_mutations();
const auto reduced = renderable->graph_snapshot();
ASSERT_EQ(reduced->graph.nodes.size(), 2u);
EXPECT_EQ(reduced->graph.nodes[0].node_id, root);
EXPECT_EQ(reduced->graph.nodes[1].node_id, first_chunk);
renderable->set_chunk_count(2);
scene.wait_for_mutations();
const auto expanded = renderable->graph_snapshot();
ASSERT_EQ(expanded->graph.nodes.size(), 3u);
EXPECT_EQ(expanded->graph.nodes[0].node_id, root);
@@ -207,11 +207,11 @@ TEST(scene2d_context_test, failed_attach_leaves_renderable_fully_detached) {
Scene2D_Attach_Throwing_Cache::throw_on_construction = 2;
Scene2D_Context<Low_Latency_Strategy<Scene2D_Frame_Data>, Scene2D_Attach_Throwing_Cache> scene;
auto renderable = std::make_shared<Scene2D_Context_Test_Renderable>(scene);
EXPECT_THROW(scene.attach_renderable(renderable), std::runtime_error);
EXPECT_THROW(scene.attach_renderable(renderable).wait(), std::runtime_error);
EXPECT_EQ(scene.renderable_count(), 0);
EXPECT_TRUE(scene.topology_snapshot().renderables.empty());
Scene2D_Attach_Throwing_Cache::throw_on_construction = 0;
EXPECT_NO_THROW(scene.attach_renderable(renderable));
EXPECT_NO_THROW(scene.attach_renderable(renderable).wait());
EXPECT_EQ(scene.renderable_count(), 1);
}
struct Scene2D_Viewport_Snapshot_Renderable : Renderable_Base {
@@ -65,7 +65,7 @@ TEST(scene3d_context_test, detach_parent_promotes_children_and_invalidates_depen
scene.render();
scene.wait_for_render();
EXPECT_EQ(child->render_count, 1);
scene.detach_renderable(parent);
scene.detach_renderable(parent).wait();
const auto topology = scene.topology_snapshot();
bool display_checked{};
bool dependency_checked{};
@@ -144,7 +144,7 @@ TEST(scene_base_test, detach_releases_renderable_from_the_live_model) {
scene.attach_renderable(renderable);
scene.render();
scene.wait_for_render();
scene.detach_renderable(renderable);
scene.detach_renderable(renderable).wait();
renderable.reset();
EXPECT_TRUE(weak.expired());
EXPECT_EQ(scene.renderable_count(), 0);
@@ -158,11 +158,11 @@ TEST(scene_base_test, topology_rejects_unattached_renderables) {
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), std::invalid_argument);
EXPECT_THROW(scene.set_display_parent(child, parent), std::invalid_argument);
EXPECT_THROW(scene.set_dependency_parent(child, parent).wait(), std::invalid_argument);
EXPECT_THROW(scene.set_display_parent(child, parent).wait(), 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), std::invalid_argument);
EXPECT_THROW(scene.set_dependency_parent(detached_child, parent).wait(), std::invalid_argument);
}
TEST(scene_base_test, setting_same_topology_parent_is_a_noop) {
Scene2D_Context<> scene;
@@ -170,9 +170,9 @@ TEST(scene_base_test, setting_same_topology_parent_is_a_noop) {
auto child = std::make_shared<Scene_Base_Test_Renderable>(scene);
scene.attach_renderable(parent);
scene.attach_renderable(child);
scene.set_dependency_parent(child, parent);
scene.set_dependency_parent(child, parent).wait();
const auto revision = child->prepare_revision();
EXPECT_NO_THROW(scene.set_dependency_parent(child, parent));
EXPECT_NO_THROW(scene.set_dependency_parent(child, parent).wait());
EXPECT_EQ(child->prepare_revision(), revision);
const auto topology = scene.topology_snapshot();
EXPECT_EQ(topology.dependency.size(), 2);
@@ -195,7 +195,24 @@ TEST(scene_base_test, display_graph_rejects_cycles) {
scene.attach_renderable(third);
scene.set_display_parent(second, first);
scene.add_display_parent(third, second);
EXPECT_THROW(scene.add_display_parent(first, third), std::invalid_argument);
EXPECT_THROW(scene.add_display_parent(first, third).wait(), std::invalid_argument);
}
TEST(scene_base_test, mutation_callback_receives_apply_failure) {
Scene2D_Context<> scene;
auto first = std::make_shared<Scene_Base_Test_Renderable>(scene);
auto second = std::make_shared<Scene_Base_Test_Renderable>(scene);
scene.attach_renderable(first).wait();
scene.attach_renderable(second).wait();
scene.set_display_parent(second, first).wait();
std::atomic<bool> callback_called{};
std::atomic<bool> callback_failed{};
auto mutation = scene.set_display_parent(first, second, [&](std::exception_ptr exception) {
callback_failed.store(static_cast<bool>(exception), std::memory_order_release);
callback_called.store(true, std::memory_order_release);
});
EXPECT_THROW(mutation.wait(), std::invalid_argument);
EXPECT_TRUE(wait_until([&] { return callback_called.load(std::memory_order_acquire); }));
EXPECT_TRUE(callback_failed.load(std::memory_order_acquire));
}
TEST(scene_base_test, atomic_attach_can_place_existing_display_children) {
Scene2D_Context<> scene;
@@ -209,7 +226,7 @@ TEST(scene_base_test, atomic_attach_can_place_existing_display_children) {
.display_children = {axis},
.dependency_parents = {parent, axis}
});
scene.wait_for_mutations();
const auto topology = scene.topology_snapshot();
const auto contains = [](const auto& relationships, const auto& child,
const auto& parent_value) {
@@ -233,21 +250,21 @@ TEST(scene_base_test, failed_atomic_attach_rolls_back_every_topology) {
.display_parents = {parent},
.display_children = {parent},
.dependency_parents = {parent}
}), std::invalid_argument);
}).wait(), std::invalid_argument);
EXPECT_EQ(scene.renderable_count(), 1u);
EXPECT_NO_THROW(scene.attach_renderable(renderable, {
.display_parents = {parent},
.dependency_parents = {parent}
}));
}).wait());
EXPECT_EQ(scene.renderable_count(), 2u);
}
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);
scene.attach_renderable(renderable).wait();
const auto snapshot = scene.topology_snapshot();
scene.detach_renderable(renderable);
scene.detach_renderable(renderable).wait();
renderable.reset();
EXPECT_FALSE(weak.expired());
EXPECT_EQ(snapshot.renderables.size(), 1);
@@ -259,6 +276,7 @@ TEST(scene_base_test, topology_snapshot_is_safe_during_topology_updates) {
auto child = std::make_shared<Scene_Base_Test_Renderable>(scene);
scene.attach_renderable(parent);
scene.attach_renderable(child);
scene.wait_for_mutations();
std::thread writer([&] {
for (int index = 0; index < 500; ++index) {
if (index % 2 == 0) {
@@ -339,6 +357,25 @@ struct Scene_Base_Render_Execution_Renderable : Renderable_Base {
std::atomic<bool> nested_render_rejected{};
std::atomic<bool> mutation_completed{};
};
TEST(scene_base_test, attach_builder_is_synchronous_before_runtime) {
Scene2D_Context<> scene;
auto renderable = std::make_shared<Scene_Base_Test_Renderable>(scene);
{
auto builder = scene.attach_builder();
builder.attach_renderable(renderable);
EXPECT_EQ(scene.renderable_count(), 1u);
}
scene.render();
scene.wait_for_render();
EXPECT_EQ(renderable->render_count, 1);
}
TEST(scene_base_test, attach_builder_is_rejected_after_runtime_starts) {
Scene2D_Context<> scene;
auto renderable = std::make_shared<Scene_Base_Test_Renderable>(scene);
scene.attach_renderable(renderable).wait();
EXPECT_THROW(scene.attach_builder(), std::logic_error);
}
TEST(scene_base_test, taskflow_execution_can_mutate_live_model_but_cannot_submit_nested_render) {
Scene2D_Context<> scene;
auto renderable = std::make_shared<Scene_Base_Render_Execution_Renderable>(scene);
@@ -90,7 +90,7 @@ struct Threading_Test_Render_Graph_Renderable : Renderable_Base {
executed->fetch_add(1, std::memory_order_relaxed);
});
}
void request_graph_rebuild() noexcept {
void request_graph_rebuild() {
rebuild_render_graph();
}
std::atomic<int>* executed;
@@ -534,32 +534,29 @@ TEST(threading_contract_test, scene_serializes_multiple_render_submitters_withou
EXPECT_EQ(executed.load(std::memory_order_acquire), render_count);
EXPECT_EQ(maximum_sequence.load(std::memory_order_acquire), render_count);
}
TEST(threading_contract_test, scene_render_and_render_graph_rebuild_can_run_concurrently) {
TEST(threading_contract_test, scene_render_and_render_graph_rebuild_are_serialized_by_mutation_barrier) {
Scene2D_Context<> scene;
constexpr int render_count = 300;
std::atomic<int> executed{};
auto renderable = std::make_shared<Threading_Test_Render_Graph_Renderable>(scene, executed);
scene.attach_renderable(renderable);
scene.attach_renderable(renderable).wait();
std::atomic<bool> start{};
std::atomic<bool> renderer_done{};
std::thread renderer([&] {
wait_start(start);
for (int index = 0; index < render_count; ++index) {
scene.render();
scene.wait_for_render();
}
renderer_done.store(true, std::memory_order_release);
});
std::thread rebuilder([&] {
wait_start(start);
while (!renderer_done.load(std::memory_order_acquire)) {
for (int index = 0; index < render_count; ++index)
renderable->request_graph_rebuild();
std::this_thread::yield();
}
});
start.store(true, std::memory_order_release);
renderer.join();
rebuilder.join();
scene.wait_for_mutations();
EXPECT_EQ(executed.load(std::memory_order_acquire), render_count);
}
TEST(threading_contract_test, real_time_data_updates_can_race_with_scene_destruction) {