blob: c8cf04b37a81367aa85296868ee408a8fd026b03 [file]
/*
* Copyright © Canonical Ltd.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "expect_protocol_error.h"
#include "generated/linux-drm-syncobj-v1-client.h"
#include "in_process_server.h"
#include "version_specifier.h"
#include "wl_interface_descriptor.h"
#include "gtest_matchers.h"
#include "xf86drm.h"
#include "fcntl.h"
#include <boost/throw_exception.hpp>
#include <chrono>
#include <drm.h>
#include <fcntl.h>
#include <system_error>
#include <type_traits>
namespace wlcs
{
WLCS_CREATE_INTERFACE_DESCRIPTOR(wp_linux_drm_syncobj_manager_v1)
WLCS_CREATE_INTERFACE_DESCRIPTOR(wp_linux_drm_syncobj_surface_v1)
WLCS_CREATE_INTERFACE_DESCRIPTOR(wp_linux_drm_syncobj_timeline_v1)
}
namespace
{
class Fd
{
public:
Fd(int fd) : fd{fd}
{
if (fd < 0)
{
BOOST_THROW_EXCEPTION(std::logic_error{"Invalid fd"});
}
}
~Fd()
{
if (fd > 0)
{
close(fd);
}
}
Fd(Fd&& from)
: fd{from.fd}
{
from.fd = -1;
}
auto dup() const -> Fd
{
return Fd{::dup(fd)};
}
Fd(Fd const&) = delete;
auto operator=(Fd const&) -> Fd& = delete;
operator int() const
{
return fd;
}
private:
int fd;
};
using DRMDeviceHandle = std::unique_ptr<
std::remove_pointer_t<drmDevicePtr>, decltype([](drmDevicePtr value) { drmFreeDevice(&value); })>;
auto get_drm_devices() -> std::vector<DRMDeviceHandle>
{
std::array<drmDevicePtr, 6> devices;
std::vector<DRMDeviceHandle> result;
if (auto count = drmGetDevices(devices.data(), devices.size()); count > 0)
{
result.reserve(count);
for (auto i = 0; i < count; ++i)
{
result.emplace_back(devices[i]);
}
}
return result;
}
auto open_drm_node(drmDevicePtr device) -> std::optional<Fd>
{
for (auto type : { DRM_NODE_PRIMARY, DRM_NODE_RENDER })
{
if (device->available_nodes & (1 << type))
{
if (auto fd = open(device->nodes[type], O_RDWR | O_CLOEXEC); fd >= 0)
{
return Fd{fd};
}
}
}
return std::nullopt;
}
class DRMSyncobjNotSupported : public std::runtime_error
{
public:
DRMSyncobjNotSupported()
: std::runtime_error{"DRM_CAP_SYNCOBJ_TIMELINE not supported by any available DRM devices."}
{
::testing::Test::RecordProperty("wlcs-skip-test", "DRM_CAP_SYNCOBJ_TIMELINE not supported by any available DRM devices.");
}
};
auto open_timeline_capable_drm_node() -> Fd
{
auto devices = get_drm_devices();
for (auto const& device : devices)
{
if (auto fd = open_drm_node(device.get()))
{
uint64_t value{0};
if (auto err = drmGetCap(*fd, DRM_CAP_SYNCOBJ_TIMELINE, &value))
{
std::cerr << "Failed to query DRM cap: " << strerror(-err) << std::endl;
}
else if (value >= 1)
{
// We've found a DRM device that supports what we need.
return std::move(*fd);
}
}
}
BOOST_THROW_EXCEPTION(DRMSyncobjNotSupported{});
}
}
class LinuxDRMSyncobjV1Test : public wlcs::StartedInProcessServer
{
public:
wlcs::Client a_client{the_server()};
wlcs::WlHandle<wp_linux_drm_syncobj_manager_v1> syncobj_manager{
a_client.bind_if_supported<wp_linux_drm_syncobj_manager_v1>(wlcs::AnyVersion)};
};
namespace
{
class Syncobj
{
public:
Syncobj(Fd drm_fd)
: fd{std::move(drm_fd)},
handle{syncobj_create_checked(fd)}
{
}
~Syncobj()
{
drmSyncobjDestroy(fd, handle);
}
auto get_fd_handle() const -> Fd
{
int timeline_fd{-1};
if (auto err = drmSyncobjHandleToFD(fd, handle, &timeline_fd))
{
std::system_error failure{-err, std::system_category(), "Failed to export DRM Syncobj handle"};
::testing::Test::RecordProperty("wlcs-skip-test", failure.what());
BOOST_THROW_EXCEPTION(failure);
}
return Fd{timeline_fd};
}
void signal(uint64_t point)
{
if (auto err = drmSyncobjTimelineSignal(fd, &handle, &point, 1))
{
BOOST_THROW_EXCEPTION((
std::system_error{
-err,
std::system_category(),
"Failed to signal DRM syncobj"}));
}
}
operator uint32_t() const
{
return handle;
}
private:
static auto syncobj_create_checked(Fd const& drm_fd) -> uint32_t
{
uint32_t timeline_handle;
if (auto err = drmSyncobjCreate(drm_fd, 0, &timeline_handle))
{
std::system_error failure{-err, std::system_category(), "Failed to create DRM Syncobj"};
::testing::Test::RecordProperty("wlcs-skip-test", failure.what());
BOOST_THROW_EXCEPTION(failure);
}
return timeline_handle;
}
Fd const fd;
uint32_t const handle;
};
}
TEST_F(LinuxDRMSyncobjV1Test, can_import_timeline)
{
Syncobj syncobj{open_timeline_capable_drm_node()};
auto timeline_fd = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd)};
a_client.roundtrip();
}
TEST_F(LinuxDRMSyncobjV1Test, request_to_import_non_timeline_fails)
{
using namespace testing;
Fd not_a_syncobj{open("/dev/null", O_RDWR | O_CLOEXEC)};
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, not_a_syncobj)};
EXPECT_PROTOCOL_ERROR(
{
a_client.roundtrip();
},
&wp_linux_drm_syncobj_manager_v1_interface,
WP_LINUX_DRM_SYNCOBJ_MANAGER_V1_ERROR_INVALID_TIMELINE);
}
TEST_F(LinuxDRMSyncobjV1Test, can_get_surface_timeline_object)
{
auto surface = a_client.create_visible_surface(200, 200);
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
a_client.roundtrip();
}
TEST_F(LinuxDRMSyncobjV1Test, can_set_timeline_acquire_point)
{
auto surface = a_client.create_visible_surface(200, 200);
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
Syncobj syncobj{open_timeline_capable_drm_node()};
auto timeline_fd = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd)};
wp_linux_drm_syncobj_surface_v1_set_acquire_point(surface_timeline, timeline, 0, 0);
a_client.roundtrip();
}
TEST_F(LinuxDRMSyncobjV1Test, get_surface_twice_is_an_error)
{
using namespace testing;
auto surface = a_client.create_visible_surface(200, 200);
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
EXPECT_PROTOCOL_ERROR(
{
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> second_timeline_surface{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
a_client.roundtrip();
},
&wp_linux_drm_syncobj_manager_v1_interface,
WP_LINUX_DRM_SYNCOBJ_MANAGER_V1_ERROR_SURFACE_EXISTS
);
}
TEST_F(LinuxDRMSyncobjV1Test, committing_without_setting_acquire_point_is_an_error)
try
{
using namespace testing;
auto surface = a_client.create_visible_surface(200, 200);
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
Syncobj syncobj{open_timeline_capable_drm_node()};
auto timeline_fd = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd)};
wp_linux_drm_syncobj_surface_v1_set_release_point(surface_timeline, timeline, 0, 0);
auto const& buffer = a_client.create_buffer(200, 200);
wl_surface_attach(surface, buffer, 0, 0);
EXPECT_PROTOCOL_ERROR(
{
wl_surface_commit(surface);
a_client.roundtrip();
},
&wp_linux_drm_syncobj_surface_v1_interface,
WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_NO_ACQUIRE_POINT);
}
catch(wlcs::ProtocolError const& err)
{
if (err.interface() == &wp_linux_drm_syncobj_surface_v1_interface &&
err.error_code() == WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_UNSUPPORTED_BUFFER)
{
::testing::Test::RecordProperty("wlcs-skip-test", "wp_linux_drm_syncobj implementation doesn't support wl_shm buffers");
}
throw;
}
TEST_F(LinuxDRMSyncobjV1Test, committing_without_setting_release_point_is_an_error)
try
{
using namespace testing;
auto surface = a_client.create_visible_surface(200, 200);
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
Syncobj syncobj{open_timeline_capable_drm_node()};
auto timeline_fd = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd)};
wp_linux_drm_syncobj_surface_v1_set_acquire_point(surface_timeline, timeline, 0, 0);
auto const& buffer = a_client.create_buffer(200, 200);
wl_surface_attach(surface, buffer, 0, 0);
EXPECT_PROTOCOL_ERROR(
{
wl_surface_commit(surface);
a_client.roundtrip();
},
&wp_linux_drm_syncobj_surface_v1_interface,
WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_NO_RELEASE_POINT);
}
catch(wlcs::ProtocolError const& err)
{
if (err.interface() == &wp_linux_drm_syncobj_surface_v1_interface &&
err.error_code() == WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_UNSUPPORTED_BUFFER)
{
::testing::Test::RecordProperty("wlcs-skip-test", "wp_linux_drm_syncobj implementation doesn't support wl_shm buffers");
}
throw;
}
TEST_F(LinuxDRMSyncobjV1Test, setting_syncpoint_without_buffer_is_error)
try
{
using namespace testing;
auto surface = a_client.create_visible_surface(200, 200);
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
Syncobj syncobj{open_timeline_capable_drm_node()};
auto timeline_fd = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd)};
wp_linux_drm_syncobj_surface_v1_set_acquire_point(surface_timeline, timeline, 0, 0);
wp_linux_drm_syncobj_surface_v1_set_release_point(surface_timeline, timeline, 0, 1);
EXPECT_PROTOCOL_ERROR(
{
wl_surface_commit(surface);
a_client.roundtrip();
},
&wp_linux_drm_syncobj_surface_v1_interface,
WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_NO_BUFFER);
}
catch(wlcs::ProtocolError const& err)
{
if (err.interface() == &wp_linux_drm_syncobj_surface_v1_interface &&
err.error_code() == WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_UNSUPPORTED_BUFFER)
{
::testing::Test::RecordProperty("wlcs-skip-test", "wp_linux_drm_syncobj implementation doesn't support wl_shm buffers");
}
throw;
}
TEST_F(LinuxDRMSyncobjV1Test, committing_without_both_syncpoint_and_buffer_is_valid)
try
{
using namespace testing;
auto surface = a_client.create_visible_surface(200, 200);
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
Syncobj syncobj{open_timeline_capable_drm_node()};
auto timeline_fd = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd)};
/* “Clients must set both acquire and release points if and only if a non-null buffer is
* attached in the same commit”
*
* We don't have a buffer, and we haven't set any release points; this is valid
*/
wl_surface_commit(surface);
a_client.roundtrip();
}
catch(wlcs::ProtocolError const& err)
{
if (err.interface() == &wp_linux_drm_syncobj_surface_v1_interface &&
err.error_code() == WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_UNSUPPORTED_BUFFER)
{
::testing::Test::RecordProperty("wlcs-skip-test", "wp_linux_drm_syncobj implementation doesn't support wl_shm buffers");
}
throw;
}
TEST_F(LinuxDRMSyncobjV1Test, committing_a_null_buffer_without_syncpoint_is_valid)
try
{
using namespace testing;
auto surface = a_client.create_visible_surface(200, 200);
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
Syncobj syncobj{open_timeline_capable_drm_node()};
auto timeline_fd = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd)};
/* “Clients must set both acquire and release points if and only if a non-null buffer is
* attached in the same commit”
*
* We are attaching a NULL buffer, and we haven't set any release points; this is valid
*/
wl_surface_attach(surface, nullptr, 0, 0);
wl_surface_commit(surface);
a_client.roundtrip();
}
catch(wlcs::ProtocolError const& err)
{
if (err.interface() == &wp_linux_drm_syncobj_surface_v1_interface &&
err.error_code() == WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_UNSUPPORTED_BUFFER)
{
::testing::Test::RecordProperty("wlcs-skip-test", "wp_linux_drm_syncobj implementation doesn't support wl_shm buffers");
}
throw;
}
TEST_F(LinuxDRMSyncobjV1Test, release_point_signalled_on_buffer_release)
try
{
using namespace testing;
auto surface = a_client.create_visible_surface(200, 200);
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
auto drm_fd = open_timeline_capable_drm_node();
Syncobj syncobj{drm_fd.dup()};
auto timeline_fd = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd)};
wp_linux_drm_syncobj_surface_v1_set_acquire_point(surface_timeline, timeline, 0, 0);
// The buffer needs to be ready for the commit to be applied.
syncobj.signal(0);
uint64_t const release_point = 42; // This only needs to be >= the acquire point
uint32_t const release_point_lo = release_point & 0xffffffff;
uint32_t const release_point_hi = release_point >> 32;
wp_linux_drm_syncobj_surface_v1_set_release_point(surface_timeline, timeline, release_point_hi, release_point_lo);
// Submit a buffer with the syncobj
wlcs::ShmBuffer buffer_one{a_client, 200, 200};
wl_surface_attach(surface, buffer_one, 0, 0);
wl_surface_commit(surface);
a_client.roundtrip();
// Submit another buffer so that the original buffer is released
Syncobj syncobj_two{drm_fd.dup()};
auto timeline_fd_two = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline_two{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd_two)};
wp_linux_drm_syncobj_surface_v1_set_acquire_point(surface_timeline, timeline_two, 0, 0);
wp_linux_drm_syncobj_surface_v1_set_release_point(surface_timeline, timeline_two, 0, 1);
// We need to actually mark the buffer as ready
syncobj_two.signal(0);
wlcs::ShmBuffer buffer_two{a_client, 200, 200};
wl_surface_attach(surface, buffer_two, 0, 0);
wl_surface_commit(surface);
a_client.roundtrip();
using namespace std::chrono;
using namespace std::chrono_literals;
// The release timepoint should now be signalled at some point.
uint32_t handle = syncobj;
if (auto err = drmSyncobjTimelineWait(drm_fd, &handle, const_cast<uint64_t*>(&release_point), 1, duration_cast<nanoseconds>((steady_clock::now() + 10s).time_since_epoch()).count(), DRM_SYNCOBJ_WAIT_FLAGS_WAIT_FOR_SUBMIT | DRM_SYNCOBJ_WAIT_FLAGS_WAIT_ALL, nullptr))
{
FAIL() << "Error (or timeout) waiting for release syncpoint to be signalled: " << strerror(-err);
}
}
catch(wlcs::ProtocolError const& err)
{
if (err.interface() == &wp_linux_drm_syncobj_surface_v1_interface &&
err.error_code() == WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_UNSUPPORTED_BUFFER)
{
::testing::Test::RecordProperty("wlcs-skip-test", "wp_linux_drm_syncobj implementation doesn't support wl_shm buffers");
}
throw;
}
TEST_F(LinuxDRMSyncobjV1Test, committed_buffer_not_applied_until_acquire_point_signalled)
try
{
using namespace testing;
using namespace std::chrono_literals;
int const original_width = 200, original_height = 200;
int const new_width = 400, new_height = 400;
auto surface = a_client.create_visible_surface(original_width, original_height);
ASSERT_THAT(surface, IsSurfaceOfSize(original_width, original_height));
wlcs::WlHandle<wp_linux_drm_syncobj_surface_v1> surface_timeline{
wp_linux_drm_syncobj_manager_v1_get_surface(syncobj_manager, surface)
};
auto drm_fd = open_timeline_capable_drm_node();
Syncobj syncobj{drm_fd.dup()};
auto timeline_fd = syncobj.get_fd_handle();
wlcs::WlHandle<wp_linux_drm_syncobj_timeline_v1> timeline{
wp_linux_drm_syncobj_manager_v1_import_timeline(syncobj_manager, timeline_fd)};
wp_linux_drm_syncobj_surface_v1_set_acquire_point(surface_timeline, timeline, 0, 0);
wp_linux_drm_syncobj_surface_v1_set_release_point(surface_timeline, timeline, 0, 1);
// Submit a buffer with a different size
wlcs::ShmBuffer buffer_one{a_client, new_width, new_height};
wl_surface_attach(surface, buffer_one, 0, 0);
wl_surface_commit(surface);
a_client.roundtrip();
// We don't have a way to ensure that WM has settled down, so just wait a little
std::this_thread::sleep_for(1s);
// We haven't signalled the acquire point, so we should see the old size.
EXPECT_THAT(surface, IsSurfaceOfSize(original_width, original_height));
syncobj.signal(0);
// Again, we don't have a way to ensure that WM has settled down, so just wait a little
std::this_thread::sleep_for(1s);
EXPECT_THAT(surface,IsSurfaceOfSize(new_width, new_height));
}
catch(wlcs::ProtocolError const& err)
{
if (err.interface() == &wp_linux_drm_syncobj_surface_v1_interface &&
err.error_code() == WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_UNSUPPORTED_BUFFER)
{
::testing::Test::RecordProperty("wlcs-skip-test", "wp_linux_drm_syncobj implementation doesn't support wl_shm buffers");
}
throw;
}