@@ -14,6 +14,7 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <thread>
#include <tuple>
// local includes
#include <src/config.h>
@@ -74,6 +75,7 @@
BaseTest::SetUp();
saved_capture = config::video.capture;
saved_address = config::video.qemu_dbus_address;
saved_output_name = config::video.output_name;
bus = std::make_unique<qemu_test::private_bus_t>();
if (!bus->ok()) {
@@ -93,11 +95,13 @@
config::video.capture = "qemu";
config::video.qemu_dbus_address = bus->address();
config::video.output_name.clear();
}
void TearDown() override {
config::video.capture = saved_capture;
config::video.qemu_dbus_address = saved_address;
config::video.output_name = saved_output_name;
fake.reset();
bus.reset();
BaseTest::TearDown();
@@ -114,8 +118,11 @@
* @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, int framerate = 60) {
const int before = fake->registrations(console_id);
std::thread sender {[&, console_id, width, height, before]() {
if (qemu_test::wait_until([&]() {
std::thread sender {[&, console_id, width, height]() {
if (fake->wait_for_listener(console_id)) {
return fake->registrations(console_id) > before;
})) {
fake->scanout(console_id, width, height, width * 4, qemu::pixman_format::x8r8g8b8, solid(width, height, 0x10, 0x20, 0x30));
}
}};
@@ -129,9 +136,10 @@
*
* @param display Display to capture from.
* @param on_frame Called for each pushed image; return false to stop.
* @param draw_cursor Whether the pipeline asks for the cursor to be drawn.
* @return Capture status; `ok` also when the tick limit stopped the capture.
*/
platf::capture_e run_capture(platf::display_t &display, const std::function<bool(std::shared_ptr<platf::img_t> &&, bool)> &on_frame, bool draw_cursor = false) {
platf::capture_e run_capture(platf::display_t &display, const std::function<bool(std::shared_ptr<platf::img_t> &&, bool)> &on_frame) {
int ticks = 0;
auto bounded = [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) {
return ++ticks < 600 && on_frame(std::move(img), frame_captured);
@@ -148,6 +156,6 @@
img_out = pool.back();
return true;
};
bool cursor = draw_cursor;
bool cursor = false;
return display.capture(bounded, pull, &cursor);
}
@@ -156,5 +164,6 @@
std::unique_ptr<qemu_test::fake_qemu_t> fake;
std::string saved_capture;
std::string saved_address;
std::string saved_output_name;
};
} // namespace
@@ -344,6 +353,189 @@
});
resizer.join();
EXPECT_EQ(status, platf::capture_e::reinit);
}
// @tag requirements: [REQ-CAP-004]
TEST_F(QemuCaptureTest, DisableShowsBlackFrameAndKeepsCapturing) {
auto display = open_display("1", 1, 4, 4);
ASSERT_NE(display, nullptr);
int captured = 0;
std::vector<std::uint8_t> last;
std::thread actor;
auto status = run_capture(*display, [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) {
if (!frame_captured) {
return true;
}
captured += 1;
last.assign(img->data, img->data + img->height * img->row_pitch);
if (captured == 1) {
EXPECT_EQ(last[0], 0x10);
actor = std::thread {[&]() {
fake->disable(1);
}};
return true;
}
if (captured == 2) {
// the display is off: a black frame, and the capture keeps running
EXPECT_EQ(last, std::vector<std::uint8_t>(4 * 4 * 4, 0));
actor.join();
actor = std::thread {[&]() {
fake->update(1, 0, 0, 4, 4, 16, qemu::pixman_format::x8r8g8b8, solid(4, 4, 0x55, 0x66, 0x77));
}};
return true;
}
// the guest turned the display back on at the same size
EXPECT_EQ(last[0], 0x55);
return false;
});
if (actor.joinable()) {
actor.join();
}
EXPECT_EQ(status, platf::capture_e::ok);
EXPECT_EQ(captured, 3);
}
// @tag requirements: [REQ-CAP-004]
TEST_F(QemuCaptureTest, SurvivesGuestRebootSequence) {
// the OS runs at 8x6; the guest reboots: Disable, firmware mode 4x2, then the OS mode 8x6 again
auto display = open_display("1", 1, 8, 6);
ASSERT_NE(display, nullptr);
std::thread reboot {[&]() {
std::this_thread::sleep_for(50ms);
fake->disable(1);
fake->scanout(1, 4, 2, 16, qemu::pixman_format::x8r8g8b8, solid(4, 2, 1, 2, 3));
}};
auto status = run_capture(*display, [&](std::shared_ptr<platf::img_t> &&, bool) {
return true;
});
reboot.join();
EXPECT_EQ(status, platf::capture_e::reinit) << "the firmware mode changes the size";
// the pipeline re-creates the display, as video.cpp does after reinit
display.reset();
display = open_display("1", 1, 4, 2);
ASSERT_NE(display, nullptr);
EXPECT_EQ(display->width, 4);
EXPECT_EQ(display->height, 2);
std::thread boot {[&]() {
std::this_thread::sleep_for(50ms);
fake->scanout(1, 8, 6, 32, qemu::pixman_format::x8r8g8b8, solid(8, 6, 9, 9, 9));
}};
status = run_capture(*display, [&](std::shared_ptr<platf::img_t> &&, bool) {
return true;
});
boot.join();
EXPECT_EQ(status, platf::capture_e::reinit);
display.reset();
display = open_display("1", 1, 8, 6);
ASSERT_NE(display, nullptr);
EXPECT_EQ(display->width, 8);
std::uint8_t first_byte = 0;
status = run_capture(*display, [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) {
if (frame_captured) {
first_byte = img->data[0];
}
return !frame_captured;
});
EXPECT_EQ(status, platf::capture_e::ok);
EXPECT_EQ(first_byte, 0x10);
}
// @tag requirements: [REQ-CAP-005]
TEST_F(QemuCaptureTest, CompositesGuestCursorWhenRequested) {
auto display = open_display("1", 1, 8, 8);
ASSERT_NE(display, nullptr);
// 2x2 opaque white cursor with its hot spot at (1, 1), pointer at (4, 4): covers (3..4, 3..4)
ASSERT_TRUE(fake->cursor_define(1, 2, 2, 1, 1, std::vector<std::uint8_t>(2 * 2 * 4, 0xff)));
ASSERT_TRUE(fake->mouse_set(1, 4, 4, true));
for (bool draw : {true, false}) {
std::vector<std::uint8_t> frame;
auto status = run_capture(
*display,
[&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) {
if (!frame_captured) {
return true;
}
frame.assign(img->data, img->data + img->height * img->row_pitch);
return false;
},
draw
);
EXPECT_EQ(status, platf::capture_e::ok);
ASSERT_EQ(frame.size(), 8 * 8 * 4);
auto pixel = [&](int x, int y) {
return frame[(y * 8 + x) * 4];
};
EXPECT_EQ(pixel(3, 3), draw ? 0xff : 0x10) << "draw=" << draw;
EXPECT_EQ(pixel(4, 4), draw ? 0xff : 0x10) << "draw=" << draw;
EXPECT_EQ(pixel(2, 2), 0x10);
EXPECT_EQ(pixel(5, 5), 0x10);
// the cursor moving produces a new frame even without framebuffer damage
ASSERT_TRUE(fake->mouse_set(1, 4, 4, true));
}
}
// @tag requirements: [REQ-CAP-006]
TEST_F(QemuCaptureTest, OutputNameSelectsConsoleByIdOrLabel) {
for (auto [output_name, console_id, width] : {
std::tuple {std::string {"2"}, 2u, 32},
std::tuple {std::string {"virtio-gpu-pci.1"}, 2u, 32},
std::tuple {std::string {"1"}, 1u, 64},
std::tuple {std::string {"VGA"}, 1u, 64},
}) {
config::video.output_name = output_name;
// Sunshine picks the display whose name equals output_name (video::refresh_displays)
auto names = platf::qemu_display_names();
auto it = std::ranges::find(names, output_name);
ASSERT_NE(it, names.end()) << output_name;
EXPECT_EQ(names.size(), 2);
const int before = fake->registrations(console_id);
auto display = open_display(*it, console_id, width, 8);
ASSERT_NE(display, nullptr) << output_name;
EXPECT_EQ(display->width, width) << output_name;
EXPECT_EQ(fake->registrations(console_id), before + 1) << output_name;
}
}
// @tag requirements: [REQ-CAP-006]
TEST_F(QemuCaptureTest, MultiHeadGuestStreamsOneConsolePerDisplay) {
auto head0 = open_display("1", 1, 8, 4);
ASSERT_NE(head0, nullptr);
auto head1 = std::shared_ptr<platf::display_t> {};
{
std::thread sender {[&]() {
if (fake->wait_for_listener(2)) {
fake->scanout(2, 6, 2, 24, qemu::pixman_format::x8r8g8b8, solid(6, 2, 0x99, 0x88, 0x77));
}
}};
head1 = platf::qemu_display(platf::mem_type_e::system, "virtio-gpu-pci.1", stream_config());
sender.join();
}
ASSERT_NE(head1, nullptr);
EXPECT_EQ(head0->width, 8);
EXPECT_EQ(head1->width, 6);
for (auto [display, blue] : {std::pair {head0, 0x10}, std::pair {head1, 0x99}}) {
int value = -1;
auto status = run_capture(*display, [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) {
if (frame_captured) {
value = img->data[0];
}
return !frame_captured;
});
EXPECT_EQ(status, platf::capture_e::ok);
EXPECT_EQ(value, blue);
}
}
// @tag requirements: [REQ-CAP-001]