Files
Renderive/test/Renderive_Core_Tests.cpp
T
2026-08-02 14:20:27 +08:00

1017 lines
40 KiB
C++

#include "Core/architecture/Frame_Scheduler.h"
#include "Core/flow/low_latency/Render_Lease.h"
#include "Core/flow/low_latency/Triple_Buffer.h"
#include "Core/flow/explicit/Explicit_Frame_Flow.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/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/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 <array>
#include <chrono>
#include <cmath>
#include <functional>
#include <gtest/gtest.h>
#include <latch>
#include <limits>
#include <rigtorp/MPMCQueue.h>
#include <thread>
#include <type_traits>
#include <utility>
#include <variant>
namespace renderive {
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;
}
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->paint_begin_ns = 30;
frame->paint_end_ns = 40;
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;
}
struct Lease_Test_Owner : Renderable {};
struct Lease_Test_State : Render_State {
int value{};
};
struct Lease_Test_Input : Input_Data {};
struct Lease_Test_Data : Typed_Render_Data<Lease_Test_Owner, Lease_Test_State, Lease_Test_Input> {};
struct No_Input_Test_Owner : Renderable {};
struct No_Input_Test_Data : Typed_Render_Data<No_Input_Test_Owner, Lease_Test_State, Input_Data> {};
void init_lease_test_state(Lease_Test_Data& data) {
data.clear_state_buffers();
for (std::uint8_t role = 0; role < 3; ++role) {
data.state_buffer_owners[role] = make_render_object<Lease_Test_State>();
data.state_buffers[role] = data.state_buffer_owners[role].get<Render_State>();
}
data.state_buffers[State_Edit]->version = 1;
}
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;
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;
}
};
} // 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, ClearBuffersWaitsForActiveLease) {
Lease_Test_Data data;
init_lease_test_state(data);
auto held = data.state_control.acquire_role(State_Edit);
ASSERT_EQ(held.result, Triple_Buffer_Result::Success);
std::atomic_bool clear_returned{false};
std::latch clear_started(1);
std::thread clearer([&]() {
clear_started.count_down();
data.clear_state_buffers();
clear_returned.store(true, std::memory_order_release);
});
clear_started.wait();
ASSERT_TRUE(wait_until([&]() {
return data.state_control.role_busy(State_Edit);
}));
EXPECT_FALSE(clear_returned.load(std::memory_order_acquire));
data.state_control.unmark_use(held.lease);
clearer.join();
EXPECT_TRUE(clear_returned.load(std::memory_order_acquire));
for (auto* state : data.state_buffers)
EXPECT_EQ(state, nullptr);
}
TEST(Renderive_State, BusyEditSetterWaitsAndCommits) {
Lease_Test_Data data;
init_lease_test_state(data);
auto held = data.state_control.acquire_role(State_Edit);
ASSERT_EQ(held.result, Triple_Buffer_Result::Success);
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));
data.state_control.unmark_use(held.lease);
setter.join();
EXPECT_TRUE(setter_returned.load(std::memory_order_acquire));
EXPECT_EQ(data.edit_state_value(&Lease_Test_State::value), 42);
}
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_Frame_Pipeline, PublishAndPaintUseMiddleVersionOnly) {
Low_Latency_Frame_Pipeline pipeline;
auto lease = pipeline.try_acquire_render_frame();
ASSERT_TRUE(static_cast<bool>(lease));
ASSERT_TRUE(lease.frame->metadata);
lease.frame->metadata->frame_id = 1;
lease.frame->metadata->render_begin_ns = 10;
lease.frame->metadata->render_end_ns = 20;
lease.frame->version.store(1, std::memory_order_release);
auto published = pipeline.try_publish_rendered_frame(std::move(lease), 25);
EXPECT_TRUE(pipeline.middle_frame_ready());
EXPECT_TRUE(published.request_present);
EXPECT_EQ(published.dropped_frame_record, nullptr);
auto painting = pipeline.try_acquire_paint_frame(30);
EXPECT_TRUE(painting.has_new_frame);
EXPECT_TRUE(static_cast<bool>(painting.lease));
EXPECT_FALSE(pipeline.middle_frame_ready());
}
TEST(Renderive_Frame_Pipeline, PaintAcquireFailureKeepsPendingRequest) {
Low_Latency_Frame_Pipeline pipeline;
auto lease = pipeline.try_acquire_render_frame();
ASSERT_TRUE(static_cast<bool>(lease));
ASSERT_TRUE(lease.frame->metadata);
lease.frame->metadata->frame_id = 1;
lease.frame->metadata->render_begin_ns = 10;
lease.frame->metadata->render_end_ns = 20;
lease.frame->version.store(1, std::memory_order_release);
auto published = pipeline.try_publish_rendered_frame(std::move(lease), 25);
ASSERT_TRUE(published.request_present);
EXPECT_TRUE(pipeline.paint_request_pending.load(std::memory_order_acquire));
auto busy = pipeline.frame_control.try_acquire_role(Frame_Painting);
ASSERT_EQ(busy.result, Triple_Buffer_Result::Success);
auto failed = pipeline.try_acquire_paint_frame(30);
EXPECT_TRUE(failed.paint_frame_unavailable);
EXPECT_FALSE(failed.present_request_was_pending);
EXPECT_TRUE(pipeline.paint_request_pending.load(std::memory_order_acquire));
pipeline.frame_control.unmark_use(busy.lease);
auto consumed = pipeline.try_acquire_paint_frame(35);
EXPECT_TRUE(consumed.present_request_was_pending);
EXPECT_TRUE(static_cast<bool>(consumed.lease));
}
TEST(Renderive_Frame_Pipeline, RequestPresentDoesNotWriteBusyMiddle) {
Low_Latency_Frame_Pipeline pipeline;
auto lease = pipeline.try_acquire_render_frame();
ASSERT_TRUE(static_cast<bool>(lease));
ASSERT_TRUE(lease.frame->metadata);
lease.frame->metadata->frame_id = 1;
lease.frame->metadata->render_begin_ns = 10;
lease.frame->metadata->render_end_ns = 20;
lease.frame->version.store(1, std::memory_order_release);
auto published = pipeline.try_publish_rendered_frame(std::move(lease), 25);
ASSERT_TRUE(published.request_present);
pipeline.paint_request_pending.store(false, std::memory_order_release);
auto busy_middle = pipeline.frame_control.try_acquire_role(Frame_Middle);
ASSERT_EQ(busy_middle.result, Triple_Buffer_Result::Success);
auto blocked = pipeline.request_present(40);
EXPECT_FALSE(blocked.request_present);
EXPECT_FALSE(pipeline.paint_request_pending.load(std::memory_order_acquire));
pipeline.frame_control.unmark_use(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>);
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(), 3u);
EXPECT_DOUBLE_EQ(points.front().y, 0.0);
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, SelectsRenderPaintAndUserSources) {
Low_Latency_Feedback_Controller controller;
controller.set_max_render_fps(200.0);
for (int i = 0; i < 4; ++i) {
auto render_limited = render_frame(0, 20000000);
controller.on_render_completed(render_limited);
}
EXPECT_EQ(controller.state().source, Refresh_Limit_Source::Render);
controller.reset();
controller.set_max_render_fps(200.0);
for (int i = 0; i < 4; ++i) {
auto frame = render_frame(0, 5000000);
controller.on_render_completed(frame);
frame.transition_12_ns = 0;
frame.paint_begin_ns = 0;
frame.paint_end_ns = 25000000;
controller.on_frame_presented(frame);
}
EXPECT_EQ(controller.state().source, Refresh_Limit_Source::Paint);
controller.reset();
controller.set_max_render_fps(30.0);
for (int i = 0; i < 4; ++i) {
auto frame = render_frame(0, 5000000);
controller.on_render_completed(frame);
frame.paint_begin_ns = 0;
frame.paint_end_ns = 8000000;
controller.on_frame_presented(frame);
}
EXPECT_EQ(controller.state().source, Refresh_Limit_Source::User);
}
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_Render_Executor, FrameAdmissionIsBounded) {
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,
1,
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,
1,
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,
1,
true
}));
auto duplicate_stat = std::make_shared<Frame_Worker_Stats>();
EXPECT_FALSE(executor.try_submit(Render_Executor_Task{
2,
2,
Render_Task_Kind::Frame,
Task([]() {}),
{},
Task([]() {}),
duplicate_stat,
1,
true
}));
auto overflow_stat = std::make_shared<Frame_Worker_Stats>();
EXPECT_FALSE(executor.try_submit(Render_Executor_Task{
4,
1,
Render_Task_Kind::Frame,
Task([]() {}),
{},
Task([]() {}),
overflow_stat,
1,
true
}));
release.store(true, std::memory_order_release);
ASSERT_TRUE(wait_until([&]() {
return completed.load(std::memory_order_acquire) == 3;
}));
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](tf::Taskflow& taskflow, tf::Task entry, tf::Task exit) {
auto prepare = taskflow.emplace([&sequence]() {
int expected = 0;
sequence.compare_exchange_strong(expected, 1, std::memory_order_acq_rel);
});
auto draw = taskflow.emplace([&sequence]() {
int expected = 1;
sequence.compare_exchange_strong(expected, 2, std::memory_order_acq_rel);
});
auto finalize = taskflow.emplace([&sequence]() {
int expected = 2;
sequence.compare_exchange_strong(expected, 3, std::memory_order_acq_rel);
});
entry.precede(prepare);
prepare.precede(draw);
draw.precede(finalize);
finalize.precede(exit);
},
Task([&completed]() {
completed.store(true, std::memory_order_release);
}),
stat,
3,
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, QueuedFramesRunRoundRobinByPlot) {
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>(),
1,
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>(),
1,
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>(),
1,
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[0], 1);
EXPECT_EQ(order[1], 2);
EXPECT_EQ(order[2], 3);
executor.shutdown();
}
TEST(Renderive_Render_Executor, ShutdownCompletesAcceptedQueuedFrameAsCancelled) {
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>(),
1,
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>(),
1,
true
}));
std::thread shutdown_thread([&]() {
executor.shutdown();
});
ASSERT_TRUE(wait_until([&]() {
return queued_completed.load(std::memory_order_acquire);
}));
EXPECT_FALSE(queued_work_ran.load(std::memory_order_acquire));
release_first.store(true, std::memory_order_release);
shutdown_thread.join();
}
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, 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_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();
}