2676 lines
116 KiB
C++
2676 lines
116 KiB
C++
#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 <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <gtest/gtest.h>
|
|
#include <initializer_list>
|
|
#include <latch>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <rigtorp/MPMCQueue.h>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <variant>
|
|
#include <vector>
|
|
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<bool, 3> 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<std::size_t>(count));
|
|
return timeline;
|
|
}
|
|
void prepare_frequency_trace(Frequency_Trace_Private& trace, std::initializer_list<int> ticks, const Timeline_Stream_Snapshot& timeline) {
|
|
trace.resize_power_buffer(static_cast<int>(ticks.size()));
|
|
for (int tick : ticks)
|
|
trace.push_power(Frequency_Sample{tick, static_cast<double>(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<bool>(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<bool>(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<bool>(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<bool()> 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<Frame_Lifecycle_Record> performance_frame(std::uint64_t frame_id, int width = 240, int height = 80) {
|
|
auto frame = std::make_shared<Frame_Lifecycle_Record>();
|
|
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<std::uint64_t>(width);
|
|
frame->pixel_height = static_cast<std::uint64_t>(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<unsigned char>(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<unsigned char>(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::nanoseconds>(
|
|
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<std::string> read_lines(const std::filesystem::path& path) {
|
|
std::ifstream input(path);
|
|
std::vector<std::string> lines;
|
|
std::string line;
|
|
while (std::getline(input, line))
|
|
lines.push_back(line);
|
|
return lines;
|
|
}
|
|
using Parsed_Log_Record = std::map<std::string, std::string>;
|
|
std::vector<Parsed_Log_Record> read_log_records(const std::filesystem::path& path, std::string_view kind) {
|
|
std::vector<Parsed_Log_Record> 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<Lease_Test_State>();
|
|
}
|
|
Render_Object_Owner create_input_data() override {
|
|
return make_render_object<Lease_Test_Input>();
|
|
}
|
|
};
|
|
struct Lease_Test_Data : Typed_Render_Data<Lease_Test_Owner, Lease_Test_State, Lease_Test_Input> {
|
|
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<No_Input_Test_Owner, Lease_Test_State, Input_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<Lease_Test_State>();
|
|
}
|
|
Render_Object_Owner create_input_data() override {
|
|
++input_create_count;
|
|
return make_render_object<Lease_Test_Input>();
|
|
}
|
|
};
|
|
struct Runtime_Count_Data : Typed_Render_Data<Runtime_Count_Owner, Lease_Test_State, Lease_Test_Input> {};
|
|
struct Frame_View_Blocking_Renderable;
|
|
struct Frame_View_Blocking_Data : Typed_Render_Data<Frame_View_Blocking_Renderable, Lease_Test_State, Lease_Test_Input> {};
|
|
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<Lease_Test_State>();
|
|
}
|
|
Render_Object_Owner create_input_data() override {
|
|
return make_render_object<Counting_Test_Input>();
|
|
}
|
|
};
|
|
struct Counting_Input_Data : Typed_Render_Data<Counting_Input_Owner, Lease_Test_State, Counting_Test_Input> {};
|
|
|
|
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<std::string> 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<const Frame_Feedback_Event_Record> 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<bool>(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<bool>(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<bool>(held));
|
|
auto* state = static_cast<Lease_Test_State*>(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<No_Input_Test_Owner, No_Input_Test_Data, Lease_Test_State, Input_Data>;
|
|
using Custom_Input_Binding = Renderable_Binding<Lease_Test_Owner, Lease_Test_Data, Lease_Test_State, Lease_Test_Input>;
|
|
EXPECT_FALSE(static_cast<bool>(No_Input_Binding::create_input_data()));
|
|
EXPECT_TRUE(static_cast<bool>(Custom_Input_Binding::create_input_data()));
|
|
|
|
No_Input_Test_Data data;
|
|
Render_Input_Lease<No_Input_Test_Data, Input_Data> lease(&data);
|
|
EXPECT_FALSE(static_cast<bool>(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<Runtime_Count_Data, Lease_Test_State> edit(&explicit_data);
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(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<bool>(edit));
|
|
edit.set_completion(completion);
|
|
}
|
|
|
|
auto frame_view = data.capture_frame_view();
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(edit));
|
|
edit.set_completion(completion);
|
|
}
|
|
|
|
auto frame_view = data.capture_frame_view();
|
|
ASSERT_TRUE(static_cast<bool>(frame_view));
|
|
auto* input = static_cast<Counting_Test_Input*>(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<std::shared_ptr<Update_State>> 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<bool>(frame_view));
|
|
auto* prepare_state = static_cast<Lease_Test_State*>(frame_view.state);
|
|
auto* draw_state = static_cast<Lease_Test_State*>(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<bool>(frame_view));
|
|
auto* prepare_input = static_cast<Lease_Test_Input*>(frame_view.input);
|
|
auto* draw_input = static_cast<Lease_Test_Input*>(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>();
|
|
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<bool>(first));
|
|
auto second = data.capture_frame_view();
|
|
EXPECT_FALSE(static_cast<bool>(second));
|
|
first = {};
|
|
auto third = data.capture_frame_view();
|
|
EXPECT_TRUE(static_cast<bool>(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_Latency_Renderable_Runtime*>(low_runtime.get()), nullptr);
|
|
EXPECT_NE(dynamic_cast<Explicit_Renderable_Runtime*>(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<bool>(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<bool>(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<bool>(first.lease));
|
|
EXPECT_TRUE(first.has_new_frame);
|
|
first.lease.reset();
|
|
auto repeated = pipeline.acquire_paint_frame(35);
|
|
EXPECT_TRUE(static_cast<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<Frame_Lifecycle_Record> returned;
|
|
auto first = flow.begin_present(30, returned);
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(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<bool>(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<Frame_Lifecycle_Record> returned;
|
|
auto first = flow.begin_present(30, returned);
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(first.lease));
|
|
first.lease.reset();
|
|
auto repeated = pipeline.acquire_paint_frame(35);
|
|
EXPECT_TRUE(static_cast<bool>(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<bool>(first.lease));
|
|
ASSERT_FALSE(first.lease.frame->image.empty());
|
|
first.lease.reset();
|
|
auto repeated = pipeline.acquire_paint_frame(35);
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(render_lease));
|
|
render_lease.reset();
|
|
auto repeated = pipeline.acquire_paint_frame(40);
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<bool>(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<std::uint64_t>(i));
|
|
ASSERT_TRUE(static_cast<bool>(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<Render_Frame_Snapshot>);
|
|
static_assert(std::is_default_constructible_v<Renderable_Frame_Node>);
|
|
static_assert(std::is_default_constructible_v<Renderable_Frame_Tree>);
|
|
static_assert(std::is_base_of_v<Render_Output, Low_Latency_Frame>);
|
|
static_assert(std::is_move_constructible_v<Present_Surface_Lease>);
|
|
static_assert(!std::is_copy_constructible_v<Present_Surface_Lease>);
|
|
static_assert(!std::is_same_v<Present_Surface_Lease, Low_Latency_Frame_Lease>);
|
|
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<int> 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<double, 5> 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<std::size_t>(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<double, 5> 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<std::size_t>(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<double, 3> 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<std::size_t>(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<double, 6> 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<std::size_t>(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<double, 5> samples{0.0, std::numeric_limits<double>::quiet_NaN(), 1.0, std::numeric_limits<double>::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<std::size_t>(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<double, 5> 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<std::size_t>(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<std::uint64_t>(static_cast<double>(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<double>(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<bool>(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<Low_Latency_Diagnostics>(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<bool>(ticket));
|
|
EXPECT_FALSE(flow.view_active());
|
|
EXPECT_EQ(host.render_attempts, 1);
|
|
|
|
Flow_Diagnostics diagnostics = flow.diagnostics();
|
|
EXPECT_TRUE(std::holds_alternative<Explicit_Diagnostics>(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<std::shared_ptr<Update_State>> states(memory_resource(Memory_Domain::Update_Completion));
|
|
flow.drain_frame_update_states(states);
|
|
|
|
ASSERT_TRUE(static_cast<bool>(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<Low_Latency_Frame_Flow>());
|
|
plot.init();
|
|
plot.set_viewport_size(Size{16, 16});
|
|
auto root = plot.root_renderable();
|
|
ASSERT_TRUE(root);
|
|
auto blocker = std::make_shared<Frame_View_Blocking_Renderable>();
|
|
blocker->init(root);
|
|
auto held_view = blocker->d()->capture_frame_view();
|
|
ASSERT_TRUE(static_cast<bool>(held_view));
|
|
auto blocked_view = blocker->d()->capture_frame_view();
|
|
ASSERT_FALSE(static_cast<bool>(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<Frame_Lifecycle_Record> returned;
|
|
auto lease = flow.begin_present(10, returned);
|
|
EXPECT_FALSE(static_cast<bool>(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<Frame_Lifecycle_Record> returned;
|
|
auto lease = flow.begin_present(30, returned);
|
|
ASSERT_TRUE(static_cast<bool>(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<Frame_Lifecycle_Record> returned;
|
|
auto first = flow.begin_present(30, returned);
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(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<Frame_Lifecycle_Record> returned;
|
|
auto first = flow.begin_present(30, returned);
|
|
ASSERT_TRUE(static_cast<bool>(first));
|
|
auto second = flow.begin_present(31, returned);
|
|
ASSERT_TRUE(static_cast<bool>(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<Frame_Lifecycle_Record> returned;
|
|
auto first = flow.begin_present(30, returned);
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(failed_surface));
|
|
flow.discard_render_frame(std::move(failed_surface));
|
|
|
|
auto repeated = flow.begin_present(40, returned);
|
|
ASSERT_TRUE(static_cast<bool>(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<Frame_Lifecycle_Record> returned;
|
|
auto first = flow.begin_present(30, returned);
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(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<bool>(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<Frame_Lifecycle_Record> returned;
|
|
auto old_lease = flow.begin_present(30, returned);
|
|
ASSERT_TRUE(static_cast<bool>(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<bool>(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<Frame_Worker_Stats>();
|
|
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<Frame_Worker_Stats>();
|
|
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<Frame_Worker_Stats>();
|
|
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<Frame_Worker_Stats>();
|
|
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<Frame_Worker_Stats>();
|
|
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<Frame_Worker_Stats>();
|
|
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<Frame_Worker_Stats>();
|
|
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<Frame_Worker_Stats>(),
|
|
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<int, 3> 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<Frame_Worker_Stats>(),
|
|
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<Frame_Worker_Stats>(),
|
|
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<Frame_Worker_Stats>(),
|
|
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<Frame_Worker_Stats>(),
|
|
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<Frame_Worker_Stats>(),
|
|
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<Low_Latency_Frame_Flow>());
|
|
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<Low_Latency_Frame_Flow>());
|
|
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<Performance_Worker_State>();
|
|
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<Performance_Worker_State>();
|
|
state->enabled.store(true, std::memory_order_release);
|
|
auto frame = std::make_shared<Frame_Lifecycle_Record>();
|
|
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<const Performance_Display_Snapshot> 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<Frame_Feedback_Event_Record>(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<Frame_Feedback_Event_Record>(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<Performance_Worker_State>();
|
|
state->enabled.store(true, std::memory_order_release);
|
|
auto outcome_width = [&]() {
|
|
return state->field_layout_states[static_cast<std::size_t>(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<Performance_Worker_State>();
|
|
state->enabled.store(true, std::memory_order_release);
|
|
auto frame_width = [&]() {
|
|
return state->field_layout_states[static_cast<std::size_t>(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();
|
|
}
|