#include #include #include #include #include #include #include #include #include "../Renderive/Axis/Axis.h" #include "../Renderive/Axis/Frequency_Axis.h" #include "../Renderive/Axis/Time_Axis.h" #include "../Renderive/plot/Latency_Eager_Plot.h" #include "../Renderive/architecture/Timeline_Stream.h" #include "../Renderive/base/Memory.h" #include "../Renderive/base/Frame_Memory.h" #include "../Renderive/architecture/Frame_Raster_Context.h" #include "../Renderive/primitive/Curve_Utils_p.h" #include "../Renderive/plottable/Audio_Frequency.h" #include "../Renderive/plottable/Planisphere.h" #include "../Renderive/plottable/Waterfall.h" #include "internal/Renderive_Private_Access.h" namespace { void process_events(int milliseconds) { QElapsedTimer timer; timer.start(); while (timer.elapsed() < milliseconds) { QApplication::processEvents(QEventLoop::AllEvents, 10); QThread::msleep(1); } } std::vector make_frequency_data(int size, int frame) { std::vector data(size); for (int i = 0; i < size; ++i) data[i] = -90.0 + 30.0 * std::sin(static_cast(i + frame) * 0.017); return data; } bool wait_ready(const std::shared_ptr& waterfall, const std::shared_ptr& time_axis, const std::shared_ptr& audio, const std::shared_ptr& planisphere) { QElapsedTimer timer; timer.start(); while (timer.elapsed() < 3000) { QApplication::processEvents(QEventLoop::AllEvents, 10); if (waterfall->ok() && time_axis->ok() && audio->ok() && planisphere->ok()) return true; QThread::msleep(1); } return false; } bool wait_ready(const std::shared_ptr& waterfall, const std::shared_ptr& time_axis) { QElapsedTimer timer; timer.start(); while (timer.elapsed() < 3000) { QApplication::processEvents(QEventLoop::AllEvents, 10); if (waterfall->ok() && time_axis->ok()) return true; QThread::msleep(1); } return false; } bool wait_idle(renderive::Latency_Eager_Plot& plot) { QElapsedTimer timer; timer.start(); while (timer.elapsed() < 3000) { QApplication::processEvents(QEventLoop::AllEvents, 10); if (!plot.is_rendering() && !renderive::Abs_Plot_Private_Access::active_render_tasks(plot)) return true; QThread::msleep(1); } return false; } void stress_data_paths() { renderive::Latency_Eager_Plot plot; plot.init(); plot.resize(720, 420); auto data_node = plot.create_renderable_node(plot.get_root_renderable(), "Data_Renderable"); auto axis_node = plot.create_renderable_node(plot.get_root_renderable(), "Axis_Renderable"); auto frequency_axis = renderive::Frequency_Axis::Builder(axis_node, Qt::Horizontal).set_pixel_size(700).set_coord_range({0.0, 100.0}).build(); auto time_axis = renderive::Time_Axis::Builder(axis_node, Qt::Vertical).set_pixel_size(400).set_time_point_size(32).build(); auto value_axis = renderive::Axis::Builder(axis_node, Qt::Vertical).set_pixel_size(400).set_coord_range({-120.0, 0.0}).build(); auto i_axis = renderive::Axis::Builder(axis_node, Qt::Horizontal).set_pixel_size(700).set_coord_range({-2.0, 2.0}).build(); auto q_axis = renderive::Axis::Builder(axis_node, Qt::Vertical).set_pixel_size(400).set_coord_range({-2.0, 2.0}).build(); auto waterfall = renderive::Waterfall::Builder(data_node, frequency_axis, time_axis).set_frequency_range({0.0, 100.0}).set_power_range({-120.0, 0.0}).set_frequency_point_size(64).build(); auto audio = renderive::Audio_Frequency::Builder(data_node, time_axis, value_axis).build(); auto planisphere = renderive::Planisphere::Builder(data_node, i_axis, q_axis).set_i_range({-2.0, 2.0}).set_q_range({-2.0, 2.0}).set_continue_millisecond(500).build(); plot.show(); plot.start_render(240); ASSERT_TRUE(wait_ready(waterfall, time_axis, audio, planisphere)); const int frequency_sizes[] = {64, 512, 8192, 8193, 128}; const int time_sizes[] = {32, 96, 48, 64, 24}; const renderive::Waterfall_Row_Update_Policy update_modes[] = { renderive::Waterfall_Row_Update_Policy::All_Pending, renderive::Waterfall_Row_Update_Policy::Latest_Only, renderive::Waterfall_Row_Update_Policy::Rate_Limited_Latest, renderive::Waterfall_Row_Update_Policy::Latest_Only, renderive::Waterfall_Row_Update_Policy::All_Pending }; for (int stage = 0; stage < 5; ++stage) { waterfall->set_frequency_point_size(frequency_sizes[stage]); waterfall->set_row_update_policy(update_modes[stage]); waterfall->set_latest_row_min_interval_ms(2); time_axis->set_time_point_size(time_sizes[stage]); process_events(100); std::unordered_map expected_frequency; QTime expected_time; double expected_power{}; QPointF expected_position; for (int frame = 0; frame < time_sizes[stage] * 2; ++frame) { std::vector frequency_data = make_frequency_data(frequency_sizes[stage], frame); expected_time = QTime::fromMSecsSinceStartOfDay((stage * 10000 + frame * 4) % 86400000); int tick = time_axis->timeline_stream()->push_time(expected_time); waterfall->give_data(tick, frequency_data); expected_frequency[tick] = frequency_data.front(); expected_power = frequency_data[frame % frequency_data.size()]; audio->give_data(tick, expected_power); double phase = static_cast(frame) * 0.13; expected_position = {1.5 * std::cos(phase), 1.5 * std::sin(phase)}; planisphere->give_data(expected_position); if ((frame & 3) == 3) process_events(8); } process_events(60); plot.pause_render(); ASSERT_TRUE(wait_idle(plot)); auto* waterfall_data = renderive::Waterfall_Private_Access::data(waterfall.get()); ASSERT_EQ(waterfall_data->ring_buffer.col_count, frequency_sizes[stage]); ASSERT_EQ(waterfall_data->ring_buffer.row_count, time_sizes[stage]); ASSERT_EQ(waterfall_data->raster_cache.image.width(), 700); ASSERT_EQ(waterfall_data->raster_cache.image.height(), time_sizes[stage]); int row_count = waterfall_data->ring_buffer.snapshot(); int previous_tick{}; for (int row = 0; row < row_count; ++row) { int tick = waterfall_data->ring_buffer.tick(row); auto expected = expected_frequency.find(tick); ASSERT_NE(expected, expected_frequency.end()); EXPECT_NEAR(waterfall_data->ring_buffer.frequency_data(row, 0, 0)[0], expected->second, 0.000001); if (row) EXPECT_GT(tick, previous_tick); previous_tick = tick; } auto* time_data = renderive::Time_Axis_Private_Access::data(time_axis.get()); ASSERT_TRUE(time_data->timeline_snapshot); ASSERT_EQ(time_data->timeline_snapshot->time_point_size, time_sizes[stage]); ASSERT_EQ(time_data->timeline_snapshot->times.size(), static_cast(time_sizes[stage])); ASSERT_FALSE(time_data->timeline_snapshot->times.empty()); EXPECT_EQ(time_data->timeline_snapshot->times.back(), expected_time); int lower = qMin(time_data->timeline_snapshot->lower, time_data->timeline_snapshot->upper()); int upper = qMax(time_data->timeline_snapshot->lower, time_data->timeline_snapshot->upper()); ASSERT_EQ(time_data->ticks.size(), time_data->timeline_snapshot->ticks.size()); for (const renderive::Timeline_Tick& tick : time_data->timeline_snapshot->ticks) { EXPECT_GE(tick.tick, lower); EXPECT_LE(tick.tick, upper); } auto* audio_data = renderive::Audio_Frequency_Private_Access::data(audio.get()); ASSERT_EQ(audio_data->point_count, time_sizes[stage]); ASSERT_EQ(audio_data->history.size(), static_cast(time_sizes[stage])); ASSERT_NE(audio_data->data_count, 0); EXPECT_NEAR(audio_data->history_value(audio_data->data_count - 1).power, expected_power, 0.000001); auto* planisphere_data = renderive::Planisphere_Private_Access::data(planisphere.get()); ASSERT_FALSE(planisphere_data->data_list.empty()); QPointF actual_position = planisphere_data->data_list.back().pos; EXPECT_NEAR(actual_position.x(), expected_position.x(), 0.000001); EXPECT_NEAR(actual_position.y(), expected_position.y(), 0.000001); plot.start_render(240); } for (int frame = 0; frame < 180; ++frame) { planisphere->set_continue_millisecond(500 + (frame & 1)); process_events(8); } plot.pause_render(); ASSERT_TRUE(wait_idle(plot)); EXPECT_TRUE(renderive::Planisphere_Private_Access::data(planisphere.get())->data_list.empty()); renderive::Memory_Stats stats = renderive::memory_stats(); EXPECT_GT(stats.upstream_allocation_count, 0); EXPECT_GT(stats.upstream_peak_bytes, 0); } } TEST(Memory_Data_Path_Stress, KeepsRingAndRenderableDataConsistentAcrossResizes) { stress_data_paths(); } TEST(Memory_Data_Path_Stress, WaterfallWritesNewestTimelineRowAtBottom) { renderive::Latency_Eager_Plot plot; plot.init(); plot.resize(1, 4); auto data_node = plot.create_renderable_node(plot.get_root_renderable(), "Data_Renderable"); auto axis_node = plot.create_renderable_node(plot.get_root_renderable(), "Axis_Renderable"); auto frequency_axis = renderive::Frequency_Axis::Builder(axis_node, Qt::Horizontal).set_pixel_size(1).set_coord_range({0.0, 1.0}).build(); auto time_axis = renderive::Time_Axis::Builder(axis_node, Qt::Vertical).set_pixel_size(4).set_time_point_size(4).build(); renderive::Color_Map color_map({qRgb(10, 0, 0), qRgb(20, 0, 0), qRgb(30, 0, 0), qRgb(40, 0, 0)}); auto waterfall = renderive::Waterfall::Builder(data_node, frequency_axis, time_axis).set_frequency_range({0.0, 1.0}).set_power_range({0.0, 4.0}).set_frequency_point_size(1).set_color_map(color_map).build(); plot.show(); plot.start_render(240); ASSERT_TRUE(wait_ready(waterfall, time_axis)); for (int frame = 0; frame < 4; ++frame) { std::pmr::vector row(renderive::memory_resource(renderive::Memory_Domain::Waterfall)); row.resize(1); row[0] = static_cast(frame); waterfall->give_data(QTime::fromMSecsSinceStartOfDay(frame), std::move(row)); } process_events(100); plot.pause_render(); ASSERT_TRUE(wait_idle(plot)); auto* data = renderive::Waterfall_Private_Access::data(waterfall.get()); ASSERT_EQ(data->raster_cache.image.width(), 1); ASSERT_EQ(data->raster_cache.image.height(), 4); EXPECT_EQ(qRed(data->raster_cache.image.image().pixel(0, 0)), 10); EXPECT_EQ(qRed(data->raster_cache.image.image().pixel(0, 3)), 40); } TEST(Memory_Data_Path_Stress, WaterfallWritesNewestTimelineRowAtTopWhenTimeAxisPushesToStart) { renderive::Latency_Eager_Plot plot; plot.init(); plot.resize(1, 4); auto data_node = plot.create_renderable_node(plot.get_root_renderable(), "Data_Renderable"); auto axis_node = plot.create_renderable_node(plot.get_root_renderable(), "Axis_Renderable"); auto frequency_axis = renderive::Frequency_Axis::Builder(axis_node, Qt::Horizontal).set_pixel_size(1).set_coord_range({0.0, 1.0}).build(); auto time_axis = renderive::Time_Axis::Builder(axis_node, Qt::Vertical).set_pixel_size(4).set_time_point_size(4).set_start_coord_to_end_coord(true).build(); renderive::Color_Map color_map({qRgb(10, 0, 0), qRgb(20, 0, 0), qRgb(30, 0, 0), qRgb(40, 0, 0)}); auto waterfall = renderive::Waterfall::Builder(data_node, frequency_axis, time_axis).set_frequency_range({0.0, 1.0}).set_power_range({0.0, 4.0}).set_frequency_point_size(1).set_color_map(color_map).build(); plot.show(); plot.start_render(240); ASSERT_TRUE(wait_ready(waterfall, time_axis)); for (int frame = 0; frame < 4; ++frame) { std::pmr::vector row(renderive::memory_resource(renderive::Memory_Domain::Waterfall)); row.resize(1); row[0] = static_cast(frame); waterfall->give_data(QTime::fromMSecsSinceStartOfDay(frame), std::move(row)); } process_events(100); plot.pause_render(); ASSERT_TRUE(wait_idle(plot)); auto* data = renderive::Waterfall_Private_Access::data(waterfall.get()); ASSERT_EQ(data->raster_cache.image.width(), 1); ASSERT_EQ(data->raster_cache.image.height(), 4); EXPECT_EQ(qRed(data->raster_cache.image.image().pixel(0, 0)), 40); EXPECT_EQ(qRed(data->raster_cache.image.image().pixel(0, 3)), 10); } TEST(Memory_Data_Path_Stress, DirtyLocalPixelCacheRebuildsAfterInput) { renderive::Latency_Eager_Plot plot; plot.init(); plot.resize(1, 4); auto cache_node = plot.create_renderable_node(plot.get_root_renderable(), "Stream_Cache"); auto data_node = plot.create_renderable_node(cache_node, "Data_Renderable"); auto axis_node = plot.create_renderable_node(plot.get_root_renderable(), "Axis_Renderable"); cache_node->set_cache_mode(renderive::Renderable_Cache_Mode::Local_Pixel); auto frequency_axis = renderive::Frequency_Axis::Builder(axis_node, Qt::Horizontal).set_pixel_size(1).set_coord_range({0.0, 1.0}).build(); auto time_axis = renderive::Time_Axis::Builder(axis_node, Qt::Vertical).set_pixel_size(4).set_time_point_size(4).build(); renderive::Color_Map color_map({qRgb(10, 0, 0), qRgb(20, 0, 0), qRgb(30, 0, 0), qRgb(40, 0, 0)}); auto waterfall = renderive::Waterfall::Builder(data_node, frequency_axis, time_axis).set_frequency_range({0.0, 1.0}).set_power_range({0.0, 4.0}).set_frequency_point_size(1).set_color_map(color_map).build(); plot.show(); plot.start_render(240); ASSERT_TRUE(wait_ready(waterfall, time_axis)); process_events(80); plot.pause_render(); ASSERT_TRUE(wait_idle(plot)); ASSERT_FALSE(renderive::Renderable_Private_Access::cache(*cache_node).isNull()); plot.start_render(240); for (int frame = 0; frame < 4; ++frame) { std::pmr::vector row(renderive::memory_resource(renderive::Memory_Domain::Waterfall)); row.resize(1); row[0] = static_cast(frame); waterfall->give_data(QTime::fromMSecsSinceStartOfDay(frame), std::move(row)); } process_events(100); plot.pause_render(); ASSERT_TRUE(wait_idle(plot)); const auto& cache = renderive::Renderable_Private_Access::cache(*cache_node); ASSERT_EQ(cache.width(), 1); ASSERT_EQ(cache.height(), 4); EXPECT_EQ(qRed(cache.image().pixel(0, 3)), 40); } TEST(Memory_Data_Path_Stress, WaterfallPendingRowsOverwriteInSubmissionOrder) { renderive::Waterfall_Input_Data input; for (int tick = 0; tick < 300; ++tick) { const double value = static_cast(tick); input.push(tick, std::span(&value, 1)); } std::vector ticks; input.read_all([&ticks](int tick, const std::pmr::vector& values) { ticks.push_back(tick); ASSERT_EQ(values.size(), 1u); EXPECT_EQ(values.front(), static_cast(tick)); }); ASSERT_EQ(ticks.size(), renderive::Waterfall_Input_Data::Max_Pending_Row_Count); EXPECT_EQ(ticks.front(), 44); EXPECT_EQ(ticks.back(), 299); } TEST(Memory_Data_Path_Stress, AllPendingUsesIncrementalScanlineMapping) { renderive::Latency_Eager_Plot plot; plot.init(); plot.resize(8, 4); auto data_node = plot.create_renderable_node(plot.get_root_renderable(), "Data_Renderable"); auto axis_node = plot.create_renderable_node(plot.get_root_renderable(), "Axis_Renderable"); auto frequency_axis = renderive::Frequency_Axis::Builder(axis_node, Qt::Horizontal).set_pixel_size(8).set_coord_range({0.0, 8.0}).build(); auto time_axis = renderive::Time_Axis::Builder(axis_node, Qt::Vertical).set_pixel_size(4).set_time_point_size(4).build(); auto waterfall = renderive::Waterfall::Builder(data_node, frequency_axis, time_axis).set_frequency_range({0.0, 8.0}).set_power_range({0.0, 8.0}).set_frequency_point_size(8).set_row_update_policy(renderive::Waterfall_Row_Update_Policy::All_Pending).build(); plot.show(); plot.start_render(240); ASSERT_TRUE(wait_ready(waterfall, time_axis)); std::vector row(8, 1.0); waterfall->give_data(time_axis->timeline_stream()->push_time(QTime(0, 0, 0, 1)), row); process_events(80); auto* data = renderive::Waterfall_Private_Access::data(waterfall.get()); const std::uint64_t rebuild_count = data->raster_cache.full_rebuild_count; row.assign(8, 2.0); waterfall->give_data(time_axis->timeline_stream()->push_time(QTime(0, 0, 0, 2)), row); process_events(80); plot.pause_render(); ASSERT_TRUE(wait_idle(plot)); EXPECT_EQ(data->raster_cache.full_rebuild_count, rebuild_count); EXPECT_GT(data->raster_cache.incremental_row_count, 0u); } TEST(Memory_Data_Path_Stress, WaterfallRasterUsesDisplayHeight) { renderive::Latency_Eager_Plot plot; plot.init(); plot.resize(8, 4); auto data_node = plot.create_renderable_node(plot.get_root_renderable(), "Data_Renderable"); auto axis_node = plot.create_renderable_node(plot.get_root_renderable(), "Axis_Renderable"); auto frequency_axis = renderive::Frequency_Axis::Builder(axis_node, Qt::Horizontal).set_pixel_size(8).set_coord_range({0.0, 8.0}).build(); auto time_axis = renderive::Time_Axis::Builder(axis_node, Qt::Vertical).set_pixel_size(4).set_time_point_size(16).build(); auto waterfall = renderive::Waterfall::Builder(data_node, frequency_axis, time_axis).set_frequency_range({0.0, 8.0}).set_power_range({0.0, 8.0}).set_frequency_point_size(8).build(); plot.show(); plot.start_render(240); ASSERT_TRUE(wait_ready(waterfall, time_axis)); for (int index = 0; index < 16; ++index) { std::vector row(8, static_cast(index)); waterfall->give_data(time_axis->timeline_stream()->push_time(QTime(0, 0, 0, index)), row); } process_events(100); plot.pause_render(); ASSERT_TRUE(wait_idle(plot)); auto* data = renderive::Waterfall_Private_Access::data(waterfall.get()); EXPECT_EQ(data->ring_buffer.row_count, 16); EXPECT_EQ(data->raster_cache.image.height(), 4); } TEST(Memory_Data_Path_Stress, RateLimitedLatestCommitsDeferredTailWithoutNewInput) { renderive::Latency_Eager_Plot plot; plot.init(); plot.resize(8, 4); auto data_node = plot.create_renderable_node(plot.get_root_renderable(), "Data_Renderable"); auto axis_node = plot.create_renderable_node(plot.get_root_renderable(), "Axis_Renderable"); auto frequency_axis = renderive::Frequency_Axis::Builder(axis_node, Qt::Horizontal).set_pixel_size(8).set_coord_range({0.0, 8.0}).build(); auto time_axis = renderive::Time_Axis::Builder(axis_node, Qt::Vertical).set_pixel_size(4).set_time_point_size(4).build(); auto waterfall = renderive::Waterfall::Builder(data_node, frequency_axis, time_axis).set_frequency_range({0.0, 8.0}).set_power_range({0.0, 8.0}).set_frequency_point_size(8).set_row_update_policy(renderive::Waterfall_Row_Update_Policy::Rate_Limited_Latest).set_latest_row_min_interval_ms(100).build(); plot.show(); plot.start_render(240); ASSERT_TRUE(wait_ready(waterfall, time_axis)); std::vector row(8, 1.0); const int first_tick = time_axis->timeline_stream()->push_time(QTime(0, 0, 0, 1)); waterfall->give_data(first_tick, row); process_events(30); row.assign(8, 2.0); const int deferred_tick = time_axis->timeline_stream()->push_time(QTime(0, 0, 0, 2)); waterfall->give_data(deferred_tick, row); process_events(180); plot.pause_render(); ASSERT_TRUE(wait_idle(plot)); auto* data = renderive::Waterfall_Private_Access::data(waterfall.get()); ASSERT_GT(data->ring_buffer.data_count, 0); EXPECT_EQ(data->ring_buffer.tick(data->ring_buffer.data_count - 1), deferred_tick); EXPECT_FALSE(data->deferred_latest.has_value()); } TEST(Memory_Data_Path_Stress, ExplicitMonotonicCurveClipsAndReducesToPixelWidth) { renderive::Frame_Arena arena; std::vector source; source.reserve(10000); for (int index = 0; index < 10000; ++index) source.emplace_back(index, index == 4500 ? 100.0 : 0.0); const renderive::Axis_Transform_Snapshot x_transform{{4000.0, 5000.0}, 0.0, 100.0, Qt::Horizontal}; const renderive::Axis_Transform_Snapshot y_transform{{0.0, 100.0}, 100.0, -100.0, Qt::Vertical}; auto exact = renderive::Curve_Utils::create_explicit_points(source, x_transform, y_transform, true, renderive::Line_Sampling_Mode::Exact); auto reduced = renderive::Curve_Utils::create_explicit_points(source, x_transform, y_transform, true, renderive::Line_Sampling_Mode::Preserve_Extrema); EXPECT_LE(exact.size(), 1003u); EXPECT_LE(reduced.size(), 404u); EXPECT_TRUE(std::any_of(reduced.begin(), reduced.end(), [](const QPointF& point) { return point.y() == 0.0; })); } TEST(Memory_Data_Path_Stress, ExplicitNonMonotonicCurveClipsSegmentsAndBreaksMissingData) { renderive::Frame_Arena arena; const std::vector source{{-2.0, 0.0}, {0.5, 1.0}, {2.0, 0.0}, {std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()}, {2.0, 2.0}, {0.5, 3.0}, {-2.0, 2.0}}; const renderive::Axis_Transform_Snapshot x_transform{{0.0, 1.0}, 0.0, 100.0, Qt::Horizontal}; const renderive::Axis_Transform_Snapshot y_transform{{0.0, 4.0}, 100.0, -100.0, Qt::Vertical}; auto points = renderive::Curve_Utils::create_explicit_points(source, x_transform, y_transform, true, renderive::Line_Sampling_Mode::Preserve_Extrema); bool saw_break = false; for (const QPointF& point : points) { if (!std::isfinite(point.x())) { saw_break = true; continue; } EXPECT_GE(point.x(), 0.0); EXPECT_LE(point.x(), 100.0); } EXPECT_TRUE(saw_break); } TEST(Memory_Data_Path_Stress, FrameRasterContextPreservesBackendOrder) { QImage image(32, 32, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); renderive::Frame_Raster_Context context(image); context.qt().fillRect(QRect(0, 0, 32, 32), Qt::red); const std::array line{{QPointF(2.0, 16.0), QPointF(29.0, 16.0)}}; context.stroke_polyline(line, QPen(Qt::blue, 3.0, Qt::SolidLine, Qt::FlatCap)); context.qt().fillRect(QRect(0, 0, 4, 4), Qt::green); context.finish(); EXPECT_EQ(image.pixelColor(8, 8), QColor(Qt::red)); EXPECT_GT(image.pixelColor(16, 16).blue(), 200); EXPECT_EQ(image.pixelColor(1, 1), QColor(Qt::green)); } TEST(Memory_Data_Path_Stress, FrameRasterContextAppliesCacheTranslation) { QImage image(8, 8, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); renderive::Frame_Raster_Context context(image, QPointF(-10.0, -10.0)); const std::array line{{QPointF(10.0, 12.0), QPointF(17.0, 12.0)}}; context.stroke_polyline(line, QPen(Qt::white, 1.0, Qt::SolidLine, Qt::FlatCap)); context.finish(); EXPECT_GT(image.pixelColor(3, 2).alpha(), 0); EXPECT_EQ(image.pixelColor(3, 6).alpha(), 0); } int main(int argc, char** argv) { qputenv("QT_QPA_PLATFORM", "offscreen"); QApplication app(argc, argv); testing::InitGoogleTest(&argc, argv); renderive::start_render_scheduler(); return RUN_ALL_TESTS(); }