#include "Core/architecture/Frame_Scheduler.h" #include "Core/architecture/Render_Lease.h" #include "Core/Axis/Axis_p.h" #include "Core/flow/low_latency/Low_Latency_Roles.h" #include "Core/flow/low_latency/Triple_Buffer.h" #include "Core/flow/explicit/Explicit_Frame_Flow.h" #include "Core/flow/explicit/Explicit_Renderable_Runtime.h" #include "Core/flow/low_latency/Low_Latency_Feedback_Controller.h" #include "Core/flow/low_latency/Low_Latency_Frame_Flow.h" #include "Core/flow/low_latency/Low_Latency_Frame_Pipeline.h" #include "Core/flow/low_latency/Low_Latency_Renderable_Runtime.h" #include "Core/flow/Present_Surface_Lease.h" #include "Core/plot/Plot_Core.h" #include "Core/execution/Render_Executor.h" #include "Core/primitive/Curve_Utils_p.h" #include "Core/plottable/Frequency_Trace_p.h" #include "Core/plottable/Performance_Overlay_p.h" #include "Core/plottable/export.h" #include "Core/render/Image.h" #include "Core/render/Render_Frame_Snapshot.h" #include "Core/render/Render_Output.h" #include "Core/render/Renderable_Frame_Node.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace renderive { struct Low_Latency_Frame_Pipeline_Test_Probe { static bool paint_request_pending(const Low_Latency_Frame_Pipeline& pipeline) { return pipeline.paint_request_pending.load(std::memory_order_acquire); } static void clear_paint_request(Low_Latency_Frame_Pipeline& pipeline) { pipeline.paint_request_pending.store(false, std::memory_order_release); } static Triple_Buffer_Mark_Record acquire_frame_role(Low_Latency_Frame_Pipeline& pipeline, std::uint8_t role) { return pipeline.frame_control.try_acquire_role(role); } static void release_frame_role(Low_Latency_Frame_Pipeline& pipeline, Triple_Buffer_Lease lease) { pipeline.frame_control.unmark_use(lease); } static bool has_published_frame(const Low_Latency_Frame_Pipeline& pipeline) { return pipeline.has_published_frame.load(std::memory_order_acquire); } }; 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{}; for (std::uint8_t index : view.index) { if (index >= seen.size() || seen[index]) return false; seen[index] = true; } return true; } Frame_Lifecycle_Record render_frame(std::uint64_t begin_ns, std::uint64_t end_ns) { Frame_Lifecycle_Record frame; frame.render_role_enter_ns = begin_ns; frame.render_begin_ns = begin_ns; frame.render_end_ns = end_ns; return frame; } Timeline_Stream_Snapshot timeline_for_ticks(int first_tick, int last_tick, int visible_count = 0) { Timeline_Stream_Snapshot timeline; int count = std::max(0, last_tick - first_tick + 1); timeline.visible_time_point_count = visible_count > 0 ? visible_count : std::max(1, count); timeline.first_stream_tick = first_tick; timeline.last_stream_tick = last_tick; timeline.coordinate_begin = first_tick; timeline.times.resize(static_cast(count)); return timeline; } void prepare_frequency_trace(Frequency_Trace_Private& trace, std::initializer_list ticks, const Timeline_Stream_Snapshot& timeline) { trace.resize_power_buffer(static_cast(ticks.size())); for (int tick : ticks) trace.push_power(Frequency_Sample{tick, static_cast(tick)}); trace.snapshot_power_buffer(); trace.rebuild_prepared_points(timeline); } Low_Latency_Frame_Pipeline::Frame_Publish_Result publish_pipeline_frame(Low_Latency_Frame_Pipeline& pipeline, std::uint64_t frame_id, std::uint64_t now_ns) { auto lease = pipeline.try_acquire_render_frame(); EXPECT_TRUE(static_cast(lease)); if (!lease) return {}; EXPECT_TRUE(lease.frame->metadata); lease.frame->metadata->frame_id = frame_id; lease.frame->metadata->render_begin_ns = now_ns >= 10 ? now_ns - 10 : now_ns; 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)); 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); } 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) { Frame_Lifecycle_Record frame; frame.frame_id = frame_id; frame.core_data_time_ns = base_ns; frame.render_role_enter_ns = base_ns; frame.render_begin_ns = base_ns; frame.render_end_ns = base_ns + render_ns; frame.ready_queue_wait_ns = ready_wait_ns; frame.transition_12_ns = frame.render_end_ns + ready_wait_ns; frame.update_request_time_ns = frame.transition_12_ns; frame.update_posted_ns = frame.update_request_time_ns; frame.paint_begin_ns = frame.transition_12_ns + gui_wait_ns; frame.paint_end_ns = frame.paint_begin_ns + paint_ns; return frame; } void feed_feedback_frame(Low_Latency_Feedback_Controller& controller, 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) { auto frame = feedback_frame(frame_id, base_ns, render_ns, paint_ns, ready_wait_ns, gui_wait_ns); controller.on_render_completed(frame); controller.on_frame_presented(frame); } void feed_backlogged_feedback_frame(Low_Latency_Feedback_Controller& controller, 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) { auto frame = feedback_frame(frame_id, base_ns, render_ns, paint_ns, ready_wait_ns, gui_wait_ns); controller.note_middle_published(frame.transition_12_ns); controller.on_render_completed(frame); controller.on_frame_presented(frame); } bool wait_until(std::function predicate) { auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (std::chrono::steady_clock::now() < deadline) { if (predicate()) return true; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return predicate(); } std::shared_ptr performance_frame(std::uint64_t frame_id, int width = 240, int height = 80) { auto frame = std::make_shared(); frame->frame_id = frame_id; frame->input_version = frame_id + 1; frame->render_begin_ns = 10; frame->prepare_begin_ns = 11; frame->prepare_end_ns = 12; frame->draw_begin_ns = 13; frame->draw_end_ns = 14; frame->render_end_ns = 20; frame->transition_12_ns = 25; frame->update_request_time_ns = 26; frame->update_posted_ns = 26; frame->paint_begin_ns = 30; frame->paint_end_ns = 40; frame->presented_frame_id = frame_id; frame->transition_32_ns = 45; frame->pixel_width = static_cast(width); frame->pixel_height = static_cast(height); frame->viewport_version = 1; frame->outcome = Frame_Outcome::Presented; frame->worker.executor_at_finish.worker_count = 4; frame->worker.executor_at_finish.active_workers = 1; frame->worker.executor_at_finish.completed_task_nodes = frame_id; return frame; } bool valid_utf8(const std::string& text) { for (std::size_t i = 0; i < text.size();) { unsigned char c = static_cast(text[i]); std::size_t n = 0; if ((c & 0x80u) == 0) n = 1; else if ((c & 0xE0u) == 0xC0u) n = 2; else if ((c & 0xF0u) == 0xE0u) n = 3; else if ((c & 0xF8u) == 0xF0u) n = 4; else return false; if (i + n > text.size()) return false; for (std::size_t j = 1; j < n; ++j) { if ((static_cast(text[i + j]) & 0xC0u) != 0x80u) return false; } i += n; } return true; } std::filesystem::path unique_test_directory(std::string_view name) { auto stamp = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count(); std::filesystem::path path = std::filesystem::temp_directory_path() / ("renderive_" + std::string(name) + "_" + std::to_string(stamp)); std::filesystem::remove_all(path); std::filesystem::create_directories(path); return path; } std::vector read_lines(const std::filesystem::path& path) { std::ifstream input(path); std::vector lines; std::string line; while (std::getline(input, line)) lines.push_back(line); return lines; } using Parsed_Log_Record = std::map; std::vector read_log_records(const std::filesystem::path& path, std::string_view kind) { std::vector records; Parsed_Log_Record current; bool in_record = false; std::string begin = "[" + std::string(kind) + "]"; std::string end = "[/" + std::string(kind) + "]"; for (const auto& line : read_lines(path)) { if (line == begin) { current.clear(); in_record = true; continue; } if (line == end) { if (in_record) records.push_back(std::move(current)); current = {}; in_record = false; continue; } if (!in_record) continue; std::size_t separator = line.find('='); if (separator != std::string::npos) current.emplace(line.substr(0, separator), line.substr(separator + 1)); } return records; } Frame_Feedback_Event_Record feedback_event(std::string event, const Frame_Lifecycle_Record& frame, std::uint64_t timestamp_ns) { Frame_Feedback_Event_Record record; record.event = std::move(event); record.frame = frame; record.timestamp_ns = timestamp_ns; record.mode_before = frame.refresh.production_mode; record.mode_after = frame.refresh.production_mode; record.effective_interval_before_ns = frame.refresh.effective_min_render_interval_ns; record.effective_interval_after_ns = frame.refresh.effective_min_render_interval_ns; record.dirty = frame.dirty_reason_mask != 0; record.middle_has_new_frame = frame.refresh.middle_published_count > 0; record.paint_update_posted = frame.update_posted_ns != 0; record.backlog_epoch_active = frame.refresh.consumer_window_size > 0; record.continuously_backlogged = frame.refresh.last_consumer_sample_result == Consumer_Sample_Result::Accepted; record.producer_starved = frame.refresh.producer_starvation_active; record.backlog_epoch_id = frame.refresh.producer_starvation_epoch_id; record.flush = true; return record; } struct Lease_Test_State : Render_State { int value{}; }; struct Lease_Test_Input : Input_Data {}; struct Lease_Test_Owner : Renderable { Render_Object_Owner create_render_state() override { return make_render_object(); } Render_Object_Owner create_input_data() override { return make_render_object(); } }; struct Lease_Test_Data : Typed_Render_Data { Lease_Test_Owner runtime_owner; void initialize_low_latency() { q_ptr = &runtime_owner; initialize_runtime(make_low_latency_renderable_runtime(runtime_owner)); } void initialize_explicit() { q_ptr = &runtime_owner; initialize_runtime(make_explicit_renderable_runtime(runtime_owner)); } }; struct No_Input_Test_Owner : Renderable {}; struct No_Input_Test_Data : Typed_Render_Data {}; struct Runtime_Count_Owner : Renderable { int state_create_count{}; int input_create_count{}; Render_Object_Owner create_render_state() override { ++state_create_count; return make_render_object(); } Render_Object_Owner create_input_data() override { ++input_create_count; return make_render_object(); } }; 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 { ++clear_count; } }; struct Counting_Input_Owner : Renderable { Render_Object_Owner create_render_state() override { return make_render_object(); } Render_Object_Owner create_input_data() override { return make_render_object(); } }; struct Counting_Input_Data : Typed_Render_Data {}; void init_lease_test_state(Lease_Test_Data& data) { data.initialize_low_latency(); } struct Flow_Test_Host : Frame_Flow_Host { Frame_Flow_Runtime_Snapshot snapshot; int render_attempts{}; int paint_requests{}; int deadline_requests{}; int deadline_cancels{}; bool render_result = true; std::vector feedback_events; Frame_Flow_Runtime_Snapshot runtime_snapshot(std::uint64_t now_ns) const override { auto result = snapshot; result.now_ns = now_ns; return result; } bool try_render_once() override { ++render_attempts; snapshot.render_job_active = true; return render_result; } void request_paint_from_middle() override { ++paint_requests; snapshot.middle_has_new_frame = false; } void schedule_render_deadline(std::uint64_t) override { ++deadline_requests; } void cancel_render_deadline() override { ++deadline_cancels; } void record_feedback_event(std::shared_ptr event) override { if (event) feedback_events.push_back(event->event); } }; 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(); ASSERT_TRUE(diagnostics.face_valid); ASSERT_TRUE(diagnostics.font_valid); ASSERT_GT(diagnostics.test_width, 0.0); ASSERT_GT(diagnostics.test_height, 0.0); Image image(400, 100); image.fill(Color::transparent()); { Canvas canvas(image); canvas.set_brush(Brush{Color::white(), Brush_Style::Solid}); canvas.set_pen(Pen{Color::black()}); canvas.fill_rect(RectF{0.0, 0.0, 400.0, 100.0}); canvas.draw_text(PointF{10.0, 10.0}, "0123456789 ABC \xE4\xB8\xAD\xE6\x96\x87"); canvas.draw_text(RectF{10.0, 50.0, 300.0, 30.0}, Text_Align::Left | Text_Align::Top, "0123"); } EXPECT_EQ(premultiplied_to_color(image.row(2)[2]), Color::white()); int dark_pixels = 0; for (int y = 8; y < 82; ++y) { const Pixel* row = image.row(y); ASSERT_NE(row, nullptr); for (int x = 8; x < 320; ++x) { Color color = premultiplied_to_color(row[x]); if (color.a && color.r < 120 && color.g < 120 && color.b < 120) ++dark_pixels; } } EXPECT_GT(dark_pixels, 20); } TEST(Renderive_Triple_Buffer, RoleSwapsAreTryOnlyAndDeterministic) { Triple_Role_Buffer_Control control; control.reset(); EXPECT_TRUE(unique_roles(control.read_view())); auto first = control.try_swap_role(Frame_Rendering, Frame_Middle, [](const Triple_Buffer_View&) { return true; }); EXPECT_EQ(first.result, Triple_Buffer_Result::Success); EXPECT_TRUE(unique_roles(control.read_view())); auto painting = control.try_acquire_role(Frame_Painting); ASSERT_EQ(painting.result, Triple_Buffer_Result::Success); auto blocked = control.try_swap_role(Frame_Middle, Frame_Painting, [](const Triple_Buffer_View&) { return true; }); EXPECT_EQ(blocked.result, Triple_Buffer_Result::Busy); control.unmark_use(painting.lease); for (int i = 0; i < 1000000; ++i) { auto a = control.try_swap_role(Frame_Rendering, Frame_Middle, [](const Triple_Buffer_View&) { return true; }); ASSERT_EQ(a.result, Triple_Buffer_Result::Success); auto b = control.try_swap_role(Frame_Middle, Frame_Painting, [](const Triple_Buffer_View&) { return true; }); ASSERT_EQ(b.result, Triple_Buffer_Result::Success); ASSERT_TRUE(unique_roles(control.read_view())); } } TEST(Renderive_State, CloseWakesBlockedAcquire) { Triple_Role_Buffer_Control control; control.reset(); auto held = control.acquire_role(State_Edit); ASSERT_EQ(held.result, Triple_Buffer_Result::Success); std::atomic_bool entered{false}; Triple_Buffer_Mark_Record blocked_result; std::thread blocked([&]() { entered.store(true, std::memory_order_release); blocked_result = control.acquire_role(State_Edit); }); ASSERT_TRUE(wait_until([&]() { return entered.load(std::memory_order_acquire); })); std::this_thread::sleep_for(std::chrono::milliseconds(10)); control.close(); blocked.join(); EXPECT_EQ(blocked_result.result, Triple_Buffer_Result::Cancelled); control.unmark_use(held.lease); } TEST(Renderive_State, CloseDoesNotReleaseActiveLease) { Triple_Role_Buffer_Control control; control.reset(); auto held = control.acquire_role(State_Edit); ASSERT_EQ(held.result, Triple_Buffer_Result::Success); EXPECT_TRUE(control.role_busy(State_Edit)); control.close(); EXPECT_TRUE(control.role_busy(State_Edit)); auto blocked = control.try_acquire_role(State_Edit); EXPECT_EQ(blocked.result, Triple_Buffer_Result::Cancelled); control.unmark_use(held.lease); control.wait_until_idle(); EXPECT_FALSE(control.role_busy(State_Edit)); } TEST(Renderive_State, ReinitializeWaitsForActiveLease) { Lease_Test_Data data; init_lease_test_state(data); auto held = data.acquire_edit_state_read(); ASSERT_TRUE(static_cast(held)); std::atomic_bool reinitialize_returned{false}; std::latch reinitialize_started(1); Lease_Test_Owner explicit_owner; std::thread reinitializer([&]() { reinitialize_started.count_down(); data.initialize_runtime(make_explicit_renderable_runtime(explicit_owner)); reinitialize_returned.store(true, std::memory_order_release); }); reinitialize_started.wait(); std::this_thread::sleep_for(std::chrono::milliseconds(20)); EXPECT_FALSE(reinitialize_returned.load(std::memory_order_acquire)); held = {}; reinitializer.join(); EXPECT_TRUE(reinitialize_returned.load(std::memory_order_acquire)); EXPECT_NE(data.edit_state(), nullptr); } TEST(Renderive_State, BusyEditSetterWaitsAndCommits) { Lease_Test_Data data; init_lease_test_state(data); auto held = data.acquire_edit_state_read(); ASSERT_TRUE(static_cast(held)); std::atomic_bool setter_returned{false}; std::thread setter([&]() { data.set_state_value(&Lease_Test_State::value, 42); setter_returned.store(true, std::memory_order_release); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); EXPECT_FALSE(setter_returned.load(std::memory_order_acquire)); held = {}; setter.join(); EXPECT_TRUE(setter_returned.load(std::memory_order_acquire)); EXPECT_EQ(data.edit_state_value(&Lease_Test_State::value), 42); } TEST(Renderive_State, GetterWaitsForWriter) { Lease_Test_Data data; init_lease_test_state(data); auto held = data.acquire_edit_state(); ASSERT_TRUE(static_cast(held)); auto* state = static_cast(held.state); ASSERT_NE(state, nullptr); state->value = 77; std::atomic_bool getter_started{false}; std::atomic_bool getter_returned{false}; int observed = -1; std::thread getter([&]() { getter_started.store(true, std::memory_order_release); observed = data.edit_state_value(&Lease_Test_State::value); getter_returned.store(true, std::memory_order_release); }); ASSERT_TRUE(wait_until([&]() { return getter_started.load(std::memory_order_acquire); })); std::this_thread::sleep_for(std::chrono::milliseconds(20)); EXPECT_FALSE(getter_returned.load(std::memory_order_acquire)); held = {}; getter.join(); EXPECT_TRUE(getter_returned.load(std::memory_order_acquire)); EXPECT_EQ(observed, 77); } TEST(Renderive_Input_Runtime, ExactInputDataBindingSkipsAllocation) { using No_Input_Binding = Renderable_Binding; using Custom_Input_Binding = Renderable_Binding; EXPECT_FALSE(static_cast(No_Input_Binding::create_input_data())); EXPECT_TRUE(static_cast(Custom_Input_Binding::create_input_data())); No_Input_Test_Data data; Render_Input_Lease lease(&data); EXPECT_FALSE(static_cast(lease)); } TEST(Renderive_Input_Runtime, ExplicitRuntimeCreatesOneStateAndInput) { Runtime_Count_Owner owner; Runtime_Count_Data explicit_data; explicit_data.initialize_runtime(make_explicit_renderable_runtime(owner)); EXPECT_EQ(owner.state_create_count, 1); EXPECT_EQ(owner.input_create_count, 1); EXPECT_NE(explicit_data.edit_state(), nullptr); { Render_Edit_Lease edit(&explicit_data); ASSERT_TRUE(static_cast(edit)); edit->value = 42; } EXPECT_EQ(explicit_data.edit_state_value(&Lease_Test_State::value), 42); } TEST(Renderive_Input_Runtime, LowLatencyRuntimeCreatesThreeStatesAndInputs) { Runtime_Count_Owner owner; Runtime_Count_Data low_latency_data; low_latency_data.initialize_runtime(make_low_latency_renderable_runtime(owner)); EXPECT_EQ(owner.state_create_count, 3); EXPECT_EQ(owner.input_create_count, 3); EXPECT_NE(low_latency_data.edit_state(), nullptr); EXPECT_NE(low_latency_data.ready_state(), nullptr); EXPECT_NE(low_latency_data.render_state(), nullptr); } TEST(Renderive_Input_Runtime, ExplicitRuntimeContainsNoTripleBuffer) { Runtime_Count_Owner owner; Runtime_Count_Data data; data.initialize_runtime(make_explicit_renderable_runtime(owner)); auto frame_view = data.capture_frame_view(); ASSERT_TRUE(static_cast(frame_view)); EXPECT_EQ(frame_view.state, data.render_state()); EXPECT_EQ(frame_view.input, data.render_input_data()); } TEST(Renderive_Input_Runtime, ExplicitInputFrameDoesNotDeadlock) { Runtime_Count_Owner owner; Runtime_Count_Data data; data.initialize_runtime(make_explicit_renderable_runtime(owner)); auto completion = make_update_state(); { auto edit = data.acquire_edit_input(true); ASSERT_TRUE(static_cast(edit)); edit.set_completion(completion); } auto frame_view = data.capture_frame_view(); ASSERT_TRUE(static_cast(frame_view)); frame_view.consume_input(); EXPECT_TRUE(completion->stage_complete(Update_Stage::Committed)); std::atomic_bool setter_returned{false}; std::thread setter([&]() { auto edit = data.acquire_edit_input(true); setter_returned.store(true, std::memory_order_release); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); EXPECT_FALSE(setter_returned.load(std::memory_order_acquire)); frame_view = {}; setter.join(); EXPECT_TRUE(setter_returned.load(std::memory_order_acquire)); } TEST(Renderive_Input_Runtime, ExplicitInputConsumedExactlyOnce) { Counting_Input_Owner owner; Counting_Input_Data data; data.initialize_runtime(make_explicit_renderable_runtime(owner)); auto completion = make_update_state(); { auto edit = data.acquire_edit_input(true); ASSERT_TRUE(static_cast(edit)); edit.set_completion(completion); } auto frame_view = data.capture_frame_view(); ASSERT_TRUE(static_cast(frame_view)); auto* input = static_cast(frame_view.input); ASSERT_NE(input, nullptr); frame_view.consume_input(); frame_view.consume_input(); EXPECT_EQ(input->clear_count, 1); EXPECT_TRUE(completion->stage_complete(Update_Stage::Committed)); std::pmr::vector> committed(memory_resource(Memory_Domain::Update_Completion)); data.drain_committed_update_states(committed); EXPECT_EQ(committed.size(), 1u); committed.clear(); data.drain_committed_update_states(committed); EXPECT_TRUE(committed.empty()); } TEST(Renderive_Input_Runtime, ExplicitPrepareAndDrawUseSameState) { Runtime_Count_Owner owner; Runtime_Count_Data data; data.initialize_runtime(make_explicit_renderable_runtime(owner)); auto frame_view = data.capture_frame_view(); ASSERT_TRUE(static_cast(frame_view)); auto* prepare_state = static_cast(frame_view.state); auto* draw_state = static_cast(frame_view.state); ASSERT_NE(prepare_state, nullptr); EXPECT_EQ(prepare_state, draw_state); std::atomic_bool setter_returned{false}; std::thread setter([&]() { auto edit = data.acquire_edit_state(); setter_returned.store(true, std::memory_order_release); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); EXPECT_FALSE(setter_returned.load(std::memory_order_acquire)); frame_view = {}; setter.join(); EXPECT_TRUE(setter_returned.load(std::memory_order_acquire)); } TEST(Renderive_Input_Runtime, ExplicitPrepareAndDrawUseSameInput) { Runtime_Count_Owner owner; Runtime_Count_Data data; data.initialize_runtime(make_explicit_renderable_runtime(owner)); auto frame_view = data.capture_frame_view(); ASSERT_TRUE(static_cast(frame_view)); auto* prepare_input = static_cast(frame_view.input); auto* draw_input = static_cast(frame_view.input); ASSERT_NE(prepare_input, nullptr); EXPECT_EQ(prepare_input, draw_input); std::atomic_bool setter_returned{false}; std::thread setter([&]() { auto edit = data.acquire_edit_input(true); setter_returned.store(true, std::memory_order_release); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); EXPECT_FALSE(setter_returned.load(std::memory_order_acquire)); frame_view = {}; setter.join(); EXPECT_TRUE(setter_returned.load(std::memory_order_acquire)); } TEST(Renderive_Input_Runtime, CrossRenderableReadUsesFrameSnapshot) { auto axis = std::make_shared(); Axis_Render_State frame_state; frame_state.orientation = Orientation::Horizontal; frame_state.x = 10; frame_state.pixel_length = 50; frame_state.coord_start = 100.0; frame_state.coord_length = 25.0; frame_state.unit_text = "Hz"; Renderable_Frame_Tree tree; auto& node = tree.emplace_back(); node.renderable = axis; node.node_id = 77; node.frame_view.state = &frame_state; Render_Frame_Snapshot snapshot; snapshot.frame_tree = &tree; ASSERT_EQ(snapshot.find_node(77), &node); Axis_Frame_Snapshot axis_snapshot = Axis_Render_Access::snapshot(axis.get(), snapshot); EXPECT_EQ(axis_snapshot.coord_range.origin, 100.0); EXPECT_EQ(axis_snapshot.coord_range.target, 125.0); EXPECT_EQ(axis_snapshot.pixel_range.origin, 10.0); EXPECT_EQ(axis_snapshot.pixel_range.target, 60.0); EXPECT_EQ(Axis_Render_Access::unit_text(axis.get(), snapshot), "Hz"); } TEST(Renderive_Input_Runtime, LowLatencyRenderLeaseHeldUntilFrameEnd) { Runtime_Count_Owner owner; Runtime_Count_Data data; data.initialize_runtime(make_low_latency_renderable_runtime(owner)); auto first = data.capture_frame_view(); ASSERT_TRUE(static_cast(first)); auto second = data.capture_frame_view(); EXPECT_FALSE(static_cast(second)); first = {}; auto third = data.capture_frame_view(); EXPECT_TRUE(static_cast(third)); } TEST(Renderive_Input_Runtime, FlowCreatesRenderableRuntime) { Runtime_Count_Owner low_owner; Runtime_Count_Owner explicit_owner; Low_Latency_Frame_Flow low_latency_flow; Explicit_Frame_Flow explicit_flow; auto low_runtime = low_latency_flow.create_renderable_runtime(low_owner); auto explicit_runtime = explicit_flow.create_renderable_runtime(explicit_owner); EXPECT_NE(dynamic_cast(low_runtime.get()), nullptr); EXPECT_NE(dynamic_cast(explicit_runtime.get()), nullptr); } TEST(Renderive_Frequency_Trace, FrequencyTraceContinuousTicksUseOneSegment) { Frequency_Trace_Private trace; auto timeline = timeline_for_ticks(0, 3); prepare_frequency_trace(trace, {0, 1, 2, 3}, timeline); EXPECT_EQ(trace.segment_count, 1); EXPECT_EQ(trace.tick_gap_count, 0); EXPECT_EQ(trace.non_monotonic_tick_count, 0); ASSERT_EQ(trace.segment_offsets.size(), 1u); EXPECT_EQ(trace.prepared_points.size(), 4u); } TEST(Renderive_Frequency_Trace, FrequencyTraceTickGapSplitsSegment) { Frequency_Trace_Private trace; auto timeline = timeline_for_ticks(0, 4); prepare_frequency_trace(trace, {0, 1, 3, 4}, timeline); EXPECT_EQ(trace.segment_count, 2); EXPECT_EQ(trace.tick_gap_count, 1); EXPECT_EQ(trace.non_monotonic_tick_count, 0); ASSERT_EQ(trace.segment_offsets.size(), 2u); EXPECT_EQ(trace.segment_offsets[0], 0u); EXPECT_EQ(trace.segment_offsets[1], 2u); } TEST(Renderive_Frequency_Trace, FrequencyTraceNonMonotonicTickSplitsSegment) { Frequency_Trace_Private trace; auto timeline = timeline_for_ticks(0, 3); prepare_frequency_trace(trace, {0, 1, 1, 2}, timeline); EXPECT_EQ(trace.segment_count, 2); EXPECT_EQ(trace.tick_gap_count, 0); EXPECT_EQ(trace.non_monotonic_tick_count, 1); ASSERT_EQ(trace.segment_offsets.size(), 2u); EXPECT_EQ(trace.segment_offsets[1], 2u); } TEST(Renderive_Frequency_Trace, FrequencyTraceMappingFailureSplitsSegment) { Frequency_Trace_Private trace; auto timeline = timeline_for_ticks(0, 2); prepare_frequency_trace(trace, {0, 1, 5, 2}, timeline); EXPECT_EQ(trace.segment_count, 2); EXPECT_EQ(trace.tick_gap_count, 0); EXPECT_EQ(trace.non_monotonic_tick_count, 0); ASSERT_EQ(trace.segment_offsets.size(), 2u); EXPECT_EQ(trace.segment_offsets[1], 2u); } TEST(Renderive_Frequency_Trace, FrequencyTraceRingWrapKeepsLogicalOrder) { Frequency_Trace_Private trace; trace.resize_power_buffer(3); trace.push_power(Frequency_Sample{0, 0.0}); trace.push_power(Frequency_Sample{1, 1.0}); trace.push_power(Frequency_Sample{2, 2.0}); trace.push_power(Frequency_Sample{3, 3.0}); trace.snapshot_power_buffer(); ASSERT_EQ(trace.data_count, 3); EXPECT_EQ(trace.data_snapshot[0].tick, 1); EXPECT_EQ(trace.data_snapshot[1].tick, 2); EXPECT_EQ(trace.data_snapshot[2].tick, 3); } TEST(Renderive_Frame_Pipeline, PublishAndPaintUseMiddleVersionOnly) { Low_Latency_Frame_Pipeline pipeline; auto published = publish_pipeline_frame(pipeline, 1, 25); EXPECT_TRUE(pipeline.middle_frame_ready()); EXPECT_TRUE(published.request_present); EXPECT_EQ(published.dropped_frame_record, nullptr); auto painting = pipeline.acquire_paint_frame(30); EXPECT_TRUE(painting.has_new_frame); EXPECT_TRUE(painting.has_valid_frame); EXPECT_TRUE(static_cast(painting.lease)); EXPECT_FALSE(pipeline.middle_frame_ready()); } TEST(Renderive_Frame_Pipeline, PresentBeforeFirstFrameReturnsEmpty) { Low_Latency_Frame_Pipeline pipeline; auto consumed = pipeline.acquire_paint_frame(10); EXPECT_FALSE(static_cast(consumed.lease)); EXPECT_FALSE(consumed.has_valid_frame); EXPECT_TRUE(consumed.underrun); } TEST(Renderive_Frame_Pipeline, PresentWithoutNewFrameRepeatsPainting) { 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)); EXPECT_TRUE(first.has_new_frame); first.lease.reset(); auto repeated = pipeline.acquire_paint_frame(35); EXPECT_TRUE(static_cast(repeated.lease)); EXPECT_FALSE(repeated.has_new_frame); EXPECT_TRUE(repeated.has_valid_frame); EXPECT_FALSE(repeated.underrun); } TEST(Renderive_Frame_Pipeline, PresentDuringMiddleBusyRepeatsPainting) { 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); EXPECT_TRUE(static_cast(repeated.lease)); EXPECT_FALSE(repeated.has_new_frame); EXPECT_TRUE(repeated.has_valid_frame); EXPECT_FALSE(repeated.underrun); 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); auto first = pipeline.acquire_paint_frame(30); ASSERT_TRUE(static_cast(first.lease)); first.lease.reset(); auto busy_painting = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Painting); ASSERT_EQ(busy_painting.result, Triple_Buffer_Result::Success); std::atomic_bool returned{false}; std::atomic_bool lease_ok{false}; std::atomic_bool underrun_seen{false}; std::thread waiter([&]() { auto consumed = pipeline.acquire_paint_frame(40); lease_ok.store(static_cast(consumed.lease), std::memory_order_release); underrun_seen.store(consumed.underrun, std::memory_order_release); returned.store(true, std::memory_order_release); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); EXPECT_FALSE(returned.load(std::memory_order_acquire)); Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_painting.lease); waiter.join(); EXPECT_TRUE(returned.load(std::memory_order_acquire)); EXPECT_TRUE(lease_ok.load(std::memory_order_acquire)); EXPECT_FALSE(underrun_seen.load(std::memory_order_acquire)); } TEST(Renderive_Frame_Pipeline, TransientPaintBusyDoesNotCountUnderrun) { 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(); auto repeated = pipeline.acquire_paint_frame(35); EXPECT_TRUE(static_cast(repeated.lease)); EXPECT_FALSE(repeated.underrun); EXPECT_FALSE(repeated.paint_frame_unavailable); } TEST(Renderive_Frame_Pipeline, RepeatedFrameDoesNotRequestBackgroundFallback) { 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)); ASSERT_FALSE(first.lease.frame->image.empty()); first.lease.reset(); auto repeated = pipeline.acquire_paint_frame(35); ASSERT_TRUE(static_cast(repeated.lease)); EXPECT_FALSE(repeated.has_new_frame); EXPECT_FALSE(repeated.lease.frame->image.empty()); EXPECT_FALSE(repeated.underrun); } TEST(Renderive_Frame_Pipeline, BeforeFirstPublishReturnsEmpty) { Low_Latency_Frame_Pipeline pipeline; auto consumed = pipeline.acquire_paint_frame(10); EXPECT_FALSE(static_cast(consumed.lease)); EXPECT_FALSE(Low_Latency_Frame_Pipeline_Test_Probe::has_published_frame(pipeline)); EXPECT_TRUE(consumed.underrun); } TEST(Renderive_Frame_Pipeline, FirstPublishCreatesPersistentPainting) { Low_Latency_Frame_Pipeline pipeline; ASSERT_TRUE(publish_pipeline_frame(pipeline, 1, 25).request_present); EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::has_published_frame(pipeline)); auto first = pipeline.acquire_paint_frame(30); ASSERT_TRUE(static_cast(first.lease)); EXPECT_TRUE(first.has_new_frame); EXPECT_TRUE(first.has_valid_frame); EXPECT_EQ(first.lease.frame->version.load(std::memory_order_acquire), 1u); EXPECT_FALSE(first.lease.frame->image.empty()); } TEST(Renderive_Frame_Pipeline, NoNewMiddleReturnsSamePainting) { 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 repeated = pipeline.acquire_paint_frame(35); ASSERT_TRUE(static_cast(repeated.lease)); EXPECT_EQ(repeated.lease.frame, first_frame); EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); EXPECT_FALSE(repeated.lease.frame->image.empty()); } TEST(Renderive_Frame_Pipeline, RenderFailureKeepsPreviousPainting) { 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.reset(); auto repeated = pipeline.acquire_paint_frame(40); ASSERT_TRUE(static_cast(repeated.lease)); EXPECT_EQ(repeated.lease.frame, first_frame); EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); EXPECT_FALSE(repeated.lease.frame->image.empty()); } TEST(Renderive_Frame_Pipeline, PublishFailureKeepsPreviousPainting) { 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 busy_middle = Low_Latency_Frame_Pipeline_Test_Probe::acquire_frame_role(pipeline, Frame_Middle); ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success); 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->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); Low_Latency_Frame_Pipeline_Test_Probe::release_frame_role(pipeline, busy_middle.lease); auto repeated = pipeline.acquire_paint_frame(45); ASSERT_TRUE(static_cast(repeated.lease)); EXPECT_EQ(repeated.lease.frame, first_frame); EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); 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); auto first = pipeline.acquire_paint_frame(30); ASSERT_TRUE(static_cast(first.lease)); auto first_version = first.lease.frame->version.load(std::memory_order_acquire); first.lease.reset(); for (int i = 0; i < 1000; ++i) { auto repeated = pipeline.acquire_paint_frame(31 + static_cast(i)); ASSERT_TRUE(static_cast(repeated.lease)); EXPECT_FALSE(repeated.lease.frame->image.empty()); EXPECT_EQ(repeated.lease.frame->version.load(std::memory_order_acquire), first_version); EXPECT_FALSE(repeated.underrun); } } 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 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) { static_assert(std::is_default_constructible_v); static_assert(std::is_default_constructible_v); static_assert(std::is_default_constructible_v); static_assert(std::is_base_of_v); static_assert(std::is_move_constructible_v); static_assert(!std::is_copy_constructible_v); static_assert(!std::is_same_v); static_assert(sizeof(Present_Surface_Lease) <= 160); Low_Latency_Frame frame; Render_Output& output = frame; EXPECT_TRUE(output.metadata); EXPECT_TRUE(output.presented_update_states.empty()); } TEST(Renderive_MPMCQueue, UsesTryOnlyBoundedCapacity) { rigtorp::MPMCQueue queue(2); EXPECT_TRUE(queue.try_push(1)); EXPECT_TRUE(queue.try_push(2)); EXPECT_FALSE(queue.try_push(3)); int value = 0; EXPECT_TRUE(queue.try_pop(value)); EXPECT_EQ(value, 1); EXPECT_TRUE(queue.try_pop(value)); EXPECT_EQ(value, 2); EXPECT_FALSE(queue.try_pop(value)); } TEST(Renderive_Curve_Sampling, AllSamplesKeepsSourcePointCount) { Axis_Mapping_2D mapping{ Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.0, 4.0}, Range{0.0, 100.0}}, Axis_Frame_Snapshot{Orientation::Vertical, Range{0.0, 10.0}, Range{100.0, 0.0}} }; std::array samples{0.0, 1.0, 2.0, 3.0, 4.0}; auto points = Curve_Utils::build_resampled_points(mapping, Range{0.0, 4.0}, 5, 5, false, Line_Interpolation_Mode::Linear_Value, [&samples](int i) { return samples[static_cast(i)]; }); ASSERT_EQ(points.size(), samples.size()); EXPECT_DOUBLE_EQ(points.front().x, 0.0); EXPECT_DOUBLE_EQ(points.back().x, 100.0); } TEST(Renderive_Curve_Sampling, AllSamplesAddsVisibleBoundaryPoints) { Axis_Mapping_2D mapping{ Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.5, 3.5}, Range{0.0, 90.0}}, Axis_Frame_Snapshot{Orientation::Vertical, Range{0.0, 40.0}, Range{0.0, 40.0}} }; std::array samples{0.0, 10.0, 20.0, 30.0, 40.0}; auto points = Curve_Utils::build_resampled_points(mapping, Range{0.0, 4.0}, 5, 5, true, Line_Interpolation_Mode::Linear_Value, [&samples](int i) { return samples[static_cast(i)]; }); ASSERT_EQ(points.size(), 5u); EXPECT_DOUBLE_EQ(mapping.domain.pixel_to_coord(points.front().x), 0.5); EXPECT_DOUBLE_EQ(points.front().y, 5.0); EXPECT_DOUBLE_EQ(mapping.domain.pixel_to_coord(points.back().x), 3.5); EXPECT_DOUBLE_EQ(points.back().y, 35.0); } TEST(Renderive_Curve_Sampling, AllSamplesUsesInterpolationModeForBoundaryPoints) { Axis_Mapping_2D mapping{ Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.5, 1.5}, Range{0.0, 100.0}}, Axis_Frame_Snapshot{Orientation::Vertical, Range{0.0, 20.0}, Range{0.0, 20.0}} }; std::array samples{0.0, 10.0, 20.0}; auto points = Curve_Utils::build_resampled_points(mapping, Range{0.0, 2.0}, 3, 3, true, Line_Interpolation_Mode::Step_Left, [&samples](int i) { return samples[static_cast(i)]; }); ASSERT_EQ(points.size(), 4u); EXPECT_DOUBLE_EQ(points.front().y, 0.0); EXPECT_DOUBLE_EQ(points[1].y, 0.0); EXPECT_DOUBLE_EQ(points[2].y, 10.0); EXPECT_DOUBLE_EQ(points[1].x, points[2].x); EXPECT_DOUBLE_EQ(points.back().y, 10.0); } TEST(Renderive_Curve_Sampling, PixelEnvelopeUsesPhysicalPixelBuckets) { Axis_Mapping_2D mapping{ Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.0, 5.0}, Range{10.1, 10.9}}, Axis_Frame_Snapshot{Orientation::Vertical, Range{-100.0, 100.0}, Range{-100.0, 100.0}} }; std::array samples{0.0, 100.0, -100.0, 80.0, -80.0, 0.0}; auto points = Curve_Utils::build_pixel_envelope_points(mapping, Range{0.0, 5.0}, 6, 6, false, [&samples](int i) { return samples[static_cast(i)]; }); EXPECT_LE(points.size(), 4u); } TEST(Renderive_Curve_Sampling, PixelEnvelopeSkipsNonFiniteSamples) { Axis_Mapping_2D mapping{ Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.0, 4.0}, Range{0.0, 4.0}}, Axis_Frame_Snapshot{Orientation::Vertical, Range{-1.0, 1.0}, Range{-1.0, 1.0}} }; std::array samples{0.0, std::numeric_limits::quiet_NaN(), 1.0, std::numeric_limits::infinity(), -1.0}; auto points = Curve_Utils::build_pixel_envelope_points(mapping, Range{0.0, 4.0}, 5, 5, false, [&samples](int i) { return samples[static_cast(i)]; }); ASSERT_FALSE(points.empty()); for (const PointF& point : points) { EXPECT_TRUE(std::isfinite(point.x)); EXPECT_TRUE(std::isfinite(point.y)); } } TEST(Renderive_Curve_Sampling, ReverseAxisEnvelopeKeepsSourceOrderInsideBucket) { Axis_Mapping_2D mapping{ Axis_Frame_Snapshot{Orientation::Horizontal, Range{0.0, 4.0}, Range{1.0, 0.0}}, Axis_Frame_Snapshot{Orientation::Vertical, Range{-20.0, 20.0}, Range{20.0, 0.0}} }; std::array samples{0.0, 10.0, -10.0, 20.0, -20.0}; auto points = Curve_Utils::build_pixel_envelope_points(mapping, Range{0.0, 4.0}, 5, 5, false, [&samples](int i) { return samples[static_cast(i)]; }); ASSERT_FALSE(points.empty()); double previous = -1.0; for (const PointF& point : points) { double coord = mapping.domain.pixel_to_coord(point.x); EXPECT_GE(coord, previous); previous = coord; } } TEST(Renderive_Frame_Feedback, AlternatingRenderPaintJitterDoesNotFlap) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(1000.0); std::uint64_t base = 0; for (std::uint64_t i = 0; i < 40; ++i, base += 20000000) { std::uint64_t render_ns = i % 2 ? 10000000 : 11000000; std::uint64_t paint_ns = i % 2 ? 11000000 : 10000000; feed_feedback_frame(controller, i + 1, base, render_ns, paint_ns); } EXPECT_NE(controller.state().bottleneck, Low_Latency_Bottleneck_State::Consumer_Limited); EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); } TEST(Renderive_Frame_Feedback, TransientPaintSpikeDoesNotChangeStrategy) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(1000.0); std::uint64_t base = 0; for (std::uint64_t i = 0; i < 24; ++i, base += 20000000) feed_feedback_frame(controller, i + 1, base, 10000000, 5000000); ASSERT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); auto spike = feedback_frame(100, base, 5000000, 60000000, 20000000, 20000000); controller.on_render_completed(spike); controller.on_frame_presented(spike); EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); EXPECT_NE(controller.state().bottleneck, Low_Latency_Bottleneck_State::Consumer_Limited); } TEST(Renderive_Frame_Feedback, ConsumerLimitedRequiresHighWindowConfidence) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(1000.0); std::uint64_t base = 0; for (std::uint64_t i = 0; i < 16; ++i, base += 25000000) feed_backlogged_feedback_frame(controller, i + 1, base, 5000000, 5000000, 5000000, 5000000); EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); EXPECT_EQ(controller.state().consumer_confidence, Consumer_Confidence::High); feed_backlogged_feedback_frame(controller, 17, base, 5000000, 5000000, 5000000, 5000000); EXPECT_EQ(controller.state().bottleneck, Low_Latency_Bottleneck_State::Consumer_Limited); EXPECT_EQ(controller.state().source, Refresh_Limit_Source::Paint); EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); EXPECT_EQ(controller.state().consumer_confidence, Consumer_Confidence::High); EXPECT_EQ(controller.state().limit_reason, Low_Latency_Limit_Reason::Consumer_Capacity); } TEST(Renderive_Frame_Feedback, ConsumerRecoveryUsesExitHysteresis) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); std::uint64_t base = 0; for (std::uint64_t i = 0; i < 18; ++i, base += 50000000) feed_backlogged_feedback_frame(controller, i + 1, base, 5000000, 5000000, 5000000, 5000000); ASSERT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); for (std::uint64_t i = 0; i < 23; ++i, base += 12000000) feed_feedback_frame(controller, 100 + i, base, 1000000, 1000000); EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); feed_feedback_frame(controller, 200, base, 1000000, 1000000); EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); EXPECT_NE(controller.state().bottleneck, Low_Latency_Bottleneck_State::Consumer_Limited); } TEST(Renderive_Frame_Feedback, QueueWaitDoesNotChangePaintService) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(1000.0); auto frame = feedback_frame(1, 0, 5000000, 5000000); frame.present_queue_wait_ns = 50000000; controller.on_render_completed(frame); controller.on_frame_presented(frame); EXPECT_LT(controller.state().paint_period_ns, 10000000u); } TEST(Renderive_Frame_Feedback, InputLimitedIsNotReportedAsRenderLimited) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(1000.0); for (std::uint64_t i = 1; i <= 24; ++i) controller.on_input_changed(i * 30000000, i, Dirty_Reason::Core_Data); std::uint64_t base = 800000000; for (std::uint64_t i = 0; i < 24; ++i, base += 30000000) feed_feedback_frame(controller, i + 1, base, 5000000, 5000000); EXPECT_EQ(controller.state().source, Refresh_Limit_Source::Input); EXPECT_EQ(controller.state().bottleneck, Low_Latency_Bottleneck_State::Input_Limited); } TEST(Renderive_Frame_Feedback, DeadlineRemainsMonotonicAcrossCandidateChanges) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); Frame_Lifecycle_Record frame; frame.render_role_enter_ns = 100000000; controller.on_render_started(frame, 100000000); std::uint64_t first_deadline = controller.next_render_time_ns(); ASSERT_GT(first_deadline, 100000000u); controller.set_max_render_fps(1000.0); EXPECT_GE(controller.next_render_time_ns(), first_deadline); std::uint64_t base = 120000000; for (std::uint64_t i = 0; i < 8; ++i, base += 20000000) feed_feedback_frame(controller, i + 1, base, i % 2 ? 30000000 : 5000000, i % 2 ? 5000000 : 30000000, 1000000, 1000000); EXPECT_GE(controller.next_render_time_ns(), first_deadline); } TEST(Renderive_Frame_Feedback, ConsumerSampleAcceptedOnlyWithContinuousBacklog) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); feed_backlogged_feedback_frame(controller, 2, 50000000, 2000000, 1000000); EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Accepted); EXPECT_EQ(controller.state().consumer_sample_accepted_count, 1u); feed_feedback_frame(controller, 3, 100000000, 2000000, 1000000); EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Rejected_No_Backlog); EXPECT_EQ(controller.state().consumer_sample_rejected_no_backlog_count, 1u); } TEST(Renderive_Frame_Feedback, AdmissionDeadlineWaitIsNotProducerStarvation) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); feed_backlogged_feedback_frame(controller, 2, 50000000, 2000000, 1000000); std::uint64_t accepted = controller.state().consumer_sample_accepted_count; auto frame = feedback_frame(3, 100000000, 2000000, 1000000); controller.note_middle_published(frame.transition_12_ns); controller.on_render_completed(frame); controller.note_admission_wait(20000000, frame.transition_12_ns + 1); controller.on_frame_presented(frame); EXPECT_EQ(controller.state().consumer_sample_accepted_count, accepted + 1); EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Accepted); EXPECT_EQ(controller.state().producer_starvation_count, 0u); EXPECT_FALSE(controller.state().producer_starvation_active); } TEST(Renderive_Frame_Feedback, ConsumerSampleRejectedWhenNoNewFrame) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); auto frame = feedback_frame(1, 100000000, 2000000, 1000000); controller.note_middle_published(frame.transition_12_ns); controller.on_render_completed(frame); controller.on_frame_presented(frame); controller.note_middle_published(frame.transition_12_ns + 1000); frame.paint_begin_ns += 50000000; frame.paint_end_ns = frame.paint_begin_ns + 1000000; controller.on_frame_presented(frame); EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Rejected_No_New_Frame); } TEST(Renderive_Frame_Feedback, LowConfidenceFallsBackToUserPeriod) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); feed_backlogged_feedback_frame(controller, 2, 100000000, 2000000, 1000000); EXPECT_EQ(controller.state().consumer_confidence, Consumer_Confidence::Low); EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Latency_Eager); EXPECT_EQ(controller.state().effective_min_render_interval_ns, controller.state().user_period_ns); } TEST(Renderive_Frame_Feedback, EffectiveIntervalNeverExceedsFourUserPeriods) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); std::uint64_t base = 0; for (std::uint64_t i = 0; i < 18; ++i, base += 200000000) feed_backlogged_feedback_frame(controller, i + 1, base, 2000000, 1000000); EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); EXPECT_GE(controller.state().effective_min_render_interval_ns, controller.state().user_period_ns); EXPECT_LE(controller.state().effective_min_render_interval_ns, controller.state().user_period_ns * 4); } TEST(Renderive_Frame_Feedback, EffectiveIntervalRisesAtBoundedRate) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); std::uint64_t base = 0; for (std::uint64_t i = 0; i < 17; ++i, base += 80000000) feed_backlogged_feedback_frame(controller, i + 1, base, 2000000, 1000000); EXPECT_EQ(controller.state().production_mode, Low_Latency_Production_Mode::Consumer_Paced); EXPECT_LE(controller.state().effective_min_render_interval_ns, static_cast(static_cast(controller.state().user_period_ns) * 1.10) + 1); } TEST(Renderive_Frame_Feedback, UpdateToPaintLatencyMeasuredSeparately) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 2000000, 0, 10000000); EXPECT_GT(controller.state().update_to_paint_begin_ns, controller.state().paint_service_ns); EXPECT_LT(controller.state().paint_service_ns, 5000000u); } TEST(Renderive_Frame_Feedback, DiagnosticsCountAcceptedAndRejectedSamples) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); feed_backlogged_feedback_frame(controller, 2, 50000000, 2000000, 1000000); feed_feedback_frame(controller, 3, 100000000, 2000000, 1000000); auto diagnostics = controller.performance_diagnostics(); EXPECT_EQ(diagnostics.consumer_sample_accepted_count, 1u); EXPECT_EQ(diagnostics.consumer_sample_rejected_no_backlog_count, 1u); } TEST(Renderive_Frame_Feedback, RawDisplayIntervalUpdatesForRejectedSample) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); feed_feedback_frame(controller, 1, 0, 2000000, 1000000); feed_feedback_frame(controller, 2, 50000000, 2000000, 1000000); EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Rejected_No_Backlog); EXPECT_GT(controller.state().raw_display_interval_current_ns, 0u); EXPECT_EQ(controller.state().accepted_consumer_interval_current_ns, 0u); } TEST(Renderive_Frame_Feedback, AcceptedConsumerIntervalUpdatesOnlyForAcceptedSample) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); feed_backlogged_feedback_frame(controller, 1, 0, 2000000, 1000000); EXPECT_EQ(controller.state().accepted_consumer_interval_current_ns, 0u); feed_backlogged_feedback_frame(controller, 2, 50000000, 2000000, 1000000); EXPECT_EQ(controller.state().last_consumer_sample_result, Consumer_Sample_Result::Accepted); EXPECT_GT(controller.state().raw_display_interval_current_ns, 0u); EXPECT_GT(controller.state().accepted_consumer_interval_current_ns, 0u); } TEST(Renderive_Frame_Feedback, OldRejectedSamplesDoNotLockConfidenceForever) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); std::uint64_t base = 0; for (std::uint64_t i = 0; i < 96; ++i, base += 20000000) feed_feedback_frame(controller, i + 1, base, 2000000, 1000000); ASSERT_EQ(controller.state().consumer_confidence, Consumer_Confidence::Low); for (std::uint64_t i = 0; i < 64; ++i, base += 50000000) feed_backlogged_feedback_frame(controller, 200 + i, base, 2000000, 1000000, 1000000, 1000000); EXPECT_EQ(controller.state().consumer_confidence, Consumer_Confidence::High); EXPECT_EQ(controller.state().consumer_window_size, 64u); EXPECT_GE(controller.state().consumer_window_accepted_ratio, 0.75); } TEST(Renderive_Frame_Feedback, ProducerStarvationCountsStateTransitionOnce) { Low_Latency_Feedback_Controller controller; controller.set_max_render_fps(60.0); auto frame = feedback_frame(1, 100000000, 2000000, 1000000); controller.on_render_completed(frame); controller.note_middle_published(frame.transition_12_ns); std::uint64_t deadline = controller.next_render_time_ns(); ASSERT_GT(deadline, 0u); controller.observe_runtime(deadline + 100000000, false, true, false); controller.observe_runtime(deadline + 200000000, false, true, false); controller.observe_runtime(deadline + 300000000, false, true, false); ASSERT_TRUE(controller.state().producer_starvation_active); EXPECT_EQ(controller.state().producer_starvation_count, 1u); controller.observe_runtime(deadline + 400000000, false, true, false); controller.observe_runtime(deadline + 500000000, false, true, false); EXPECT_EQ(controller.state().producer_starvation_count, 1u); } TEST(Renderive_Frame_Feedback, ProducerIntervalUsesActualRenderStart) { Low_Latency_Feedback_Controller controller; auto first = feedback_frame(1, 100000000, 2000000, 1000000); auto second = feedback_frame(2, 116666667, 2000000, 1000000); controller.on_render_completed(first); controller.on_render_completed(second); EXPECT_NEAR(static_cast(controller.state().producer_interval_current_ns), 16666667.0, 1.0); } TEST(Renderive_Frame_Feedback, AdmissionWaitRecordsActualElapsedTimeAndClears) { Low_Latency_Feedback_Controller controller; controller.note_admission_wait(5000000, 100000000); EXPECT_EQ(controller.state().admission_wait_remaining_ns, 5000000u); controller.note_render_admitted(104000000); EXPECT_EQ(controller.state().admission_wait_elapsed_ns, 4000000u); EXPECT_EQ(controller.state().admission_wait_remaining_ns, 0u); controller.note_admission_wait(5000000, 200000000); controller.cancel_admission_wait(201000000); EXPECT_EQ(controller.state().admission_wait_elapsed_ns, 0u); EXPECT_EQ(controller.state().admission_wait_remaining_ns, 0u); } TEST(Renderive_Frame_Flow, LatencyEagerRendersWhileMiddleIsPending) { Low_Latency_Frame_Flow flow; Flow_Test_Host host; host.snapshot.dirty = true; host.snapshot.middle_has_new_frame = true; flow.attach(host); flow.activate_view(20); EXPECT_EQ(host.paint_requests, 1); EXPECT_EQ(host.render_attempts, 1); } TEST(Renderive_Frame_Flow, PendingPresentRequestSuppressesDuplicateRequest) { Low_Latency_Frame_Flow flow; auto published = publish_low_latency_flow_frame(flow, 1, 20); ASSERT_TRUE(published.request_present); Flow_Test_Host host; host.snapshot.middle_has_new_frame = true; flow.attach(host); flow.activate_view(30); EXPECT_EQ(host.paint_requests, 0); } TEST(Renderive_Frame_Flow, LowLatencyEmitsFeedbackEventsThroughHost) { Low_Latency_Frame_Flow flow; Flow_Test_Host host; host.snapshot.dirty = true; flow.attach(host); flow.notify_model_dirty(Dirty_Reason::Core_Data, 10); flow.activate_view(20); EXPECT_NE(std::find(host.feedback_events.begin(), host.feedback_events.end(), "InputChanged"), host.feedback_events.end()); EXPECT_NE(std::find(host.feedback_events.begin(), host.feedback_events.end(), "RenderAdmitted"), host.feedback_events.end()); EXPECT_EQ(std::find(host.feedback_events.begin(), host.feedback_events.end(), "RenderStarted"), host.feedback_events.end()); Frame_Lifecycle_Record frame; frame.render_begin_ns = 30; frame.render_end_ns = 40; flow.notify_render_finished(frame, 40); EXPECT_NE(std::find(host.feedback_events.begin(), host.feedback_events.end(), "RenderStarted"), host.feedback_events.end()); EXPECT_NE(std::find(host.feedback_events.begin(), host.feedback_events.end(), "RenderCompleted"), host.feedback_events.end()); } TEST(Renderive_Frame_Pipeline, NewRenderSupersedesOldMiddle) { 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(); auto second = publish_pipeline_frame(pipeline, 2, 40); ASSERT_TRUE(second.middle_published); auto third = publish_pipeline_frame(pipeline, 3, 50); EXPECT_TRUE(third.middle_published); ASSERT_NE(third.superseded_frame_record, nullptr); EXPECT_EQ(third.superseded_frame_record->frame_id, 2u); EXPECT_TRUE(third.duplicate_present_request_suppressed); } TEST(Renderive_Frame_Pipeline, OnlyOneUpdateRemainsPosted) { Low_Latency_Frame_Pipeline pipeline; auto first = publish_pipeline_frame(pipeline, 1, 25); ASSERT_TRUE(first.request_present); ASSERT_TRUE(first.update_posted); auto repeated = pipeline.request_present(30); EXPECT_FALSE(repeated.request_present); EXPECT_TRUE(repeated.duplicate_present_request_suppressed); EXPECT_TRUE(Low_Latency_Frame_Pipeline_Test_Probe::paint_request_pending(pipeline)); } TEST(Renderive_Frame_Flow, LowLatencyOwnsActiveState) { Low_Latency_Frame_Flow flow; Flow_Test_Host host; host.snapshot.dirty = true; flow.attach(host); flow.notify_model_dirty(10); EXPECT_FALSE(flow.view_active()); EXPECT_EQ(host.render_attempts, 0); flow.activate_view(20); EXPECT_TRUE(flow.view_active()); EXPECT_EQ(host.render_attempts, 1); host.snapshot.render_job_active = false; flow.deactivate_view(); flow.notify_model_dirty(30); EXPECT_FALSE(flow.view_active()); EXPECT_EQ(host.render_attempts, 1); EXPECT_GT(host.deadline_cancels, 0); Flow_Diagnostics diagnostics = flow.diagnostics(); EXPECT_TRUE(std::holds_alternative(diagnostics.strategy)); EXPECT_FALSE(diagnostics.common.active); } TEST(Renderive_Frame_Flow, ExplicitRequestDoesNotNeedActiveView) { Explicit_Frame_Flow flow; Flow_Test_Host host; host.snapshot.dirty = true; flow.attach(host); auto ticket = flow.request_render(); flow.notify_model_dirty(10); EXPECT_TRUE(static_cast(ticket)); EXPECT_FALSE(flow.view_active()); EXPECT_EQ(host.render_attempts, 1); Flow_Diagnostics diagnostics = flow.diagnostics(); EXPECT_TRUE(std::holds_alternative(diagnostics.strategy)); } TEST(Renderive_Frame_Flow, ExplicitTicketsDrainIntoAcceptedFrame) { Explicit_Frame_Flow flow; Flow_Test_Host host; flow.attach(host); auto ticket = flow.request_render(); std::pmr::vector> states(memory_resource(Memory_Domain::Update_Completion)); flow.drain_frame_update_states(states); ASSERT_TRUE(static_cast(ticket)); ASSERT_EQ(states.size(), 1u); EXPECT_EQ(ticket.id(), states.front()->id()); complete_update_states(states, Update_Stage::Presented); 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; auto lease = flow.begin_present(10, returned); EXPECT_FALSE(static_cast(lease)); EXPECT_EQ(returned, nullptr); } TEST(Renderive_Explicit_Present, PublishedOutputRemainsPresentable) { Explicit_Frame_Flow flow; ASSERT_TRUE(publish_explicit_frame(flow, 1, 25).request_present); std::shared_ptr returned; auto lease = flow.begin_present(30, returned); ASSERT_TRUE(static_cast(lease)); EXPECT_FALSE(lease.image().empty()); (void)flow.end_present(std::move(lease), 31, 32); } TEST(Renderive_Explicit_Present, RepeatedPresentReturnsSameOutput) { 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 repeated = flow.begin_present(40, returned); ASSERT_TRUE(static_cast(repeated)); EXPECT_EQ(Present_Surface_Lease_Access::output(repeated), first_output); EXPECT_FALSE(repeated.image().empty()); (void)flow.end_present(std::move(repeated), 41, 42); } TEST(Renderive_Explicit_Present, ActivePresentLeaseDoesNotHideOutput) { 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)); auto second = flow.begin_present(31, returned); ASSERT_TRUE(static_cast(second)); EXPECT_EQ(Present_Surface_Lease_Access::output(first), Present_Surface_Lease_Access::output(second)); EXPECT_FALSE(first.image().empty()); EXPECT_FALSE(second.image().empty()); (void)flow.end_present(std::move(second), 32, 33); (void)flow.end_present(std::move(first), 34, 35); } TEST(Renderive_Explicit_Present, FailedRenderKeepsPreviousOutput) { 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 failed_surface = flow.begin_render_frame(); ASSERT_TRUE(static_cast(failed_surface)); flow.discard_render_frame(std::move(failed_surface)); auto repeated = flow.begin_present(40, returned); ASSERT_TRUE(static_cast(repeated)); EXPECT_EQ(Present_Surface_Lease_Access::output(repeated), first_output); EXPECT_FALSE(repeated.image().empty()); (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); std::shared_ptr returned; auto old_lease = flow.begin_present(30, returned); ASSERT_TRUE(static_cast(old_lease)); Render_Output* old_output = Present_Surface_Lease_Access::output(old_lease); ASSERT_TRUE(publish_explicit_frame(flow, 2, 50, 3, 3, Color::red()).request_present); auto new_lease = flow.begin_present(55, returned); ASSERT_TRUE(static_cast(new_lease)); Render_Output* new_output = Present_Surface_Lease_Access::output(new_lease); EXPECT_NE(new_output, old_output); EXPECT_FALSE(old_lease.image().empty()); EXPECT_FALSE(new_lease.image().empty()); EXPECT_EQ(new_lease.image().width, 3); EXPECT_EQ(new_lease.image().height, 3); (void)flow.end_present(std::move(new_lease), 56, 57); (void)flow.end_present(std::move(old_lease), 58, 59); } TEST(Renderive_Render_Executor, ExecutorDoesNotOwnPlotAdmission) { Render_Executor executor(Render_Runtime_Config{1}); std::atomic_bool entered{false}; std::atomic_bool release{false}; std::atomic_int completed{0}; auto first_stat = std::make_shared(); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 1, 1, Render_Task_Kind::Frame, Task([&]() { entered.store(true, std::memory_order_release); while (!release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); }), {}, Task([&]() { completed.fetch_add(1, std::memory_order_acq_rel); }), first_stat, true })); ASSERT_TRUE(wait_until([&]() { return entered.load(std::memory_order_acquire); })); auto second_stat = std::make_shared(); EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ 2, 1, Render_Task_Kind::Frame, Task([]() {}), {}, Task([&]() { completed.fetch_add(1, std::memory_order_acq_rel); }), second_stat, true })); auto third_stat = std::make_shared(); EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ 3, 1, Render_Task_Kind::Frame, Task([]() {}), {}, Task([&]() { completed.fetch_add(1, std::memory_order_acq_rel); }), third_stat, true })); auto duplicate_stat = std::make_shared(); EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ 2, 2, Render_Task_Kind::Frame, Task([]() {}), {}, Task([&]() { completed.fetch_add(1, std::memory_order_acq_rel); }), duplicate_stat, true })); auto overflow_stat = std::make_shared(); EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ 4, 1, Render_Task_Kind::Frame, Task([]() {}), {}, Task([&]() { completed.fetch_add(1, std::memory_order_acq_rel); }), overflow_stat, true })); release.store(true, std::memory_order_release); ASSERT_TRUE(wait_until([&]() { return completed.load(std::memory_order_acquire) == 5; })); executor.shutdown(); } TEST(Renderive_Render_Executor, TaskflowTopologyRunsInFrameOrder) { Render_Executor executor(Render_Runtime_Config{2}); std::atomic_int sequence{0}; std::atomic_bool completed{false}; auto stat = std::make_shared(); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 1, 1, Render_Task_Kind::Frame, Task{}, [&sequence](Render_Task_Graph& graph, Render_Task_Graph_Node entry, Render_Task_Graph_Node exit) { auto prepare = graph.emplace(Render_Task_Kind::Frame, Task([&sequence]() { int expected = 0; sequence.compare_exchange_strong(expected, 1, std::memory_order_acq_rel); })); auto draw = graph.emplace(Render_Task_Kind::Primitive, Task([&sequence]() { int expected = 1; sequence.compare_exchange_strong(expected, 2, std::memory_order_acq_rel); })); auto finalize = graph.emplace(Render_Task_Kind::Frame, Task([&sequence]() { int expected = 2; sequence.compare_exchange_strong(expected, 3, std::memory_order_acq_rel); })); graph.precede(entry, prepare); graph.precede(prepare, draw); graph.precede(draw, finalize); graph.precede(finalize, exit); }, Task([&completed]() { completed.store(true, std::memory_order_release); }), stat, true })); ASSERT_TRUE(wait_until([&]() { return completed.load(std::memory_order_acquire); })); EXPECT_EQ(sequence.load(std::memory_order_acquire), 3); EXPECT_EQ(stat->task_count, 3u); executor.shutdown(); } TEST(Renderive_Render_Executor, FrameDrawWaitsForPrepareSubflow) { Render_Executor executor(Render_Runtime_Config{2}); std::latch child_entered(1); std::latch release_child(1); std::latch completed(1); std::atomic_bool child_finished{false}; std::atomic_bool draw_started{false}; auto stat = std::make_shared(); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 1, 1, Render_Task_Kind::Frame, Task{}, [&](Render_Task_Graph& graph, Render_Task_Graph_Node entry, Render_Task_Graph_Node exit) { auto prepare = graph.emplace_subflow(Render_Task_Kind::Frame, [&](Render_Task_Subflow& subflow) { auto child = subflow.emplace(Render_Task_Kind::Curve, Task([&]() { child_entered.count_down(); release_child.wait(); child_finished.store(true, std::memory_order_release); })); auto merge = subflow.emplace(Render_Task_Kind::Compose, Task([&]() { EXPECT_TRUE(child_finished.load(std::memory_order_acquire)); })); subflow.precede(child, merge); }); auto draw = graph.emplace(Render_Task_Kind::Primitive, Task([&]() { draw_started.store(true, std::memory_order_release); EXPECT_TRUE(child_finished.load(std::memory_order_acquire)); })); graph.precede(entry, prepare); graph.precede(prepare, draw); graph.precede(draw, exit); }, Task([&]() { completed.count_down(); }), stat, true })); child_entered.wait(); EXPECT_FALSE(draw_started.load(std::memory_order_acquire)); release_child.count_down(); completed.wait(); EXPECT_TRUE(draw_started.load(std::memory_order_acquire)); EXPECT_GE(stat->task_count, 3u); EXPECT_GE(stat->peak_parallelism, 1u); executor.shutdown(); } TEST(Renderive_Render_Executor, SubflowKindCountersReflectChildren) { Render_Executor executor(Render_Runtime_Config{4}); std::latch completed(1); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 1, 1, Render_Task_Kind::Frame, Task{}, [](Render_Task_Graph& graph, Render_Task_Graph_Node entry, Render_Task_Graph_Node exit) { auto prepare = graph.emplace_subflow(Render_Task_Kind::Frame, [](Render_Task_Subflow& subflow) { auto curve0 = subflow.emplace(Render_Task_Kind::Curve, Task([]() {})); auto curve1 = subflow.emplace(Render_Task_Kind::Curve, Task([]() {})); auto waterfall0 = subflow.emplace(Render_Task_Kind::Waterfall, Task([]() {})); auto waterfall1 = subflow.emplace(Render_Task_Kind::Waterfall, Task([]() {})); auto compose = subflow.emplace(Render_Task_Kind::Compose, Task([]() {})); subflow.precede(curve0, compose); subflow.precede(curve1, compose); subflow.precede(waterfall0, compose); subflow.precede(waterfall1, compose); }); auto draw = graph.emplace(Render_Task_Kind::Primitive, Task([]() {})); auto finalize = graph.emplace(Render_Task_Kind::Frame, Task([]() {})); graph.precede(entry, prepare); graph.precede(prepare, draw); graph.precede(draw, finalize); graph.precede(finalize, exit); }, Task([&]() { completed.count_down(); }), std::make_shared(), true })); completed.wait(); executor.shutdown(); Render_Executor_Snapshot snapshot = executor.snapshot(); EXPECT_GE(snapshot.curve_tasks, 2u); EXPECT_GE(snapshot.waterfall_tasks, 2u); EXPECT_GE(snapshot.compose_tasks, 1u); EXPECT_GE(snapshot.primitive_tasks, 3u); } TEST(Renderive_Render_Executor, SubmittedFramesAllCompleteOnSingleWorker) { Render_Executor executor(Render_Runtime_Config{1}); std::atomic_bool first_entered{false}; std::atomic_bool release_first{false}; std::atomic_int order_index{0}; std::array order{}; ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 1, 1, Render_Task_Kind::Frame, Task([&]() { first_entered.store(true, std::memory_order_release); while (!release_first.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 1; }), {}, Task([]() {}), std::make_shared(), true })); ASSERT_TRUE(wait_until([&]() { return first_entered.load(std::memory_order_acquire); })); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 2, 1, Render_Task_Kind::Frame, Task([&]() { order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 2; }), {}, Task([]() {}), std::make_shared(), true })); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 3, 1, Render_Task_Kind::Frame, Task([&]() { order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 3; }), {}, Task([]() {}), std::make_shared(), true })); release_first.store(true, std::memory_order_release); ASSERT_TRUE(wait_until([&]() { return order_index.load(std::memory_order_acquire) == 3; })); EXPECT_EQ(order_index.load(std::memory_order_acquire), 3); executor.shutdown(); } TEST(Renderive_Render_Executor, ShutdownWaitsForAcceptedQueuedFrame) { Render_Executor executor(Render_Runtime_Config{1}); std::atomic_bool first_entered{false}; std::atomic_bool release_first{false}; std::atomic_bool queued_work_ran{false}; std::atomic_bool queued_completed{false}; ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 1, 1, Render_Task_Kind::Frame, Task([&]() { first_entered.store(true, std::memory_order_release); while (!release_first.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); }), {}, Task([]() {}), std::make_shared(), true })); ASSERT_TRUE(wait_until([&]() { return first_entered.load(std::memory_order_acquire); })); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 2, 1, Render_Task_Kind::Frame, Task([&]() { queued_work_ran.store(true, std::memory_order_release); }), {}, Task([&]() { queued_completed.store(true, std::memory_order_release); }), std::make_shared(), true })); std::thread shutdown_thread([&]() { executor.shutdown(); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); EXPECT_FALSE(queued_completed.load(std::memory_order_acquire)); release_first.store(true, std::memory_order_release); shutdown_thread.join(); EXPECT_TRUE(queued_work_ran.load(std::memory_order_acquire)); EXPECT_TRUE(queued_completed.load(std::memory_order_acquire)); } TEST(Renderive_Performance_Shower, ToggleDoesNotReenterRenderableTreeUnsafely) { Plot_Core plot(std::make_unique()); plot.init(); auto root = plot.root_renderable(); ASSERT_TRUE(root); EXPECT_TRUE(root->children_snapshot().empty()); set_performance_overlay_enabled(plot, true); EXPECT_TRUE(performance_overlay_enabled(plot)); EXPECT_TRUE(root->children_snapshot().empty()); set_performance_overlay_enabled(plot, false); EXPECT_FALSE(performance_overlay_enabled(plot)); EXPECT_TRUE(root->children_snapshot().empty()); } TEST(Renderive_Performance_Shower, TogglePreservesAttachedOptions) { Plot_Core plot(std::make_unique()); plot.init(); Performance_Overlay_Options options; options.font.size = 17.0; options.background = Color{10, 20, 30, 255}; options.foreground = Color{240, 240, 240, 255}; auto original = attach_performance_overlay(plot, options); ASSERT_TRUE(original); set_performance_overlay_enabled(plot, true); EXPECT_TRUE(performance_overlay_enabled(plot)); set_performance_overlay_enabled(plot, false); EXPECT_FALSE(performance_overlay_enabled(plot)); set_performance_overlay_enabled(plot, true); auto toggled = performance_overlay(plot); ASSERT_EQ(toggled, original); EXPECT_EQ(toggled->options().font.size, options.font.size); EXPECT_EQ(toggled->options().background, options.background); EXPECT_EQ(toggled->options().foreground, options.foreground); } TEST(Renderive_Hover_Tooltip, DefaultTextPenContrastsWithDefaultBackground) { Hover_Tooltip_State state; EXPECT_EQ(state.tooltip_background_brush.color, Color::white()); EXPECT_EQ(state.tooltip_text_pen.color, Color::black()); } TEST(Renderive_Performance_Overlay, MetricUpdatesDoNotRequestIndependentRepaint) { auto state = std::make_shared(); state->enabled.store(true, std::memory_order_release); std::atomic_int update_count{}; state->update_callback = [&update_count]() { update_count.fetch_add(1, std::memory_order_relaxed); }; Performance_Frame_Record frame = performance_frame(1); ASSERT_TRUE(state->metric_queue.try_push(frame)); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { return state->published_snapshot.load(std::memory_order_acquire) && !state->worker_active.load(std::memory_order_acquire); })); EXPECT_EQ(update_count.load(std::memory_order_acquire), 0); state->snapshot_rebuild_requested.store(true, std::memory_order_release); schedule_performance_metrics_worker(state, true); ASSERT_TRUE(wait_until([&]() { return update_count.load(std::memory_order_acquire) == 1 && !state->worker_active.load(std::memory_order_acquire); })); state->cancelled.store(true, std::memory_order_release); } TEST(Renderive_Performance_Overlay, WorkerConsumesSharedFrameRecord) { auto state = std::make_shared(); state->enabled.store(true, std::memory_order_release); auto frame = std::make_shared(); frame->frame_id = 7; frame->input_version = 3; frame->render_begin_ns = 10; frame->render_end_ns = 20; frame->transition_12_ns = 25; frame->paint_begin_ns = 30; frame->paint_end_ns = 40; frame->transition_32_ns = 45; frame->pixel_width = 320; frame->pixel_height = 240; frame->outcome = Frame_Outcome::Presented; Performance_Frame_Record record = frame; ASSERT_TRUE(state->metric_queue.try_push(record)); schedule_performance_metrics_worker(state); auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); std::shared_ptr snapshot; while (std::chrono::steady_clock::now() < deadline) { snapshot = state->published_snapshot.load(std::memory_order_acquire); if (snapshot && !state->worker_active.load(std::memory_order_acquire)) break; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } state->cancelled.store(true, std::memory_order_release); ASSERT_TRUE(snapshot); EXPECT_FALSE(snapshot->lines.empty()); EXPECT_TRUE(state->aggregate.has_frame); EXPECT_EQ(state->aggregate.latest.frame_id, 7); } TEST(Renderive_Performance_Overlay, LowLatencyDiagnosticsExposePresentFeedbackFields) { Performance_Overlay shower; shower.set_enabled(true); shower.set_viewport(Size{360, 180}, 1); auto frame = performance_frame(10, 360, 180); frame->refresh.production_mode = Low_Latency_Production_Mode::Consumer_Paced; frame->refresh.limit_reason = Low_Latency_Limit_Reason::Consumer_Capacity; frame->refresh.consumer_confidence = Consumer_Confidence::High; frame->refresh.consumer_sample_accepted_count = 8; frame->refresh.consumer_sample_total_count = 10; frame->refresh.consumer_sample_accepted_ratio = 0.8; frame->refresh.update_to_paint_begin_ns = 1200000; frame->refresh.paint_service_ns = 800000; frame->refresh.producer_starvation_count = 2; frame->refresh.middle_superseded_count = 3; frame->refresh.update_posted_count = 4; frame->refresh.duplicate_present_request_suppressed_count = 1; frame->refresh.update_ack_count = 3; frame->refresh.update_repost_count = 1; shower.collect_frame(frame); ASSERT_TRUE(wait_until([&]() { auto snapshot = shower.display_snapshot(); return snapshot && !snapshot->lines.empty(); })); auto snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); std::string text; for (const auto& line : snapshot->lines) text += line + "\n"; EXPECT_NE(text.find("limit_reason:consumer_capacity"), std::string::npos); EXPECT_NE(text.find("consumer_confidence:high"), std::string::npos); EXPECT_NE(text.find("update_to_paint"), std::string::npos); EXPECT_NE(text.find("producer_starvation"), std::string::npos); EXPECT_NE(text.find("middle_superseded"), std::string::npos); EXPECT_NE(text.find("duplicate_suppressed:"), std::string::npos); } TEST(Renderive_Performance_Overlay, PerformanceLogDisabledWritesNothing) { performance_log_service().shutdown(); auto directory = unique_test_directory("perf_log_disabled"); Performance_Overlay shower; Performance_Overlay_Options options; options.log.enabled = false; options.log.directory = directory.string(); options.log.session_name = "disabled"; shower.set_options(options); shower.set_enabled(false); shower.collect_frame(performance_frame(1)); performance_log_service().shutdown(); EXPECT_FALSE(std::filesystem::exists(directory / "disabled_frames.log")); EXPECT_FALSE(std::filesystem::exists(directory / "disabled_feedback.log")); EXPECT_FALSE(std::filesystem::exists(directory / "disabled_meta.json")); std::filesystem::remove_all(directory); } TEST(Renderive_Performance_Overlay, PerformanceLogWorksWhenOverlayHidden) { performance_log_service().shutdown(); auto directory = unique_test_directory("perf_log_hidden"); Performance_Overlay shower; Performance_Overlay_Options options; options.log.enabled = true; options.log.directory = directory.string(); options.log.session_name = "hidden"; options.log.flush_interval_ms = 1; shower.set_log_identity(Performance_Log_Plot_Identity{42, "hidden,plot\"a"}); shower.set_options(options); shower.set_enabled(false); auto frame = performance_frame(7); frame->dirty_reason_mask = dirty_reason_bit(Dirty_Reason::Core_Data); frame->refresh.update_posted_count = 1; frame->refresh.consumer_window_size = 1; frame->refresh.consumer_window_accepted = 1; frame->refresh.consumer_window_accepted_ratio = 1.0; shower.collect_frame(frame); auto event = std::make_shared(feedback_event("InputChanged", *frame, 1000)); shower.collect_feedback_event(event); performance_log_service().shutdown(); auto frame_records = read_log_records(directory / "hidden_frames.log", "frame"); auto feedback_records = read_log_records(directory / "hidden_feedback.log", "feedback"); ASSERT_EQ(frame_records.size(), 1u); ASSERT_EQ(feedback_records.size(), 1u); const auto& frame_record = frame_records.front(); EXPECT_EQ(frame_record.at("schema_version"), "3"); EXPECT_EQ(frame_record.at("plot_id"), "42"); EXPECT_EQ(frame_record.at("plot_name"), "hidden,plot\"a"); EXPECT_TRUE(frame_record.contains("render_slot_idle_wait_ns")); EXPECT_TRUE(frame_record.contains("executor_queue_wait_current_ns")); EXPECT_TRUE(frame_record.contains("executor_started_topologies")); EXPECT_TRUE(frame_record.contains("executor_completed_topologies")); EXPECT_TRUE(frame_record.contains("render_request_to_start_ns")); EXPECT_TRUE(frame_record.contains("duplicate_present_request_suppressed")); EXPECT_TRUE(std::filesystem::exists(directory / "hidden_meta.json")); std::filesystem::remove_all(directory); } TEST(Renderive_Performance_Overlay, PerformanceLogSeparatesMultiplePlots) { performance_log_service().shutdown(); auto directory = unique_test_directory("perf_log_multi_plot"); Performance_Overlay_Options options; options.log.enabled = true; options.log.directory = directory.string(); options.log.session_name = "multi"; options.log.flush_interval_ms = 1; Performance_Overlay first; Performance_Overlay second; first.set_log_identity(Performance_Log_Plot_Identity{11, "spectrum"}); second.set_log_identity(Performance_Log_Plot_Identity{12, "waterfall"}); first.set_options(options); second.set_options(options); first.set_enabled(false); second.set_enabled(false); first.collect_frame(performance_frame(1)); second.collect_frame(performance_frame(2)); performance_log_service().shutdown(); auto records = read_log_records(directory / "multi_frames.log", "frame"); ASSERT_EQ(records.size(), 2u); bool saw_spectrum = false; bool saw_waterfall = false; for (const auto& record : records) { saw_spectrum = saw_spectrum || (record.at("plot_id") == "11" && record.at("plot_name") == "spectrum"); saw_waterfall = saw_waterfall || (record.at("plot_id") == "12" && record.at("plot_name") == "waterfall"); } EXPECT_TRUE(saw_spectrum); EXPECT_TRUE(saw_waterfall); std::filesystem::remove_all(directory); } TEST(Renderive_Performance_Overlay, FeedbackEventLogUsesRuntimeEventRecord) { performance_log_service().shutdown(); auto directory = unique_test_directory("perf_feedback_event"); Performance_Overlay shower; Performance_Overlay_Options options; options.log.enabled = true; options.log.directory = directory.string(); options.log.session_name = "feedback"; options.log.flush_interval_ms = 1; shower.set_log_identity(Performance_Log_Plot_Identity{91, "event_plot"}); shower.set_options(options); shower.set_enabled(false); auto frame = performance_frame(9); frame->refresh.production_mode = Low_Latency_Production_Mode::Consumer_Paced; frame->refresh.effective_min_render_interval_ns = 33333333; auto event = std::make_shared(feedback_event("ProducerStarvationEnter", *frame, 2000)); event->mode_before = Low_Latency_Production_Mode::Latency_Eager; event->mode_after = Low_Latency_Production_Mode::Consumer_Paced; event->effective_interval_before_ns = 16666667; event->effective_interval_after_ns = 33333333; event->dirty = true; event->producer_starved = true; shower.collect_feedback_event(event); performance_log_service().shutdown(); auto records = read_log_records(directory / "feedback_feedback.log", "feedback"); ASSERT_EQ(records.size(), 1u); const auto& record = records.front(); EXPECT_EQ(record.at("event"), "ProducerStarvationEnter"); EXPECT_EQ(record.at("mode_before"), "latency_eager"); EXPECT_EQ(record.at("mode_after"), "consumer_paced"); EXPECT_EQ(record.at("dirty"), "1"); EXPECT_EQ(record.at("producer_starved"), "1"); EXPECT_EQ(record.at("effective_interval_before_ns"), "16666667"); EXPECT_EQ(record.at("effective_interval_after_ns"), "33333333"); std::filesystem::remove_all(directory); } TEST(Renderive_Performance_Overlay, SummaryUsesSingleColumnAndTextRoles) { Performance_Overlay shower; Performance_Overlay_Options options; options.section_color = Color{10, 20, 30, 255}; options.label_color = Color{40, 50, 60, 255}; options.value_color = Color{70, 80, 90, 255}; options.warning_color = Color{200, 20, 20, 255}; shower.set_options(options); shower.set_enabled(true); shower.set_viewport(Size{1280, 260}, 1); auto frame = performance_frame(1, 1280, 260); frame->refresh.producer_starvation_active = true; frame->refresh.producer_starvation_count = 1; frame->refresh.consumer_confidence = Consumer_Confidence::Low; frame->refresh.production_mode = Low_Latency_Production_Mode::Consumer_Paced; shower.collect_frame(frame); ASSERT_TRUE(wait_until([&]() { auto snapshot = shower.display_snapshot(); return snapshot && !snapshot->text_runs.empty(); })); auto snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); bool has_section = false; bool has_label = false; bool has_value = false; bool has_warning = false; for (const auto& run : snapshot->text_runs) { EXPECT_EQ(run.column_index, 0); has_section = has_section || run.role == Performance_Text_Role::Section; has_label = has_label || run.role == Performance_Text_Role::Label; has_value = has_value || run.role == Performance_Text_Role::Value; has_warning = has_warning || run.role == Performance_Text_Role::Warning; } EXPECT_TRUE(has_section); EXPECT_TRUE(has_label); EXPECT_TRUE(has_value); EXPECT_TRUE(has_warning); } TEST(Renderive_Performance_Overlay, LayoutFallsBackToOneColumn) { Performance_Overlay shower; shower.set_enabled(true); shower.set_viewport(Size{260, 220}, 1); shower.collect_frame(performance_frame(1, 260, 220)); ASSERT_TRUE(wait_until([&]() { auto snapshot = shower.display_snapshot(); return snapshot && !snapshot->text_runs.empty(); })); auto snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); for (const auto& run : snapshot->text_runs) EXPECT_EQ(run.column_index, 0); } TEST(Renderive_Performance_Shower, OptionsAffectSnapshotRendering) { Performance_Overlay shower; Performance_Overlay_Options options; options.font.size = 18.0; options.background = Color{10, 20, 30, 255}; options.foreground = Color{240, 240, 240, 255}; shower.set_options(options); shower.set_enabled(true); shower.set_viewport(Size{260, 120}, 1); shower.collect_frame(performance_frame(1, 260, 120)); ASSERT_TRUE(wait_until([&]() { auto snapshot = shower.display_snapshot(); return snapshot && !snapshot->image.empty(); })); auto snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); ASSERT_FALSE(snapshot->image.empty()); EXPECT_EQ(shower.options().font.size, 18.0); EXPECT_GT(snapshot->line_height, 12.0); EXPECT_EQ(premultiplied_to_color(snapshot->image.row(1)[1]), options.background); } TEST(Renderive_Performance_Shower, OutcomeWidthIsSeededWithLongestEnumText) { auto state = std::make_shared(); state->enabled.store(true, std::memory_order_release); auto outcome_width = [&]() { return state->field_layout_states[static_cast(Performance_Metric_Id::Outcome)].max_value_width; }; auto presented = performance_frame(1, 260, 120); presented->outcome = Frame_Outcome::Presented; ASSERT_TRUE(state->metric_queue.try_push(Performance_Frame_Record{presented})); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { return state->aggregate.latest.frame_id == 1 && !state->worker_active.load(std::memory_order_acquire); })); double seeded_width = outcome_width(); EXPECT_GT(seeded_width, 0.0); auto dropped = performance_frame(2, 260, 120); dropped->outcome = Frame_Outcome::Worker_Budget_Dropped; ASSERT_TRUE(state->metric_queue.try_push(Performance_Frame_Record{dropped})); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { return state->aggregate.latest.frame_id == 2 && !state->worker_active.load(std::memory_order_acquire); })); EXPECT_EQ(outcome_width(), seeded_width); state->cancelled.store(true, std::memory_order_release); } TEST(Renderive_Performance_Shower, NumericFieldWidthIsMonotonicAcrossResetAndResize) { auto state = std::make_shared(); state->enabled.store(true, std::memory_order_release); auto frame_width = [&]() { return state->field_layout_states[static_cast(Performance_Metric_Id::Frame_Id)].max_value_width; }; double previous = 0.0; double wide = 0.0; for (std::uint64_t frame_id : {1ull, 99ull, 123456ull, 2ull, 8ull}) { Performance_Frame_Record record = performance_frame(frame_id, 320, 120); ASSERT_TRUE(state->metric_queue.try_push(record)); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { auto snapshot = state->published_snapshot.load(std::memory_order_acquire); return snapshot && state->aggregate.latest.frame_id == frame_id && !state->worker_active.load(std::memory_order_acquire); })); double current = frame_width(); EXPECT_GE(current, previous); previous = current; if (frame_id == 123456) wide = current; if (frame_id == 2 || frame_id == 8) EXPECT_EQ(current, wide); } state->reset_requested.store(true, std::memory_order_release); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { return !state->worker_active.load(std::memory_order_acquire); })); EXPECT_EQ(frame_width(), wide); state->viewport_width.store(180, std::memory_order_release); state->viewport_height.store(90, std::memory_order_release); state->viewport_version.store(2, std::memory_order_release); state->snapshot_rebuild_requested.store(true, std::memory_order_release); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { auto snapshot = state->published_snapshot.load(std::memory_order_acquire); return snapshot && snapshot->viewport_version == 2 && !state->worker_active.load(std::memory_order_acquire); })); EXPECT_EQ(frame_width(), wide); state->cancelled.store(true, std::memory_order_release); } TEST(Renderive_Performance_Shower, ScrollCommandsUpdateOffsetAndClamp) { Performance_Overlay shower; shower.set_enabled(true); shower.set_viewport(Size{220, 70}, 1); auto frame = performance_frame(1, 220, 70); shower.collect_frame(frame); ASSERT_TRUE(wait_until([&]() { auto snapshot = shower.display_snapshot(); return snapshot && snapshot->max_scroll_offset > 0.0; })); auto snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); EXPECT_TRUE(shower.handle_wheel(PointF{5.0, 5.0}, -120.0, 0.0)); ASSERT_TRUE(wait_until([&]() { auto next = shower.display_snapshot(); return next && next->scroll_offset > snapshot->scroll_offset; })); snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); PointF page_down{snapshot->scroll_track_rect.x + 1.0, snapshot->scroll_thumb_rect.bottom() + 1.0}; EXPECT_TRUE(shower.handle_pointer_press(page_down)); ASSERT_TRUE(wait_until([&]() { auto next = shower.display_snapshot(); return next && next->scroll_offset > snapshot->scroll_offset; })); snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); PointF thumb_center{snapshot->scroll_thumb_rect.x + 1.0, snapshot->scroll_thumb_rect.y + snapshot->scroll_thumb_rect.height * 0.5}; EXPECT_TRUE(shower.handle_pointer_press(thumb_center)); EXPECT_TRUE(shower.handle_pointer_move(PointF{thumb_center.x, snapshot->scroll_track_rect.bottom()})); EXPECT_TRUE(shower.handle_pointer_release(PointF{thumb_center.x, snapshot->scroll_track_rect.bottom()})); ASSERT_TRUE(wait_until([&]() { auto next = shower.display_snapshot(); return next && next->max_scroll_offset > 0.0 && next->scroll_offset > next->max_scroll_offset * 0.8; })); } TEST(Renderive_Performance_Shower, Utf8WrappingKeepsValidText) { Performance_Overlay shower; shower.set_enabled(true); shower.set_viewport(Size{120, 90}, 1); auto frame = performance_frame(1, 120, 90); Renderable_Frame_Stat stat; stat.object_name = "\xE4\xB8\xAD\xE6\x96\x87\xE6\xB5\x8B\xE8\xAF\x95\xE5\xAD\x97\xE6\xAE\xB5\xE5\xBE\x88\xE9\x95\xBF\xE5\xBE\x88\xE9\x95\xBF"; stat.tree_depth = 0; stat.render_count = 1; stat.total_render_time_ns = 1000000; frame->renderable_stats.push_back(stat); shower.collect_frame(frame); ASSERT_TRUE(wait_until([&]() { auto snapshot = shower.display_snapshot(); return snapshot && !snapshot->lines.empty(); })); auto snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); for (const auto& line : snapshot->lines) EXPECT_TRUE(valid_utf8(line)) << line; } } // namespace renderive int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }