diff --git a/Core/architecture/Render_Data.h b/Core/architecture/Render_Data.h index 01f023e..2113553 100644 --- a/Core/architecture/Render_Data.h +++ b/Core/architecture/Render_Data.h @@ -324,6 +324,9 @@ struct Render_Data { virtual Rect pixel_bounds(const Size& plot_size) { return {0, 0, plot_size.width, plot_size.height}; } + [[nodiscard]] virtual bool requires_frame_view() const { + return false; + } protected: virtual void copy_render_state_object(Render_State& target, const Render_State& source) { target.version = source.version; diff --git a/Core/architecture/Renderable.cpp b/Core/architecture/Renderable.cpp index 2d8f631..301c0a5 100644 --- a/Core/architecture/Renderable.cpp +++ b/Core/architecture/Renderable.cpp @@ -188,7 +188,9 @@ void Renderable::append_tree_snapshot( node.cache_parent_node_id = cache_parent_node_id; node.cache_parent_object_name = cache_parent_object_name; node.cache_tree_depth = cache_tree_depth; - if (d_ptr && should_prepare()) + bool should_capture_frame_view = d_ptr && should_prepare(); + node.frame_view_required = should_capture_frame_view && d_ptr->requires_frame_view(); + if (should_capture_frame_view) node.frame_view = d_ptr->capture_frame_view(); snapshot.push_back(std::move(node)); std::string child_cache_parent = cache_parent_object_name; diff --git a/Core/architecture/Strategy_Render_Data.cpp b/Core/architecture/Strategy_Render_Data.cpp index 43f8582..53b6554 100644 --- a/Core/architecture/Strategy_Render_Data.cpp +++ b/Core/architecture/Strategy_Render_Data.cpp @@ -62,4 +62,8 @@ void Strategy_Render_Data::clear_render_input() { runtime->clear_render_input(); } +bool Strategy_Render_Data::requires_frame_view() const { + return static_cast(runtime); +} + } // namespace renderive diff --git a/Core/architecture/Strategy_Render_Data.h b/Core/architecture/Strategy_Render_Data.h index e88a7ca..92d2b25 100644 --- a/Core/architecture/Strategy_Render_Data.h +++ b/Core/architecture/Strategy_Render_Data.h @@ -27,6 +27,7 @@ struct LIB_DECL Strategy_Render_Data : Render_Data { Render_State* state(Renderable_State_Role role) override; Input_Data* input_data(Renderable_Input_Role role) override; void clear_render_input(); + [[nodiscard]] bool requires_frame_view() const override; private: std::unique_ptr runtime; diff --git a/Core/flow/explicit/Explicit_Frame_Flow.cpp b/Core/flow/explicit/Explicit_Frame_Flow.cpp index ec9de0a..2c877bc 100644 --- a/Core/flow/explicit/Explicit_Frame_Flow.cpp +++ b/Core/flow/explicit/Explicit_Frame_Flow.cpp @@ -84,6 +84,7 @@ Render_Frame_Surface Explicit_Frame_Flow::begin_render_frame() { if (render_job_active.exchange(true, std::memory_order_acq_rel)) return {}; auto output = std::make_shared(); + output->render_complete.store(false, std::memory_order_release); auto handle = std::make_unique(output); auto* output_pointer = output.get(); std::uint64_t input_version = edit_version.load(std::memory_order_acquire); @@ -107,6 +108,17 @@ Render_Surface_Publish_Result Explicit_Frame_Flow::publish_rendered_frame(Render render_job_active.store(false, std::memory_order_release); return result; } + if (!handle->output->render_complete.load(std::memory_order_acquire) + || !handle->output->metadata + || !handle->output->metadata->render_end_ns + || handle->output->image.empty()) { + if (handle->output->metadata) + handle->output->metadata->outcome = Frame_Outcome::Publish_Dropped; + result.dropped_frame_record = handle->output->metadata; + supersede_update_states(handle->output->presented_update_states); + render_job_active.store(false, std::memory_order_release); + return result; + } if (handle->output->metadata) { handle->output->metadata->transition_12_ns = now_ns; handle->output->metadata->ready_queue_wait_ns = now_ns > handle->output->metadata->render_end_ns ? now_ns - handle->output->metadata->render_end_ns : 0; diff --git a/Core/flow/low_latency/Low_Latency_Frame_Flow.cpp b/Core/flow/low_latency/Low_Latency_Frame_Flow.cpp index c2ae86b..3b7dfe8 100644 --- a/Core/flow/low_latency/Low_Latency_Frame_Flow.cpp +++ b/Core/flow/low_latency/Low_Latency_Frame_Flow.cpp @@ -162,6 +162,8 @@ Render_Frame_Surface Low_Latency_Frame_Flow::begin_render_frame() { d->pipeline.render_job_state = Frame_Job_State::Running; d->pipeline.rendered_edit_version = d->pipeline.edit_version.load(std::memory_order_acquire); auto* output = handle->lease.frame; + output->render_complete.store(false, std::memory_order_release); + output->image.clear(); std::uint64_t input_version = d->pipeline.rendered_edit_version; return Render_Frame_Surface(output, input_version, std::move(handle)); } @@ -288,4 +290,12 @@ void Low_Latency_Frame_Flow::shutdown() { d->host = nullptr; } +Low_Latency_Frame_Pipeline& Low_Latency_Frame_Flow::pipeline_for_test() { + return d->pipeline; +} + +const Low_Latency_Frame_Pipeline& Low_Latency_Frame_Flow::pipeline_for_test() const { + return d->pipeline; +} + } // namespace renderive diff --git a/Core/flow/low_latency/Low_Latency_Frame_Flow.h b/Core/flow/low_latency/Low_Latency_Frame_Flow.h index b734030..384d142 100644 --- a/Core/flow/low_latency/Low_Latency_Frame_Flow.h +++ b/Core/flow/low_latency/Low_Latency_Frame_Flow.h @@ -6,6 +6,9 @@ namespace renderive { +class Low_Latency_Frame_Pipeline; +struct Low_Latency_Frame_Flow_Test_Probe; + class Low_Latency_Frame_Flow final : public Frame_Flow { public: Low_Latency_Frame_Flow(); @@ -45,6 +48,10 @@ public: void shutdown() override; private: + friend struct Low_Latency_Frame_Flow_Test_Probe; + [[nodiscard]] Low_Latency_Frame_Pipeline& pipeline_for_test(); + [[nodiscard]] const Low_Latency_Frame_Pipeline& pipeline_for_test() const; + struct Private; std::unique_ptr d; }; diff --git a/Core/flow/low_latency/Low_Latency_Frame_Pipeline.cpp b/Core/flow/low_latency/Low_Latency_Frame_Pipeline.cpp index 0358f75..71593b8 100644 --- a/Core/flow/low_latency/Low_Latency_Frame_Pipeline.cpp +++ b/Core/flow/low_latency/Low_Latency_Frame_Pipeline.cpp @@ -8,7 +8,10 @@ namespace renderive { namespace { bool has_complete_frame(const Low_Latency_Frame* frame) { - return frame && frame->version.load(std::memory_order_acquire) != 0 && !frame->image.empty(); + return frame && frame->version.load(std::memory_order_acquire) != 0 + && frame->render_complete.load(std::memory_order_acquire) + && frame->metadata && frame->metadata->render_end_ns + && !frame->image.empty(); } } // namespace @@ -93,11 +96,20 @@ Low_Latency_Frame_Pipeline::Frame_Publish_Result Low_Latency_Frame_Pipeline::try if (!rendering_lease) return result; Low_Latency_Frame* rendered = rendering_lease.frame; + if (!has_complete_frame(rendered)) { + if (rendered->metadata) + rendered->metadata->outcome = Frame_Outcome::Publish_Dropped; + result.dropped_frame_record = rendered->metadata; + supersede_update_states(rendered->presented_update_states); + ++attempt_counters.publish_dropped_count; + return result; + } auto swap = frame_control.try_swap_role_with_left_lease(Frame_Rendering, Frame_Middle, rendering_lease.lease, [this](const Triple_Buffer_View& view) { const Low_Latency_Frame* rendering = frame_by_index(view[Frame_Rendering]); const Low_Latency_Frame* middle = frame_by_index(view[Frame_Middle]); - return buffer_version_newer(rendering->version.load(std::memory_order_acquire), middle->version.load(std::memory_order_acquire)); + return has_complete_frame(rendering) + && buffer_version_newer(rendering->version.load(std::memory_order_acquire), middle->version.load(std::memory_order_acquire)); }, [this, &result, now_ns](const Triple_Buffer_View& old_view, const Triple_Buffer_View& new_view) { Low_Latency_Frame* rendered = frame_by_index(old_view[Frame_Rendering]); Low_Latency_Frame* old_middle = frame_by_index(old_view[Frame_Middle]); @@ -127,8 +139,7 @@ Low_Latency_Frame_Pipeline::Frame_Publish_Result Low_Latency_Frame_Pipeline::try } rendering_lease.reset(); - if (has_complete_frame(rendered)) - has_published_frame.store(true, std::memory_order_release); + has_published_frame.store(true, std::memory_order_release); Frame_Publish_Result update_result = request_present(now_ns); update_result.superseded_frame_record = result.superseded_frame_record; update_result.dropped_frame_record = result.dropped_frame_record; @@ -137,25 +148,17 @@ Low_Latency_Frame_Pipeline::Frame_Publish_Result Low_Latency_Frame_Pipeline::try Low_Latency_Frame_Pipeline::Frame_Publish_Result Low_Latency_Frame_Pipeline::request_present(std::uint64_t now_ns) { Frame_Publish_Result result; - auto record = frame_control.try_acquire_role(Frame_Middle); - if (record.result != Triple_Buffer_Result::Success) + Triple_Buffer_View view = frame_control.read_view(); + const Low_Latency_Frame* middle = frame_by_index(view[Frame_Middle]); + const Low_Latency_Frame* painting = frame_by_index(view[Frame_Painting]); + if (!has_complete_frame(middle) + || !buffer_version_newer(middle->version.load(std::memory_order_acquire), painting->version.load(std::memory_order_acquire))) return result; - Low_Latency_Frame_Lease middle_lease{frame_by_index(record.lease.index), &frame_control, record.lease}; - Low_Latency_Frame* middle = middle_lease.frame; - const Low_Latency_Frame* painting = frame_by_role(Frame_Painting); - if (!buffer_version_newer(middle->version.load(std::memory_order_acquire), painting->version.load(std::memory_order_acquire))) + if (paint_request_pending.load(std::memory_order_acquire)) return result; - - if (paint_request_pending.load(std::memory_order_acquire)) { - middle->metadata->update_request_id = pending_present_request_id.load(std::memory_order_acquire); - middle->metadata->update_request_time_ns = pending_present_request_time_ns.load(std::memory_order_acquire); - return result; - } result.update_request_id = ++next_present_request_id; result.update_request_time = now_ns; - middle->metadata->update_request_id = result.update_request_id; - middle->metadata->update_request_time_ns = result.update_request_time; pending_present_request_id.store(result.update_request_id, std::memory_order_release); pending_present_request_time_ns.store(result.update_request_time, std::memory_order_release); paint_request_pending.store(true, std::memory_order_release); @@ -167,7 +170,7 @@ Low_Latency_Frame_Pipeline::Frame_Publish_Result Low_Latency_Frame_Pipeline::req Low_Latency_Frame_Pipeline::Frame_Consume_Result Low_Latency_Frame_Pipeline::acquire_paint_frame(std::uint64_t now_ns) { Frame_Consume_Result result; - bool pending = paint_request_pending.load(std::memory_order_acquire); + result.present_request_was_pending = paint_request_pending.exchange(false, std::memory_order_acq_rel); for (;;) { bool attempted_middle_swap = false; @@ -189,6 +192,11 @@ Low_Latency_Frame_Pipeline::Frame_Consume_Result Low_Latency_Frame_Pipeline::acq } new_painting->metadata->transition_23_ns = now_ns; new_painting->metadata->present_queue_wait_ns = now_ns > new_painting->metadata->transition_12_ns ? now_ns - new_painting->metadata->transition_12_ns : 0; + std::uint64_t request_id = pending_present_request_id.load(std::memory_order_acquire); + if (request_id) { + new_painting->metadata->update_request_id = request_id; + new_painting->metadata->update_request_time_ns = pending_present_request_time_ns.load(std::memory_order_acquire); + } }); middle_swap_result = swap.result; } @@ -199,8 +207,6 @@ Low_Latency_Frame_Pipeline::Frame_Consume_Result Low_Latency_Frame_Pipeline::acq result.has_valid_frame = has_complete_frame(result.lease.frame); if (result.has_valid_frame) { - if (result.has_new_frame && pending) - result.present_request_was_pending = paint_request_pending.exchange(false, std::memory_order_acq_rel); return result; } result.lease.reset(); @@ -230,7 +236,8 @@ Low_Latency_Frame_Pipeline::Frame_Consume_Result Low_Latency_Frame_Pipeline::acq bool Low_Latency_Frame_Pipeline::middle_frame_ready() const { const Low_Latency_Frame* middle = frame_by_role(Frame_Middle); const Low_Latency_Frame* painting = frame_by_role(Frame_Painting); - return buffer_version_newer(middle->version.load(std::memory_order_acquire), painting->version.load(std::memory_order_acquire)); + return has_complete_frame(middle) + && buffer_version_newer(middle->version.load(std::memory_order_acquire), painting->version.load(std::memory_order_acquire)); } bool Low_Latency_Frame_Pipeline::painting_frame_in_use() const { diff --git a/Core/plot/Plot_Core.cpp b/Core/plot/Plot_Core.cpp index 67fdf24..960e546 100644 --- a/Core/plot/Plot_Core.cpp +++ b/Core/plot/Plot_Core.cpp @@ -50,6 +50,7 @@ public: if (!frame_surface) return false; Render_Output* buffer = frame_surface.output; + buffer->render_complete.store(false, std::memory_order_release); cancel_update_states(buffer->presented_update_states); if (flow) flow->drain_frame_update_states(buffer->presented_update_states); @@ -87,6 +88,16 @@ public: previous_frame_time_ns = submit_time; job->renderables = renderable_tree_snapshot(); job->render_snapshot.frame_tree = &job->renderables; + if (!frame_tree_views_complete(job->renderables)) { + frame.outcome = Frame_Outcome::Cancelled; + if (flow) + flow->cancel_render_frame(std::move(job->frame_surface)); + supersede_update_states(buffer->presented_update_states); + release_frame_views(job); + collect_frame(frame_record); + notify_render_finished(frame); + return false; + } auto& scheduler = Global::instance()->render_scheduler(); bool submitted = scheduler.try_submit_render_task(Render_Executor_Task{ plot_execution_id, @@ -130,10 +141,22 @@ public: frame.worker.rejected_task_count++; if (flow) flow->discard_render_frame(std::move(job->frame_surface)); + release_frame_views(job); collect_frame(frame_record); notify_render_finished(frame); return false; } + static bool frame_tree_views_complete(const Renderable_Frame_Tree& tree) { + return std::all_of(tree.begin(), tree.end(), [](const Renderable_Frame_Node& node) { + return !node.frame_view_required || static_cast(node.frame_view); + }); + } + static void release_frame_views(const std::shared_ptr& job) { + if (!job) + return; + job->render_snapshot.frame_tree = nullptr; + job->renderables.clear(); + } static bool frame_job_valid(const std::shared_ptr& job) { return job && job->buffer && job->lifecycle && !job->render_snapshot.destroying(); } @@ -174,6 +197,7 @@ public: if (!frame_job_valid(job) || !job->lifecycle->prepare_end_ns) return; Frame_Lifecycle_Record& frame = *job->lifecycle; + job->buffer->render_complete.store(false, std::memory_order_release); job->buffer->image.resize(job->render_snapshot.size.width, job->render_snapshot.size.height); job->buffer->image.fill(job->render_snapshot.background_color); Canvas canvas(job->buffer->image); @@ -191,6 +215,7 @@ public: if (update_state) update_state->complete_until(Update_Stage::Rendered); } + job->buffer->render_complete.store(true, std::memory_order_release); } void execute_frame_job(const std::shared_ptr& job) { if (!job || !job->buffer || !job->lifecycle || job->render_snapshot.destroying()) @@ -210,15 +235,18 @@ public: frame.worker = *job->worker_stat; bool valid_generation = job->generation == generation.load(std::memory_order_acquire); bool valid_context = !destroying(); - if (!valid_generation || !valid_context || !frame.render_end_ns) { + bool render_complete = job->buffer->render_complete.load(std::memory_order_acquire); + if (!valid_generation || !valid_context || !frame.render_end_ns || !render_complete) { if (flow) flow->cancel_render_frame(std::move(job->frame_surface)); frame.outcome = Frame_Outcome::Cancelled; supersede_update_states(job->buffer->presented_update_states); + release_frame_views(job); notify_render_finished(frame); return; } auto publish_result = flow ? flow->publish_rendered_frame(std::move(job->frame_surface), steady_now_ns()) : Render_Surface_Publish_Result{}; + release_frame_views(job); if (publish_result.superseded_frame_record) { notify_frame_returned(publish_result.superseded_frame_record); } @@ -574,11 +602,6 @@ Present_Surface_Lease Plot_Core::begin_present() { data->notify_frame_returned(frame); }); } - if (!result.new_frame() && result.request_was_pending()) { - d->post_scheduler_action([data = d]() { - data->notify_paint_finished(); - }); - } return result; } void Plot_Core::end_present(Present_Surface_Lease&& frame, std::uint64_t paint_begin_ns, std::uint64_t paint_end_ns) { diff --git a/Core/render/Render_Output.h b/Core/render/Render_Output.h index 32beacf..4935b27 100644 --- a/Core/render/Render_Output.h +++ b/Core/render/Render_Output.h @@ -2,6 +2,7 @@ #include "../architecture/Frame_Scheduler.h" #include "../architecture/Update_Completion.h" #include "Image.h" +#include #include #include @@ -10,6 +11,7 @@ namespace renderive { struct Render_Output { Render_Output(); Image image; + std::atomic_bool render_complete{false}; std::pmr::vector> presented_update_states; std::shared_ptr metadata; }; diff --git a/Core/render/Renderable_Frame_Node.h b/Core/render/Renderable_Frame_Node.h index 676b24f..ae15d25 100644 --- a/Core/render/Renderable_Frame_Node.h +++ b/Core/render/Renderable_Frame_Node.h @@ -29,6 +29,7 @@ struct Renderable_Frame_Node { Renderable_Id cache_parent_node_id{}; std::string cache_parent_object_name; int cache_tree_depth{}; + bool frame_view_required{}; Renderable_Frame_View frame_view; }; diff --git a/test/Renderive_Core_Tests.cpp b/test/Renderive_Core_Tests.cpp index 77053ab..970355d 100644 --- a/test/Renderive_Core_Tests.cpp +++ b/test/Renderive_Core_Tests.cpp @@ -57,6 +57,12 @@ struct Low_Latency_Frame_Pipeline_Test_Probe { } }; +struct Low_Latency_Frame_Flow_Test_Probe { + static Low_Latency_Frame_Pipeline& pipeline(Low_Latency_Frame_Flow& flow) { + return flow.pipeline_for_test(); + } +}; + namespace { bool unique_roles(const Triple_Buffer_View& view) { std::array seen{}; @@ -102,9 +108,24 @@ Low_Latency_Frame_Pipeline::Frame_Publish_Result publish_pipeline_frame(Low_Late lease.frame->metadata->render_end_ns = now_ns >= 5 ? now_ns - 5 : now_ns; lease.frame->image.resize(2, 2); lease.frame->image.fill(Color::black()); + lease.frame->render_complete.store(true, std::memory_order_release); lease.frame->version.store(frame_id, std::memory_order_release); return pipeline.try_publish_rendered_frame(std::move(lease), now_ns); } +Render_Surface_Publish_Result publish_low_latency_flow_frame(Low_Latency_Frame_Flow& flow, std::uint64_t frame_id, std::uint64_t now_ns, int width = 2, int height = 2, Color color = Color::black()) { + auto surface = flow.begin_render_frame(); + EXPECT_TRUE(static_cast(surface)); + if (!surface) + return {}; + EXPECT_TRUE(surface.output && surface.output->metadata); + surface.output->metadata->frame_id = frame_id; + surface.output->metadata->render_begin_ns = now_ns >= 10 ? now_ns - 10 : now_ns; + surface.output->metadata->render_end_ns = now_ns >= 5 ? now_ns - 5 : now_ns; + surface.output->image.resize(width, height); + surface.output->image.fill(color); + surface.output->render_complete.store(true, std::memory_order_release); + return flow.publish_rendered_frame(std::move(surface), now_ns); +} Render_Surface_Publish_Result publish_explicit_frame(Explicit_Frame_Flow& flow, std::uint64_t frame_id, std::uint64_t now_ns, int width = 2, int height = 2, Color color = Color::green()) { auto surface = flow.begin_render_frame(); EXPECT_TRUE(static_cast(surface)); @@ -116,6 +137,7 @@ Render_Surface_Publish_Result publish_explicit_frame(Explicit_Frame_Flow& flow, surface.output->metadata->render_end_ns = now_ns >= 5 ? now_ns - 5 : now_ns; surface.output->image.resize(width, height); surface.output->image.fill(color); + surface.output->render_complete.store(true, std::memory_order_release); return flow.publish_rendered_frame(std::move(surface), now_ns); } Frame_Lifecycle_Record feedback_frame(std::uint64_t frame_id, std::uint64_t base_ns, std::uint64_t render_ns, std::uint64_t paint_ns, std::uint64_t ready_wait_ns = 0, std::uint64_t gui_wait_ns = 0) { @@ -232,6 +254,16 @@ struct Runtime_Count_Owner : Renderable { } }; struct Runtime_Count_Data : Typed_Render_Data {}; +struct Frame_View_Blocking_Renderable; +struct Frame_View_Blocking_Data : Typed_Render_Data {}; +struct Frame_View_Blocking_Renderable : Renderable { + Frame_View_Blocking_Renderable(); + Frame_View_Blocking_Data* d(); + Render_Object_Owner create_render_data() override; + Render_Object_Owner create_render_state() override; + Render_Object_Owner create_input_data() override; +}; +RENDERIVE_DEFINE_RENDERABLE_BINDING(Frame_View_Blocking_Renderable, Frame_View_Blocking_Data, Lease_Test_State, Lease_Test_Input, "Frame_View_Blocking") struct Counting_Test_Input : Input_Data { int clear_count{}; void clear() override { @@ -281,6 +313,28 @@ struct Flow_Test_Host : Frame_Flow_Host { ++deadline_cancels; } }; + +struct Low_Latency_Present_Test_Host : Frame_Flow_Host { + Low_Latency_Frame_Flow* flow{}; + std::uint64_t request_now_ns{}; + int paint_requests{}; + Render_Surface_Publish_Result last_present_request; + + Frame_Flow_Runtime_Snapshot runtime_snapshot(std::uint64_t now_ns) const override { + return flow ? flow->runtime_snapshot(now_ns, false) : Frame_Flow_Runtime_Snapshot{false, false, false, false, false, now_ns}; + } + bool try_render_once() override { + return false; + } + void request_paint_from_middle() override { + ++paint_requests; + if (flow) + last_present_request = flow->request_present(request_now_ns); + } + void schedule_render_deadline(std::uint64_t) override {} + void cancel_render_deadline() override {} +}; + } // namespace TEST(Renderive_Canvas, TextUsesPenAndFillUsesBrush) { auto diagnostics = Canvas::default_font_diagnostics(); @@ -758,11 +812,140 @@ TEST(Renderive_Frame_Pipeline, PresentDuringMiddleBusyRepeatsPainting) { EXPECT_FALSE(repeated.has_new_frame); EXPECT_TRUE(repeated.has_valid_frame); EXPECT_FALSE(repeated.underrun); - EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + EXPECT_TRUE(repeated.present_request_was_pending); + EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); repeated.lease.reset(); Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); } +TEST(Renderive_Frame_Pipeline, MiddleBusyPaintAcknowledgesPostedUpdate) { + Low_Latency_Frame_Pipeline pipeline; + ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); + auto first = pipeline.acquire_paint_frame(30); + ASSERT_TRUE(static_cast(first.lease)); + first.lease.reset(); + ASSERT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + + ASSERT_TRUE(publish_pipeline_frame(pipeline, 2, 40).request_present); + ASSERT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + + auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); + ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); + auto repeated = pipeline.acquire_paint_frame(45); + + EXPECT_TRUE(static_cast(repeated.lease)); + EXPECT_FALSE(repeated.has_new_frame); + EXPECT_TRUE(repeated.has_valid_frame); + EXPECT_TRUE(repeated.present_request_was_pending); + EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + + repeated.lease.reset(); + Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); +} + +TEST(Renderive_Frame_Pipeline, MiddleBusyPaintRepostsAfterPaintFinished) { + Low_Latency_Frame_Flow flow; + Low_Latency_Present_Test_Host host; + host.flow = &flow; + flow.attach(host); + flow.activate_view(20); + ASSERT_TRUE(publish_low_latency_flow_frame(flow, 1, 25).request_present); + std::shared_ptr returned; + auto first = flow.begin_present(30, returned); + ASSERT_TRUE(static_cast(first)); + EXPECT_TRUE(first.new_frame()); + (void)flow.end_present(std::move(first), 31, 32); + + ASSERT_TRUE(publish_low_latency_flow_frame(flow, 2, 40).request_present); + Low_Latency_Frame_Pipeline& pipeline = Low_Latency_Frame_Flow_Test_Probe::pipeline(flow); + auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); + ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); + auto repeated = flow.begin_present(45, returned); + ASSERT_TRUE(static_cast(repeated)); + EXPECT_FALSE(repeated.new_frame()); + EXPECT_TRUE(repeated.request_was_pending()); + (void)flow.end_present(std::move(repeated), 46, 47); + + host.request_now_ns = 50; + flow.notify_paint_finished(50); + EXPECT_EQ(host.paint_requests, 1); + EXPECT_TRUE(host.last_present_request.request_present); + EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); + + auto recovered = flow.begin_present(55, returned); + EXPECT_TRUE(static_cast(recovered)); + EXPECT_TRUE(recovered.new_frame()); + EXPECT_TRUE(recovered.request_was_pending()); + EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); +} + +TEST(Renderive_Frame_Pipeline, LostPresentWakeupDoesNotFreezeFlow) { + Low_Latency_Frame_Flow flow; + Low_Latency_Present_Test_Host host; + host.flow = &flow; + flow.attach(host); + flow.activate_view(20); + ASSERT_TRUE(publish_low_latency_flow_frame(flow, 1, 25).request_present); + std::shared_ptr returned; + auto first = flow.begin_present(30, returned); + ASSERT_TRUE(static_cast(first)); + (void)flow.end_present(std::move(first), 31, 32); + + ASSERT_TRUE(publish_low_latency_flow_frame(flow, 2, 40).request_present); + Low_Latency_Frame_Pipeline& pipeline = Low_Latency_Frame_Flow_Test_Probe::pipeline(flow); + auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); + ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); + auto repeated = flow.begin_present(45, returned); + ASSERT_TRUE(static_cast(repeated)); + EXPECT_FALSE(repeated.new_frame()); + EXPECT_TRUE(repeated.request_was_pending()); + EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + (void)flow.end_present(std::move(repeated), 46, 47); + + EXPECT_TRUE(pipeline.middle_frame_ready()); + host.request_now_ns = 50; + flow.notify_paint_finished(50); + ASSERT_TRUE(host.last_present_request.request_present); + Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); + + auto consumed = flow.begin_present(55, returned); + ASSERT_TRUE(static_cast(consumed)); + EXPECT_TRUE(consumed.new_frame()); + EXPECT_TRUE(consumed.request_was_pending()); + EXPECT_FALSE(pipeline.middle_frame_ready()); +} + +TEST(Renderive_Frame_Pipeline, HoverUpdatesDoNotRequireExternalRepaint) { + Low_Latency_Frame_Pipeline pipeline; + ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); + auto first = pipeline.acquire_paint_frame(30); + ASSERT_TRUE(static_cast(first.lease)); + first.lease.reset(); + + ASSERT_TRUE(publish_pipeline_frame(pipeline, 2, 40).request_present); + auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); + ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); + auto repeated = pipeline.acquire_paint_frame(45); + ASSERT_TRUE(static_cast(repeated.lease)); + EXPECT_TRUE(repeated.present_request_was_pending); + EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + repeated.lease.reset(); + Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); + + auto hover_publish = publish_pipeline_frame(pipeline, 3, 60); + ASSERT_TRUE(hover_publish.request_present); + EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + + auto consumed = pipeline.acquire_paint_frame(65); + ASSERT_TRUE(static_cast(consumed.lease)); + EXPECT_TRUE(consumed.has_new_frame); + EXPECT_TRUE(consumed.has_valid_frame); + EXPECT_TRUE(consumed.present_request_was_pending); + EXPECT_EQ(consumed.lease.frame->version.load(std::memory_order_acquire), 3u); + EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); +} + TEST(Renderive_Frame_Pipeline, PaintingLeaseBlocksUntilAvailable) { Low_Latency_Frame_Pipeline pipeline; ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); @@ -902,6 +1085,35 @@ TEST(Renderive_Frame_Pipeline, PublishFailureKeepsPreviousPainting) { EXPECT_FALSE(repeated.lease.frame->image.empty()); } +TEST(Renderive_Frame_Pipeline, IncompleteFrameDoesNotReplacePainting) { + Low_Latency_Frame_Pipeline pipeline; + ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); + auto first = pipeline.acquire_paint_frame(30); + ASSERT_TRUE(static_cast(first.lease)); + Low_Latency_Frame* first_frame = first.lease.frame; + auto first_version = first_frame->version.load(std::memory_order_acquire); + first.lease.reset(); + + auto render_lease = pipeline.try_acquire_render_frame(); + ASSERT_TRUE(static_cast(render_lease)); + render_lease.frame->metadata->frame_id = 2; + render_lease.frame->metadata->render_begin_ns = 35; + render_lease.frame->metadata->render_end_ns = 38; + render_lease.frame->image.resize(2, 2); + render_lease.frame->image.fill(Color::red()); + render_lease.frame->render_complete.store(false, std::memory_order_release); + render_lease.frame->version.store(2, std::memory_order_release); + auto failed_publish = pipeline.try_publish_rendered_frame(std::move(render_lease), 40); + EXPECT_FALSE(failed_publish.request_present); + EXPECT_NE(failed_publish.dropped_frame_record, nullptr); + + auto repeated = pipeline.acquire_paint_frame(45); + ASSERT_TRUE(static_cast(repeated.lease)); + EXPECT_FALSE(repeated.has_new_frame); + EXPECT_EQ(repeated.lease.frame, first_frame); + EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); +} + TEST(Renderive_Frame_Pipeline, RepeatedPresentNeverReturnsEmpty) { Low_Latency_Frame_Pipeline pipeline; ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); @@ -919,17 +1131,24 @@ TEST(Renderive_Frame_Pipeline, RepeatedPresentNeverReturnsEmpty) { } } -TEST(Renderive_Frame_Pipeline, RequestPresentDoesNotWriteBusyMiddle) { +TEST(Renderive_Frame_Pipeline, RequestPresentDoesNotLeaseBusyMiddle) { Low_Latency_Frame_Pipeline pipeline; auto published = publish_pipeline_frame(pipeline, 1, 25); ASSERT_TRUE(published.request_present); Low_Latency_Frame_Pipeline_Test_Probe::clear_paint_request(pipeline); + Low_Latency_Frame* middle = pipeline.frame_by_role(Frame_Middle); + ASSERT_TRUE(middle); + ASSERT_TRUE(middle->metadata); + middle->metadata->update_request_id = 0; + middle->metadata->update_request_time_ns = 0; auto busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); - auto blocked = pipeline.request_present(40); - EXPECT_FALSE(blocked.request_present); - EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + auto posted = pipeline.request_present(40); + EXPECT_TRUE(posted.request_present); + EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); + EXPECT_EQ(middle->metadata->update_request_id, 0u); + EXPECT_EQ(middle->metadata->update_request_time_ns, 0u); Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); } TEST(Renderive_Render_Model, NeutralSnapshotAndOutputTypesAreWired) { @@ -1197,6 +1416,33 @@ TEST(Renderive_Frame_Flow, ExplicitTicketsDrainIntoAcceptedFrame) { EXPECT_TRUE(ticket.is_complete()); } +TEST(Renderive_Plot_Core, FrameViewCaptureFailureMarksTreeIncomplete) { + Plot_Core plot(std::make_unique()); + plot.init(); + plot.set_viewport_size(Size{16, 16}); + auto root = plot.root_renderable(); + ASSERT_TRUE(root); + auto blocker = std::make_shared(); + blocker->init(root); + auto held_view = blocker->d()->capture_frame_view(); + ASSERT_TRUE(static_cast(held_view)); + auto blocked_view = blocker->d()->capture_frame_view(); + ASSERT_FALSE(static_cast(blocked_view)); + + Renderable_Frame_Tree blocked_tree; + root->append_tree_snapshot(blocked_tree); + EXPECT_TRUE(std::any_of(blocked_tree.begin(), blocked_tree.end(), [](const Renderable_Frame_Node& node) { + return node.frame_view_required && !node.frame_view; + })); + + held_view = {}; + Renderable_Frame_Tree recovered_tree; + root->append_tree_snapshot(recovered_tree); + EXPECT_FALSE(std::any_of(recovered_tree.begin(), recovered_tree.end(), [](const Renderable_Frame_Node& node) { + return node.frame_view_required && !node.frame_view; + })); +} + TEST(Renderive_Explicit_Present, BeforeFirstRenderReturnsEmpty) { Explicit_Frame_Flow flow; std::shared_ptr returned; @@ -1266,6 +1512,35 @@ TEST(Renderive_Explicit_Present, FailedRenderKeepsPreviousOutput) { (void)flow.end_present(std::move(repeated), 41, 42); } +TEST(Renderive_Explicit_Present, IncompleteFrameDoesNotReplaceOutput) { + Explicit_Frame_Flow flow; + ASSERT_TRUE(publish_explicit_frame(flow, 1, 25).request_present); + std::shared_ptr returned; + auto first = flow.begin_present(30, returned); + ASSERT_TRUE(static_cast(first)); + Render_Output* first_output = Present_Surface_Lease_Access::output(first); + (void)flow.end_present(std::move(first), 31, 32); + + auto incomplete = flow.begin_render_frame(); + ASSERT_TRUE(static_cast(incomplete)); + incomplete.output->metadata->frame_id = 2; + incomplete.output->metadata->render_begin_ns = 35; + incomplete.output->metadata->render_end_ns = 38; + incomplete.output->image.resize(3, 3); + incomplete.output->image.fill(Color::red()); + incomplete.output->render_complete.store(false, std::memory_order_release); + auto failed_publish = flow.publish_rendered_frame(std::move(incomplete), 40); + EXPECT_FALSE(failed_publish.request_present); + EXPECT_NE(failed_publish.dropped_frame_record, nullptr); + + auto repeated = flow.begin_present(45, returned); + ASSERT_TRUE(static_cast(repeated)); + EXPECT_EQ(Present_Surface_Lease_Access::output(repeated), first_output); + EXPECT_FALSE(repeated.new_frame()); + EXPECT_FALSE(repeated.image().empty()); + (void)flow.end_present(std::move(repeated), 46, 47); +} + TEST(Renderive_Explicit_Present, NewOutputAtomicallyReplacesPreviousOutput) { Explicit_Frame_Flow flow; ASSERT_TRUE(publish_explicit_frame(flow, 1, 25).request_present);