@@ -8,6 +8,8 @@
#include "fake_qemu.h"
// standard includes
#include <algorithm>
#include <atomic>
#include <cstring>
#include <sys/socket.h>
#include <sys/un.h>
@@ -29,14 +31,15 @@
namespace {
/**
* @brief A 60 fps stream configuration.
* @brief A 720p stream configuration.
*
* @param framerate Client frame rate.
* @return Video configuration.
*/
video::config_t stream_config() {
video::config_t stream_config(int framerate = 60) {
video::config_t config {};
config.width = 1280;
config.height = 720;
config.framerate = framerate;
config.framerate = 60;
return config;
}
@@ -107,15 +110,16 @@
* @param console_id Console the fake scans out on.
* @param width Scanout width.
* @param height Scanout height.
* @param framerate Client frame rate.
* @return Display, or nullptr.
*/
std::shared_ptr<platf::display_t> open_display(const std::string &display_name, std::uint32_t console_id, int width, int height) {
std::shared_ptr<platf::display_t> open_display(const std::string &display_name, std::uint32_t console_id, int width, int height, int framerate = 60) {
std::thread sender {[&, console_id, width, height]() {
if (fake->wait_for_listener(console_id)) {
fake->scanout(console_id, width, height, width * 4, qemu::pixman_format::x8r8g8b8, solid(width, height, 0x10, 0x20, 0x30));
}
}};
auto display = platf::qemu_display(platf::mem_type_e::system, display_name, stream_config());
auto display = platf::qemu_display(platf::mem_type_e::system, display_name, stream_config(framerate));
sender.join();
return display;
}
@@ -221,6 +225,109 @@
EXPECT_EQ(status, platf::capture_e::ok);
EXPECT_EQ(captured, 2);
EXPECT_GT(idle, 0);
}
// @tag requirements: [REQ-NFR-001]
TEST_F(QemuCaptureTest, PushesDamageWithinMillisecondsNotAtTheNextTick) {
// 30 fps: a fixed-tick capture loop would delay damage by 0-33 ms, 17 ms on average
auto display = open_display("1", 1, 4, 4, 30);
ASSERT_NE(display, nullptr);
std::vector<std::chrono::steady_clock::duration> delays;
std::atomic<bool> stop {false};
std::thread painter;
int captured = 0;
auto status = run_capture(*display, [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) {
if (!frame_captured) {
return true;
}
captured += 1;
if (captured == 1) {
painter = std::thread {[&]() {
// damage lands at varying points of the frame interval, always later than one interval after the last push
for (int i = 0; i < 12 && !stop; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(40 + (i * 7) % 30));
fake->update(1, 0, 0, 1, 1, 4, qemu::pixman_format::x8r8g8b8, {(std::uint8_t) i, 0, 0, 0xff});
}
}};
return true;
}
delays.push_back(std::chrono::steady_clock::now() - *img->frame_timestamp);
return delays.size() < 10;
});
stop = true;
if (painter.joinable()) {
painter.join();
}
EXPECT_EQ(status, platf::capture_e::ok);
ASSERT_EQ(delays.size(), 10);
std::ranges::sort(delays);
EXPECT_LT(delays[5], 5ms) << "median damage-to-push delay";
EXPECT_LT(delays.back(), 16ms) << "worst damage-to-push delay";
}
// @tag requirements: [REQ-NFR-001]
TEST_F(QemuCaptureTest, BurstsOfDamagePushAtMostOneFramePerInterval) {
auto display = open_display("1", 1, 4, 4, 30);
ASSERT_NE(display, nullptr);
const auto interval = std::chrono::nanoseconds {1s} / 30;
std::vector<std::chrono::steady_clock::time_point> pushes;
std::atomic<bool> painting {true};
std::thread painter;
auto status = run_capture(*display, [&](std::shared_ptr<platf::img_t> &&, bool frame_captured) {
if (frame_captured) {
pushes.push_back(std::chrono::steady_clock::now());
if (pushes.size() == 1) {
painter = std::thread {[&]() {
const auto end = std::chrono::steady_clock::now() + 600ms;
for (std::uint8_t i = 0; std::chrono::steady_clock::now() < end; ++i) {
fake->update(1, 0, 0, 1, 1, 4, qemu::pixman_format::x8r8g8b8, {i, 0, 0, 0xff});
std::this_thread::sleep_for(2ms);
}
painting = false;
}};
}
}
return painting || pushes.size() < 2;
});
if (painter.joinable()) {
painter.join();
}
EXPECT_EQ(status, platf::capture_e::ok);
// 600 ms of damage every 2 ms is about 250 updates; at 30 fps that is at most 19 frames plus the first
EXPECT_GE(pushes.size(), 10);
EXPECT_LE(pushes.size(), 21);
for (std::size_t i = 1; i < pushes.size(); ++i) {
EXPECT_GE(pushes[i] - pushes[i - 1], interval - 2ms) << "push " << i;
}
}
// @tag requirements: [REQ-NFR-001]
TEST_F(QemuCaptureTest, NoDamageRepeatsNoFrames) {
auto display = open_display("1", 1, 4, 4, 60);
ASSERT_NE(display, nullptr);
int captured = 0;
int idle = 0;
std::chrono::steady_clock::time_point first;
auto status = run_capture(*display, [&](std::shared_ptr<platf::img_t> &&, bool frame_captured) {
if (frame_captured) {
captured += 1;
first = std::chrono::steady_clock::now();
return true;
}
idle += 1;
return captured == 0 || std::chrono::steady_clock::now() - first < 300ms;
});
EXPECT_EQ(status, platf::capture_e::ok);
EXPECT_EQ(captured, 1);
// the pipeline still gets a heartbeat about once per frame interval, so it can stop the capture
EXPECT_GE(idle, 10);
EXPECT_LE(idle, 40);
}
// @tag requirements: [REQ-CAP-001]