修改成异步的接口
This commit is contained in:
@@ -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_;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user