Files

1115 lines
38 KiB
C++

/**
* @file tests/unit/platform/linux/test_wayland.cpp
* @brief Test Wayland output mode selection.
*/
#ifdef SUNSHINE_BUILD_WAYLAND
// test includes
#include "../../../tests_common.h"
// standard includes
#include <array>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <format>
#include <fstream>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>
// system includes
#include <drm_fourcc.h>
#include <fcntl.h>
#include <unistd.h>
#include <xf86drm.h>
// local includes
#include <src/platform/linux/wayland.h>
#ifdef SUNSHINE_BUILD_CUDA
#include <src/platform/linux/cuda.h>
#endif
namespace {
struct fake_gbm_bo_t {
int plane_count {};
int failed_plane {-1};
std::uint64_t modifier {};
std::array<std::uint32_t, 4> strides {};
std::array<std::uint32_t, 4> offsets {};
std::array<int, 4> exported_fds {-1, -1, -1, -1};
};
fake_gbm_bo_t &fake_bo(gbm_bo *bo) {
return *static_cast<fake_gbm_bo_t *>(static_cast<void *>(bo));
}
int get_plane_count(gbm_bo *bo) {
return fake_bo(bo).plane_count;
}
int get_fd_for_plane(gbm_bo *bo, int plane) {
auto &fake = fake_bo(bo);
if (plane == fake.failed_plane) {
return -1;
}
fake.exported_fds[plane] = open("/dev/null", O_RDONLY | O_CLOEXEC);
return fake.exported_fds[plane];
}
std::uint32_t get_stride_for_plane(gbm_bo *bo, int plane) {
return fake_bo(bo).strides[plane];
}
std::uint32_t get_offset(gbm_bo *bo, int plane) {
return fake_bo(bo).offsets[plane];
}
std::uint64_t get_modifier(gbm_bo *bo) {
return fake_bo(bo).modifier;
}
const wl::gbm_bo_accessors_t fake_accessors {
.get_plane_count = get_plane_count,
.get_fd_for_plane = get_fd_for_plane,
.get_stride_for_plane = get_stride_for_plane,
.get_offset = get_offset,
.get_modifier = get_modifier,
};
} // namespace
TEST(WaylandMonitorTest, IgnoresNonCurrentModesAroundCurrentMode) {
wl::monitor_t monitor {nullptr};
monitor.wl_mode(nullptr, WL_OUTPUT_MODE_PREFERRED, 1280, 720, 60000);
monitor.wl_mode(nullptr, WL_OUTPUT_MODE_CURRENT, 1920, 1080, 60000);
monitor.wl_mode(nullptr, 0, 2560, 1440, 60000);
EXPECT_EQ(monitor.viewport.width, 1920);
EXPECT_EQ(monitor.viewport.height, 1080);
}
TEST(WaylandMonitorTest, UpdatesCurrentMode) {
wl::monitor_t monitor {nullptr};
monitor.wl_mode(nullptr, WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED, 1920, 1080, 60000);
monitor.wl_mode(nullptr, WL_OUTPUT_MODE_CURRENT, 2560, 1440, 120000);
EXPECT_EQ(monitor.viewport.width, 2560);
EXPECT_EQ(monitor.viewport.height, 1440);
}
TEST(WaylandMonitorTest, RecordsRefreshOfCurrentMode) {
wl::monitor_t monitor {nullptr};
EXPECT_EQ(monitor.refresh_mhz, 0);
monitor.wl_mode(nullptr, WL_OUTPUT_MODE_PREFERRED, 1920, 1080, 60000);
EXPECT_EQ(monitor.refresh_mhz, 0);
monitor.wl_mode(nullptr, WL_OUTPUT_MODE_CURRENT, 1920, 1080, 120000);
monitor.wl_mode(nullptr, 0, 1920, 1080, 144000);
EXPECT_EQ(monitor.refresh_mhz, 120000);
}
/**
* @brief Ignored wl-output events must not overwrite xdg-output placement or the active pixel mode.
*/
TEST(WaylandMonitorTest, ListenerIgnoresPhysicalGeometryScaleAndDone) {
wl::monitor_t monitor {nullptr};
monitor.xdg_position(nullptr, 100, 200);
monitor.xdg_size(nullptr, 1280, 720);
monitor.wl_mode(nullptr, WL_OUTPUT_MODE_CURRENT, 2560, 1440, 120000);
monitor.name = "DP-1";
monitor.description = "Logical monitor";
monitor.wl_listener.geometry(&monitor, nullptr, -500, -600, 600, 340, WL_OUTPUT_SUBPIXEL_UNKNOWN, "Other", "Physical monitor", WL_OUTPUT_TRANSFORM_90);
monitor.wl_listener.scale(&monitor, nullptr, 3);
monitor.wl_listener.done(&monitor, nullptr);
EXPECT_EQ(monitor.viewport.offset_x, 100);
EXPECT_EQ(monitor.viewport.offset_y, 200);
EXPECT_EQ(monitor.viewport.logical_width, 1280);
EXPECT_EQ(monitor.viewport.logical_height, 720);
EXPECT_EQ(monitor.viewport.width, 2560);
EXPECT_EQ(monitor.viewport.height, 1440);
EXPECT_EQ(monitor.refresh_mhz, 120000);
EXPECT_EQ(monitor.name, "DP-1");
EXPECT_EQ(monitor.description, "Logical monitor");
}
TEST(WaylandCaptureTest, UsesDamageCaptureOnlyOnAFasterOutput) {
// An output at twice the stream's rate, and one just at the threshold.
EXPECT_TRUE(wl::use_damage_capture(3, 60.0, 120000));
EXPECT_TRUE(wl::use_damage_capture(3, 60.0, 90000));
// An output at the stream's own rate keeps the paced copy, as does one only slightly faster.
EXPECT_FALSE(wl::use_damage_capture(3, 60.0, 60000));
EXPECT_FALSE(wl::use_damage_capture(3, 60.0, 89999));
EXPECT_FALSE(wl::use_damage_capture(3, 120.0, 120000));
}
TEST(WaylandCaptureTest, KeepsPacedCaptureWithoutProtocolOrRates) {
// copy_with_damage arrived in version 2 of the protocol.
EXPECT_FALSE(wl::use_damage_capture(1, 60.0, 120000));
EXPECT_TRUE(wl::use_damage_capture(2, 60.0, 120000));
// A compositor that never reported a mode, and a stream without a rate.
EXPECT_FALSE(wl::use_damage_capture(3, 60.0, 0));
EXPECT_FALSE(wl::use_damage_capture(3, 0.0, 120000));
}
TEST(WaylandCaptureTest, DamageRequestBudgetFollowsASourceAtTheStreamRate) {
using namespace std::chrono_literals;
constexpr std::chrono::nanoseconds delay = 16ms;
const std::chrono::steady_clock::time_point start {1s};
// Frames one interval apart: the next request is always allowed a quarter interval before
// the next frame is due, however long that goes on.
auto next_request = std::chrono::steady_clock::time_point {};
for (int i = 0; i < 100; ++i) {
const auto frame_time = start + i * delay;
next_request = wl::next_damage_request(next_request, frame_time, delay);
EXPECT_EQ(next_request, frame_time + delay - delay / 4);
}
}
TEST(WaylandCaptureTest, DamageRequestBudgetHoldsAFasterSourceToTheStreamRate) {
using namespace std::chrono_literals;
constexpr std::chrono::nanoseconds delay = 16ms;
const std::chrono::steady_clock::time_point start {1s};
// Frames arriving twice as fast as the stream: each one still costs a whole interval.
auto next_request = wl::next_damage_request({}, start, delay);
const auto first = next_request;
for (int i = 1; i <= 10; ++i) {
next_request = wl::next_damage_request(next_request, start + i * (delay / 2), delay);
}
EXPECT_EQ(next_request, first + 10 * delay);
}
TEST(WaylandCaptureTest, DamageRequestBudgetResetsAfterALateFrame) {
using namespace std::chrono_literals;
constexpr std::chrono::nanoseconds delay = 16ms;
const std::chrono::steady_clock::time_point start {1s};
// A still picture, then a frame a second later: no budget is carried over from the pause.
auto next_request = wl::next_damage_request({}, start, delay);
const auto late = start + 1s;
next_request = wl::next_damage_request(next_request, late, delay);
EXPECT_EQ(next_request, late + delay - delay / 4);
}
TEST(WaylandCaptureTest, WaitsOnlyForADamageRequestWithinOneInterval) {
using namespace std::chrono_literals;
constexpr std::chrono::nanoseconds delay = 16ms;
const std::chrono::steady_clock::time_point now {1s};
EXPECT_TRUE(wl::should_wait_for_damage_request(now + 4ms, now, delay));
// Already due.
EXPECT_FALSE(wl::should_wait_for_damage_request(now, now, delay));
EXPECT_FALSE(wl::should_wait_for_damage_request(now - 4ms, now, delay));
EXPECT_FALSE(wl::should_wait_for_damage_request({}, now, delay));
// Further off than one interval: a budget gone wrong, not something to sleep on.
EXPECT_FALSE(wl::should_wait_for_damage_request(now + delay, now, delay));
EXPECT_FALSE(wl::should_wait_for_damage_request(now + 1h, now, delay));
}
TEST(WaylandCaptureTest, DamageFrameTimeTrustsOnlyATimestampFromItsOwnClock) {
using namespace std::chrono_literals;
constexpr std::chrono::nanoseconds delay = 16ms;
const std::chrono::steady_clock::time_point now {2h};
// Presented a moment before it was received.
EXPECT_EQ(wl::damage_frame_time(now - 1ms, now, delay), now - 1ms);
EXPECT_EQ(wl::damage_frame_time(now, now, delay), now);
// Ahead of the clock, a whole interval or more behind it, or missing.
EXPECT_EQ(wl::damage_frame_time(now + 1ms, now, delay), now);
EXPECT_EQ(wl::damage_frame_time(now + 1h, now, delay), now);
EXPECT_EQ(wl::damage_frame_time(now - delay, now, delay), now);
EXPECT_EQ(wl::damage_frame_time(now - 1h, now, delay), now);
EXPECT_EQ(wl::damage_frame_time(std::nullopt, now, delay), now);
}
TEST(WaylandCaptureTest, DamageRequestBudgetCapsTheRateWhateverClockTheCompositorUses) {
using namespace std::chrono_literals;
constexpr std::chrono::nanoseconds delay = 10ms;
constexpr std::chrono::nanoseconds source_interval = delay / 2;
const std::chrono::steady_clock::time_point start {2h};
// A source at twice the stream's rate, captured for one second the way the capture loop does
// it: wait for the budget, ask, and take the source's next frame.
for (const auto clock_offset : std::initializer_list<std::chrono::nanoseconds> {0ns, 3ms, -3ms, 1s, -1s, 1h, -1h}) {
auto next_request = std::chrono::steady_clock::time_point {};
auto now = start;
int frames = 0;
while (now < start + 1s) {
if (wl::should_wait_for_damage_request(next_request, now, delay)) {
now = next_request;
}
const auto since_start = now - start;
const auto ticks = (since_start + source_interval - 1ns) / source_interval;
now = start + ticks * source_interval;
const auto frame_time = wl::damage_frame_time(now + clock_offset, now, delay);
next_request = wl::next_damage_request(next_request, frame_time, delay);
++frames;
// The source has moved on by the time the next frame is asked for.
now += 1ns;
}
// One hundred for the second, the frame that opens it, and at most one more at the start
// from a timestamp that lags by less than an interval.
EXPECT_GE(frames, 100) << "clock offset " << clock_offset.count() << " ns";
EXPECT_LE(frames, 102) << "clock offset " << clock_offset.count() << " ns";
}
}
TEST(WaylandCaptureTest, AsksForTheFirstFrameOutrightAndThenForChanges) {
using enum wl::screencopy_request_e;
EXPECT_EQ(wl::next_screencopy_request(false, false, true, true, false), copy);
EXPECT_EQ(wl::next_screencopy_request(false, true, true, true, true), copy_with_damage);
// Paced capture asks outright every time.
EXPECT_EQ(wl::next_screencopy_request(false, true, true, false, true), copy);
}
TEST(WaylandCaptureTest, ReplacesAPendingRequestWhenTheCursorIsToggledOnAStillPicture) {
using enum wl::screencopy_request_e;
// A still picture: the request for the next change outlives one timeout after another.
EXPECT_EQ(wl::next_screencopy_request(true, true, true, true, true), keep);
EXPECT_EQ(wl::next_screencopy_request(true, true, true, true, true), keep);
// The cursor is switched off, which damages nothing. The pending request would still answer
// with the cursor in the picture, so it gives way to a plain copy.
EXPECT_EQ(wl::next_screencopy_request(true, true, false, true, true), copy);
// That copy was made with the new setting and is kept in turn, then changes are asked for again.
EXPECT_EQ(wl::next_screencopy_request(true, false, false, true, true), keep);
EXPECT_EQ(wl::next_screencopy_request(false, false, false, true, true), copy_with_damage);
// And back on.
EXPECT_EQ(wl::next_screencopy_request(true, false, true, true, true), copy);
// Paced capture replaces a request that did not answer in time the same way.
EXPECT_EQ(wl::next_screencopy_request(true, true, false, false, true), copy);
}
TEST(WaylandCaptureTest, AsksOutrightWhenTheCursorIsToggledBetweenRequests) {
using enum wl::screencopy_request_e;
// A frame with the cursor was delivered and nothing is pending. The cursor is switched off
// before the next request is made. Nothing on the output changes, so a request for the next
// change would wait while the stream goes on showing the cursor.
EXPECT_EQ(wl::next_screencopy_request(false, true, false, true, true), copy);
// That copy was made without the cursor. The picture is up to date, changes are asked for
// again and the request is kept over timeouts.
EXPECT_EQ(wl::next_screencopy_request(false, false, false, true, true), copy_with_damage);
EXPECT_EQ(wl::next_screencopy_request(true, false, false, true, true), keep);
// And back on.
EXPECT_EQ(wl::next_screencopy_request(false, false, true, true, true), copy);
}
TEST(WaylandCaptureTest, AsksOutrightAfterARequestThatCouldNotBeWithdrawn) {
using enum wl::screencopy_request_e;
// The cursor is switched off while a request made with it is out. It is to be replaced, but
// its buffer is still being set up and it can not be withdrawn. The caller keeps it and leaves
// the setting it remembers alone, because no request was made with the new one.
bool requested_cursor = true;
EXPECT_EQ(wl::next_screencopy_request(true, requested_cursor, false, true, true), copy);
// The old request is answered, with the cursor in the picture. The next request has to bring
// the picture up to date, not wait for a change.
EXPECT_EQ(wl::next_screencopy_request(false, requested_cursor, false, true, true), copy);
// Only that request was made with the new setting.
requested_cursor = false;
EXPECT_EQ(wl::next_screencopy_request(false, requested_cursor, false, true, true), copy_with_damage);
}
TEST(WaylandCaptureTest, UsesVramForVaapi) {
EXPECT_TRUE(wl::use_vram_capture(platf::mem_type_e::vaapi));
}
TEST(WaylandCaptureTest, UsesSystemMemoryForSoftwareEncoding) {
EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::system));
}
TEST(WaylandCaptureTest, UsesVramForCudaOnlyWhenCaptureNodeMatchesCudaPci) {
#ifdef SUNSHINE_BUILD_CUDA
const wl::render_node_pci_reader_t render_reader = [](const std::string &) {
return std::optional<std::string> {"0000:0f:00.0"};
};
const wl::cuda_device_pci_reader_t matching_cuda_reader = [](int) {
return std::optional<std::string> {"00000000:0F:00.0"};
};
const wl::cuda_device_pci_reader_t other_cuda_reader = [](int) {
return std::optional<std::string> {"00000000:0E:00.0"};
};
EXPECT_TRUE(wl::use_vram_capture(platf::mem_type_e::cuda, render_reader, matching_cuda_reader));
EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::cuda, render_reader, other_cuda_reader));
#else
EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::cuda));
#endif
}
TEST(WaylandCaptureTest, ParsesCudaAndSysfsPciBusIds) {
const auto cuda_form = wl::parse_pci_bus_id("00000000:0F:00.0");
const auto short_cuda_form = wl::parse_pci_bus_id("0000:0F:00.0");
const auto sysfs_form = wl::parse_pci_bus_id("0000:0f:00.0");
ASSERT_TRUE(cuda_form.has_value());
ASSERT_TRUE(short_cuda_form.has_value());
ASSERT_TRUE(sysfs_form.has_value());
EXPECT_EQ(*cuda_form, *sysfs_form);
EXPECT_EQ(*short_cuda_form, *sysfs_form);
EXPECT_EQ(cuda_form->domain, 0u);
EXPECT_EQ(cuda_form->bus, 0x0fu);
EXPECT_EQ(cuda_form->device, 0x00u);
EXPECT_EQ(cuda_form->function, 0u);
}
TEST(WaylandCaptureTest, RejectsMalformedPciBusIds) {
for (const char *bus_id : {"", "0000:0f:00", "0000:0f:00.0:1", "0000:0f:00.8", "0000:0f:00.0 ", "pci:0000:0f:00.0", "0000:0g:00.0", "0x10de"}) {
EXPECT_FALSE(wl::parse_pci_bus_id(bus_id).has_value()) << bus_id;
}
}
TEST(WaylandCaptureTest, FallsBackToRamBridgeWhenPciIsUnprovable) {
const wl::render_node_pci_reader_t render_reader = [](const std::string &) {
return std::optional<std::string> {"0000:0f:00.0"};
};
const wl::render_node_pci_reader_t missing_render_reader = [](const std::string &) {
return std::optional<std::string> {};
};
const wl::cuda_device_pci_reader_t cuda_reader = [](int) {
return std::optional<std::string> {"00000000:0F:00.0"};
};
const wl::cuda_device_pci_reader_t missing_cuda_reader = [](int) {
return std::optional<std::string> {};
};
EXPECT_FALSE(wl::capture_node_is_cuda_device(missing_render_reader, cuda_reader));
EXPECT_FALSE(wl::capture_node_is_cuda_device(render_reader, missing_cuda_reader));
EXPECT_FALSE(wl::capture_node_is_cuda_device(missing_render_reader, missing_cuda_reader));
#ifdef SUNSHINE_BUILD_CUDA
EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::cuda, missing_render_reader, cuda_reader));
EXPECT_FALSE(wl::use_vram_capture(platf::mem_type_e::cuda, render_reader, missing_cuda_reader));
#endif
}
TEST(WaylandCaptureTest, ResolvesRenderNodeAliasBeforePciLookup) {
const wl::render_path_resolver_t fake_resolver = [](const std::string &) {
return "/dev/dri/renderD128";
};
std::string observed_path;
const wl::render_node_pci_reader_t recording_reader = [&observed_path](std::string_view render_path) {
observed_path = render_path;
return std::optional<std::string> {"0000:0f:00.0"};
};
const wl::cuda_device_pci_reader_t cuda_reader = [](int) {
return std::optional<std::string> {"00000000:0F:00.0"};
};
EXPECT_TRUE(wl::capture_node_is_cuda_device(recording_reader, cuda_reader, fake_resolver));
EXPECT_EQ(observed_path, "/dev/dri/renderD128");
}
TEST(WaylandLinearCopyTest, SkipsLinearAttemptUnlessAdvertised) {
const std::vector<std::uint64_t> without_linear {0x100000000000004};
const std::vector<std::uint64_t> with_linear {0x100000000000004, DRM_FORMAT_MOD_LINEAR};
EXPECT_FALSE(wl::should_attempt_linear_copy(false, with_linear));
EXPECT_FALSE(wl::should_attempt_linear_copy(true, without_linear));
EXPECT_FALSE(wl::should_attempt_linear_copy(true, {}));
EXPECT_TRUE(wl::should_attempt_linear_copy(true, with_linear));
}
TEST(WaylandAdvertisedModifiersTest, ReturnsNullWhenUnavailable) {
const std::map<std::uint32_t, std::vector<std::uint64_t>> empty_map {};
const std::map<std::uint32_t, std::vector<std::uint64_t>> map_with_empty_list {{0x34325258, {}}};
EXPECT_EQ(wl::find_advertised_modifiers(nullptr, 0x34325258), nullptr);
EXPECT_EQ(wl::find_advertised_modifiers(&empty_map, 0x34325258), nullptr);
EXPECT_EQ(wl::find_advertised_modifiers(&map_with_empty_list, 0x34325258), nullptr);
EXPECT_EQ(wl::find_advertised_modifiers(&map_with_empty_list, 0xdeadbeef), nullptr);
}
TEST(WaylandAdvertisedModifiersTest, ReturnsAdvertisedListWhenPresent) {
const std::vector<std::uint64_t> expected {0x100000000000004, DRM_FORMAT_MOD_LINEAR};
const std::map<std::uint32_t, std::vector<std::uint64_t>> modifiers {{0x34325258, expected}};
const auto *actual = wl::find_advertised_modifiers(&modifiers, 0x34325258);
ASSERT_NE(actual, nullptr);
EXPECT_EQ(*actual, expected);
}
/**
* @brief Test fixture for DRM render-node path resolution.
*/
class WaylandResolvePathTest: public BaseTest {
protected:
/**
* @brief Create an empty directory for the current test.
*/
void SetUp() override {
BaseTest::SetUp();
std::filesystem::remove_all(test_dir);
std::filesystem::create_directories(test_dir);
}
/**
* @brief Remove files created by the current test.
*/
void TearDown() override {
std::filesystem::remove_all(test_dir);
BaseTest::TearDown();
}
const std::filesystem::path test_dir {std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "wayland_resolve_path_tests"}; ///< Directory containing resolve-path test files.
};
TEST_F(WaylandResolvePathTest, FollowsSymlinkedAliasToTarget) {
std::error_code ec;
const auto dir = std::filesystem::weakly_canonical(test_dir, ec);
ASSERT_FALSE(ec);
const auto target = dir / "renderD128";
const auto alias = dir / "by-path" / "pci-0000:00:02.0-render";
std::filesystem::create_directories(alias.parent_path(), ec);
ASSERT_FALSE(ec) << ec.message();
{
std::ofstream out {target};
ASSERT_TRUE(out.good());
}
std::filesystem::remove(alias, ec);
std::filesystem::create_symlink(target, alias, ec);
ASSERT_FALSE(ec) << ec.message();
EXPECT_EQ(wl::resolve_drm_node_path(alias.string()), target.string());
}
TEST_F(WaylandResolvePathTest, FallsBackToVerbatimPath) {
std::error_code ec;
const auto dir = std::filesystem::weakly_canonical(test_dir, ec);
ASSERT_FALSE(ec);
const auto missing = dir / "renderD999";
EXPECT_EQ(wl::resolve_drm_node_path(missing.string()), missing.string());
const auto dangling_target = dir / "no-such-target";
const auto dangling_link = dir / "by-path" / "pci-0000:00:02.0-render";
std::filesystem::create_directories(dangling_link.parent_path(), ec);
ASSERT_FALSE(ec) << ec.message();
std::filesystem::remove(dangling_link, ec);
std::filesystem::create_symlink(dangling_target, dangling_link, ec);
ASSERT_FALSE(ec) << ec.message();
EXPECT_EQ(wl::resolve_drm_node_path(dangling_link.string()), dangling_link.string());
}
TEST_F(WaylandResolvePathTest, FallsBackToVerbatimPathOnSymlinkLoop) {
std::error_code ec;
const auto dir = std::filesystem::weakly_canonical(test_dir, ec);
ASSERT_FALSE(ec);
// A symlink pointing at itself cannot be canonicalized (ELOOP), forcing the error_code path.
const auto loop = dir / "self-loop";
std::filesystem::remove(loop, ec);
std::filesystem::create_symlink(loop, loop, ec);
ASSERT_FALSE(ec) << ec.message();
EXPECT_EQ(wl::resolve_drm_node_path(loop.string()), loop.string());
}
TEST_F(WaylandResolvePathTest, ReturnsNoPciIdentityWhenRenderNodeCannotOpen) {
EXPECT_FALSE(wl::render_node_pci_bus_id((test_dir / "missing-render-node").string()).has_value());
}
TEST_F(WaylandResolvePathTest, ReturnsNoPciIdentityWhenDrmQueryFails) {
const auto regular_file = test_dir / "regular-file";
{
std::ofstream output {regular_file};
ASSERT_TRUE(output.good());
}
EXPECT_FALSE(wl::render_node_pci_bus_id(regular_file.string()).has_value());
}
TEST(WaylandInterfaceTest, RecordsOnlyExplicitDmabufModifiers) {
constexpr std::uint32_t format = DRM_FORMAT_XRGB8888;
constexpr std::uint64_t explicit_modifier = 0x100000000000004;
wl::interface_t interface;
interface.dmabuf_modifier(nullptr, format, DRM_FORMAT_MOD_INVALID >> 32, DRM_FORMAT_MOD_INVALID & 0xffffffff);
interface.dmabuf_modifier(nullptr, format, explicit_modifier >> 32, explicit_modifier & 0xffffffff);
const auto modifiers = interface.supported_modifiers.find(format);
ASSERT_NE(modifiers, interface.supported_modifiers.end());
ASSERT_EQ(modifiers->second.size(), 1);
EXPECT_EQ(modifiers->second.front(), explicit_modifier);
}
TEST(WaylandCaptureTest, ExportsEveryGbmBufferPlane) {
fake_gbm_bo_t bo {
.plane_count = 2,
.modifier = 0x100000000000004,
.strides = {10240, 256},
.offsets = {0, 0x800000},
};
wl::frame_t frame;
const auto plane_count = wl::export_gbm_bo_planes(static_cast<gbm_bo *>(static_cast<void *>(&bo)), frame, fake_accessors);
ASSERT_EQ(plane_count, 2);
EXPECT_EQ(frame.sd.modifier, bo.modifier);
for (std::size_t plane = 0; plane < *plane_count; ++plane) {
EXPECT_EQ(frame.sd.fds[plane], bo.exported_fds[plane]);
EXPECT_EQ(frame.sd.pitches[plane], bo.strides[plane]);
EXPECT_EQ(frame.sd.offsets[plane], bo.offsets[plane]);
}
EXPECT_EQ(frame.sd.fds[2], -1);
EXPECT_EQ(frame.sd.fds[3], -1);
frame.destroy();
}
TEST(WaylandCaptureTest, RejectsUnsupportedGbmPlaneCounts) {
wl::frame_t frame;
fake_gbm_bo_t empty_bo {};
fake_gbm_bo_t oversized_bo {
.plane_count = 5,
};
EXPECT_FALSE(wl::export_gbm_bo_planes(static_cast<gbm_bo *>(static_cast<void *>(&empty_bo)), frame, fake_accessors));
EXPECT_FALSE(wl::export_gbm_bo_planes(static_cast<gbm_bo *>(static_cast<void *>(&oversized_bo)), frame, fake_accessors));
}
TEST(WaylandCaptureTest, ClosesExportedPlanesAfterLaterPlaneFails) {
fake_gbm_bo_t bo {
.plane_count = 2,
.failed_plane = 1,
};
wl::frame_t frame;
EXPECT_FALSE(wl::export_gbm_bo_planes(static_cast<gbm_bo *>(static_cast<void *>(&bo)), frame, fake_accessors));
ASSERT_GE(bo.exported_fds[0], 0);
const auto fd_status = fcntl(bo.exported_fds[0], F_GETFD);
const auto fd_error = errno;
EXPECT_EQ(fd_status, -1);
EXPECT_EQ(fd_error, EBADF);
EXPECT_EQ(frame.sd.fds[0], -1);
}
namespace {
/**
* @brief Count of fake GBM devices destroyed by the accessors below (function-local state, reset per test).
*/
int &fake_destroyed_devices() {
static int count = 0;
return count;
}
/**
* @brief Descriptor most recently returned by open_fake_render_node() (reset per test).
*/
int &last_fake_render_node() {
static int fd = -1;
return fd;
}
/**
* @brief Count of create_device calls made through the accessors below (reset per test).
*/
int &fake_create_device_calls() {
static int count = 0;
return count;
}
int open_fake_render_node(const char *) {
last_fake_render_node() = open("/dev/null", O_RDWR | O_CLOEXEC);
return last_fake_render_node();
}
int fail_to_open_render_node(const char *) {
return -1;
}
gbm_device *create_fake_device(int fd) {
++fake_create_device_calls();
// The address of this object stands in for an opaque GBM device.
static int storage = 0;
return fd >= 0 ? static_cast<gbm_device *>(static_cast<void *>(&storage)) : nullptr;
}
gbm_device *fail_to_create_device(int) {
++fake_create_device_calls();
return nullptr;
}
void destroy_fake_device(gbm_device *) {
++fake_destroyed_devices();
}
bool descriptor_is_open(int fd) {
return fcntl(fd, F_GETFD) != -1;
}
} // namespace
TEST(WaylandGbmDeviceTest, ClosesRenderNodeDescriptorOnReset) {
const wl::gbm_device_accessors_t accessors {
.open_render_node = open_fake_render_node,
.create_device = create_fake_device,
.destroy_device = destroy_fake_device,
};
fake_destroyed_devices() = 0;
wl::gbm_device_t device;
ASSERT_TRUE(device.init("/dev/dri/renderD128", accessors));
ASSERT_TRUE(device);
const int fd = device.fd();
ASSERT_GE(fd, 0);
EXPECT_TRUE(descriptor_is_open(fd));
device.reset();
EXPECT_FALSE(device);
EXPECT_EQ(device.fd(), -1);
EXPECT_EQ(fake_destroyed_devices(), 1);
EXPECT_FALSE(descriptor_is_open(fd));
}
TEST(WaylandGbmDeviceTest, ClosesRenderNodeDescriptorWhenDeviceCreationFails) {
const wl::gbm_device_accessors_t accessors {
.open_render_node = open_fake_render_node,
.create_device = fail_to_create_device,
.destroy_device = destroy_fake_device,
};
fake_destroyed_devices() = 0;
fake_create_device_calls() = 0;
last_fake_render_node() = -1;
wl::gbm_device_t device;
EXPECT_FALSE(device.init("/dev/dri/renderD128", accessors));
EXPECT_FALSE(device);
EXPECT_EQ(device.fd(), -1);
EXPECT_EQ(fake_create_device_calls(), 1);
EXPECT_EQ(fake_destroyed_devices(), 0);
// Check the descriptor the fake accessor actually opened.
const int fd = last_fake_render_node();
ASSERT_GE(fd, 0);
EXPECT_FALSE(descriptor_is_open(fd));
}
TEST(WaylandGbmDeviceTest, DoesNotCreateDeviceWhenRenderNodeFailsToOpen) {
const wl::gbm_device_accessors_t accessors {
.open_render_node = fail_to_open_render_node,
.create_device = create_fake_device,
.destroy_device = destroy_fake_device,
};
fake_destroyed_devices() = 0;
fake_create_device_calls() = 0;
wl::gbm_device_t device;
EXPECT_FALSE(device.init("/dev/dri/renderD128", accessors));
EXPECT_FALSE(device);
EXPECT_EQ(device.get(), nullptr);
EXPECT_EQ(device.fd(), -1);
EXPECT_EQ(fake_create_device_calls(), 0);
EXPECT_EQ(fake_destroyed_devices(), 0);
}
TEST(WaylandGbmDeviceTest, DestructorClosesRenderNodeDescriptor) {
const wl::gbm_device_accessors_t accessors {
.open_render_node = open_fake_render_node,
.create_device = create_fake_device,
.destroy_device = destroy_fake_device,
};
fake_destroyed_devices() = 0;
int fd = -1;
{
wl::gbm_device_t device;
ASSERT_TRUE(device.init("/dev/dri/renderD128", accessors));
fd = device.fd();
ASSERT_TRUE(descriptor_is_open(fd));
}
EXPECT_EQ(fake_destroyed_devices(), 1);
EXPECT_FALSE(descriptor_is_open(fd));
}
namespace {
/**
* @brief Scripted answer for query_fake_drm_device().
*/
struct fake_drm_query_t {
int result = 0;
drmDevice *device = nullptr;
};
/**
* @brief Scripted answer returned by query_fake_drm_device() (reset per test).
*/
fake_drm_query_t &fake_drm_query() {
static fake_drm_query_t query;
return query;
}
/**
* @brief Count of query_fake_drm_device() calls (reset per test).
*/
int &drm_get_device_calls() {
static int count = 0;
return count;
}
/**
* @brief Count of free_fake_drm_device() calls (reset per test).
*/
int &drm_free_device_calls() {
static int count = 0;
return count;
}
/**
* @brief Fake PCI bus info backing query_fake_drm_device() (reset per test).
*/
drmPciBusInfo &fake_pci_bus_info() {
static drmPciBusInfo bus_info {};
return bus_info;
}
/**
* @brief Fake DRM device backing query_fake_drm_device() (reset per test).
*/
drmDevice &fake_drm_device() {
static drmDevice device {};
return device;
}
int query_fake_drm_device(int, std::uint32_t, _drmDevice **device) {
++drm_get_device_calls();
*device = fake_drm_query().device;
return fake_drm_query().result;
}
void free_fake_drm_device(_drmDevice **device) {
++drm_free_device_calls();
if (device) {
*device = nullptr;
}
}
/**
* @brief Reset the fake DRM query state before a PCI reader test.
*/
void reset_drm_pci_fakes() {
drm_get_device_calls() = 0;
drm_free_device_calls() = 0;
last_fake_render_node() = -1;
fake_pci_bus_info() = drmPciBusInfo {};
fake_drm_device() = drmDevice {};
fake_drm_query() = fake_drm_query_t {};
}
} // namespace
TEST(WaylandPciReaderTest, ReturnsFormattedPciIdentityOnSuccess) {
reset_drm_pci_fakes();
fake_pci_bus_info() = drmPciBusInfo {.domain = 0x00a5, .bus = 0x0f, .dev = 0xa0, .func = 0x1};
fake_drm_device().bustype = DRM_BUS_PCI;
fake_drm_device().businfo.pci = &fake_pci_bus_info();
fake_drm_query() = {.result = 0, .device = &fake_drm_device()};
const wl::drm_device_accessors_t accessors {
.open_render_node = open_fake_render_node,
.get_device = query_fake_drm_device,
.free_device = free_fake_drm_device,
};
const auto bus_id = wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors);
ASSERT_TRUE(bus_id.has_value());
EXPECT_EQ(*bus_id, "00a5:0f:a0.1");
EXPECT_EQ(drm_get_device_calls(), 1);
EXPECT_EQ(drm_free_device_calls(), 1);
const int fd = last_fake_render_node();
ASSERT_GE(fd, 0);
EXPECT_FALSE(descriptor_is_open(fd));
}
TEST(WaylandPciReaderTest, ReturnsNoPciIdentityWhenRenderNodeCannotOpen) {
reset_drm_pci_fakes();
const wl::drm_device_accessors_t accessors {
.open_render_node = fail_to_open_render_node,
.get_device = query_fake_drm_device,
.free_device = free_fake_drm_device,
};
EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value());
EXPECT_EQ(drm_get_device_calls(), 0);
EXPECT_EQ(drm_free_device_calls(), 0);
}
TEST(WaylandPciReaderTest, ReturnsNoPciIdentityWhenDrmQueryFails) {
reset_drm_pci_fakes();
fake_drm_query() = {.result = -1, .device = nullptr};
const wl::drm_device_accessors_t accessors {
.open_render_node = open_fake_render_node,
.get_device = query_fake_drm_device,
.free_device = free_fake_drm_device,
};
EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value());
EXPECT_EQ(drm_get_device_calls(), 1);
EXPECT_EQ(drm_free_device_calls(), 0);
const int fd = last_fake_render_node();
ASSERT_GE(fd, 0);
EXPECT_FALSE(descriptor_is_open(fd));
}
TEST(WaylandPciReaderTest, ReturnsNoPciIdentityWhenDeviceIsMissing) {
reset_drm_pci_fakes();
fake_drm_query() = {.result = 0, .device = nullptr};
const wl::drm_device_accessors_t accessors {
.open_render_node = open_fake_render_node,
.get_device = query_fake_drm_device,
.free_device = free_fake_drm_device,
};
EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value());
EXPECT_EQ(drm_free_device_calls(), 0);
const int fd = last_fake_render_node();
ASSERT_GE(fd, 0);
EXPECT_FALSE(descriptor_is_open(fd));
}
TEST(WaylandPciReaderTest, ReturnsNoPciIdentityForNonPciDevice) {
reset_drm_pci_fakes();
fake_drm_device().bustype = DRM_BUS_PLATFORM;
fake_drm_query() = {.result = 0, .device = &fake_drm_device()};
const wl::drm_device_accessors_t accessors {
.open_render_node = open_fake_render_node,
.get_device = query_fake_drm_device,
.free_device = free_fake_drm_device,
};
EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value());
EXPECT_EQ(drm_free_device_calls(), 1);
const int fd = last_fake_render_node();
ASSERT_GE(fd, 0);
EXPECT_FALSE(descriptor_is_open(fd));
}
TEST(WaylandPciReaderTest, ReturnsNoPciIdentityWhenPciBusInfoMissing) {
reset_drm_pci_fakes();
fake_drm_device().bustype = DRM_BUS_PCI;
fake_drm_device().businfo.pci = nullptr;
fake_drm_query() = {.result = 0, .device = &fake_drm_device()};
const wl::drm_device_accessors_t accessors {
.open_render_node = open_fake_render_node,
.get_device = query_fake_drm_device,
.free_device = free_fake_drm_device,
};
EXPECT_FALSE(wl::render_node_pci_bus_id("/dev/dri/renderD128", accessors).has_value());
EXPECT_EQ(drm_free_device_calls(), 1);
const int fd = last_fake_render_node();
ASSERT_GE(fd, 0);
EXPECT_FALSE(descriptor_is_open(fd));
}
#ifdef SUNSHINE_BUILD_CUDA
namespace {
/**
* @brief Whether the fake CUDA function table counts as loaded (reset per test).
*/
bool &fake_cuda_initialized() {
static bool initialized = true;
return initialized;
}
/**
* @brief Count of fake_cuda_initialize() calls (reset per test).
*/
int &cuda_initialize_calls() {
static int count = 0;
return count;
}
/**
* @brief Result returned by fake_cuda_initialize() (reset per test).
*/
int &cuda_initialize_result() {
static int result = 0;
return result;
}
/**
* @brief Count of fake_cuda_get_device() calls (reset per test).
*/
int &cuda_get_device_calls() {
static int count = 0;
return count;
}
/**
* @brief Result returned by fake_cuda_get_device() (reset per test).
*/
CUresult &cuda_get_device_result() {
static CUresult result = CUDA_SUCCESS;
return result;
}
/**
* @brief Count of fake_cuda_get_pci_bus_id() calls (reset per test).
*/
int &cuda_get_pci_bus_id_calls() {
static int count = 0;
return count;
}
/**
* @brief Result returned by fake_cuda_get_pci_bus_id() (reset per test).
*/
CUresult &cuda_get_pci_bus_id_result() {
static CUresult result = CUDA_SUCCESS;
return result;
}
/**
* @brief Bus id text written by fake_cuda_get_pci_bus_id() (reset per test).
*/
std::string &cuda_fake_bus_id() {
static std::string bus_id;
return bus_id;
}
/**
* @brief Device handle observed by fake_cuda_get_pci_bus_id() (reset per test).
*/
CUdevice &cuda_seen_device() {
static CUdevice device = 0;
return device;
}
bool fake_cuda_is_initialized() {
return fake_cuda_initialized();
}
int fake_cuda_initialize() {
++cuda_initialize_calls();
return cuda_initialize_result();
}
CUresult fake_cuda_get_device(CUdevice *device, int) {
++cuda_get_device_calls();
if (device) {
*device = 7;
}
return cuda_get_device_result();
}
CUresult fake_cuda_get_pci_bus_id(char *pci_bus_id, int len, CUdevice device) {
++cuda_get_pci_bus_id_calls();
cuda_seen_device() = device;
if (cuda_get_pci_bus_id_result() == CUDA_SUCCESS && pci_bus_id != nullptr && len > 0) {
const auto end = std::format_to_n(pci_bus_id, len - 1, "{}", cuda_fake_bus_id()).out;
*end = '\0';
}
return cuda_get_pci_bus_id_result();
}
const cuda::cuda_pci_accessors_t fake_cuda_pci_accessors {
.is_initialized = fake_cuda_is_initialized,
.initialize = fake_cuda_initialize,
.get_device = fake_cuda_get_device,
.get_pci_bus_id = fake_cuda_get_pci_bus_id,
};
/**
* @brief Reset the fake CUDA query state before a PCI reader test.
*/
void reset_cuda_pci_fakes() {
fake_cuda_initialized() = true;
cuda_initialize_calls() = 0;
cuda_initialize_result() = 0;
cuda_get_device_calls() = 0;
cuda_get_device_result() = CUDA_SUCCESS;
cuda_get_pci_bus_id_calls() = 0;
cuda_get_pci_bus_id_result() = CUDA_SUCCESS;
cuda_fake_bus_id().clear();
cuda_seen_device() = 0;
}
} // namespace
TEST(CudaPciReaderTest, ReturnsLowercaseBusIdWithoutReinitializing) {
reset_cuda_pci_fakes();
cuda_fake_bus_id() = "00000000:0F:00.0";
const auto bus_id = cuda::device_pci_bus_id(0, fake_cuda_pci_accessors);
ASSERT_TRUE(bus_id.has_value());
EXPECT_EQ(*bus_id, "00000000:0f:00.0");
EXPECT_EQ(cuda_initialize_calls(), 0);
EXPECT_EQ(cuda_get_device_calls(), 1);
EXPECT_EQ(cuda_get_pci_bus_id_calls(), 1);
EXPECT_EQ(cuda_seen_device(), 7);
}
TEST(CudaPciReaderTest, ReturnsShortBusIdLowercased) {
reset_cuda_pci_fakes();
cuda_fake_bus_id() = "0000:0F:00.0";
const auto bus_id = cuda::device_pci_bus_id(0, fake_cuda_pci_accessors);
ASSERT_TRUE(bus_id.has_value());
EXPECT_EQ(*bus_id, "0000:0f:00.0");
}
TEST(CudaPciReaderTest, InitializesUninitializedFunctionTable) {
reset_cuda_pci_fakes();
fake_cuda_initialized() = false;
cuda_fake_bus_id() = "0000:0F:00.0";
const auto bus_id = cuda::device_pci_bus_id(0, fake_cuda_pci_accessors);
ASSERT_TRUE(bus_id.has_value());
EXPECT_EQ(*bus_id, "0000:0f:00.0");
EXPECT_EQ(cuda_initialize_calls(), 1);
EXPECT_EQ(cuda_get_device_calls(), 1);
}
TEST(CudaPciReaderTest, ReturnsNoBusIdWhenInitFails) {
reset_cuda_pci_fakes();
fake_cuda_initialized() = false;
cuda_initialize_result() = -1;
EXPECT_FALSE(cuda::device_pci_bus_id(0, fake_cuda_pci_accessors).has_value());
EXPECT_EQ(cuda_initialize_calls(), 1);
EXPECT_EQ(cuda_get_device_calls(), 0);
EXPECT_EQ(cuda_get_pci_bus_id_calls(), 0);
}
TEST(CudaPciReaderTest, ReturnsNoBusIdWhenDeviceQueryFails) {
reset_cuda_pci_fakes();
cuda_get_device_result() = CUDA_ERROR_UNKNOWN;
EXPECT_FALSE(cuda::device_pci_bus_id(0, fake_cuda_pci_accessors).has_value());
EXPECT_EQ(cuda_get_device_calls(), 1);
EXPECT_EQ(cuda_get_pci_bus_id_calls(), 0);
}
TEST(CudaPciReaderTest, ReturnsNoBusIdWhenBusIdQueryFails) {
reset_cuda_pci_fakes();
cuda_get_pci_bus_id_result() = CUDA_ERROR_UNKNOWN;
EXPECT_FALSE(cuda::device_pci_bus_id(0, fake_cuda_pci_accessors).has_value());
EXPECT_EQ(cuda_get_pci_bus_id_calls(), 1);
}
#endif
#endif