From 34f45eea541c806e0338fe7bae0dccffc37eddc2 Mon Sep 17 00:00:00 2001 From: Christopher James Halse Rogers Date: Mon, 5 Aug 2024 13:11:57 +1000 Subject: [PATCH] platforms: Add atomic-kms platform This is initially a copy of the display half of `gbm-kms`, quickly ported to use only the atomic KMS APIs. It shall be further developed to usefully use the atomic APIs to fix various TODOs, and provide support for extra performance features --- CMakeLists.txt | 13 +- src/platforms/CMakeLists.txt | 4 + src/platforms/atomic-kms/CMakeLists.txt | 1 + .../include/gbm_format_conversions.h | 34 + .../atomic-kms/server/CMakeLists.txt | 24 + .../atomic-kms/server/display_helpers.cpp | 241 ++++++ .../atomic-kms/server/display_helpers.h | 89 +++ .../server/gbm_display_allocator.cpp | 214 +++++ .../atomic-kms/server/gbm_display_allocator.h | 37 + .../atomic-kms/server/kms/CMakeLists.txt | 82 ++ .../server/kms/atomic_kms_output.cpp | 739 ++++++++++++++++++ .../atomic-kms/server/kms/atomic_kms_output.h | 93 +++ .../atomic-kms/server/kms/bypass.cpp | 46 ++ src/platforms/atomic-kms/server/kms/bypass.h | 44 ++ .../atomic-kms/server/kms/display.cpp | 497 ++++++++++++ src/platforms/atomic-kms/server/kms/display.h | 131 ++++ .../atomic-kms/server/kms/display_buffer.cpp | 357 +++++++++ .../atomic-kms/server/kms/display_sink.h | 127 +++ .../atomic-kms/server/kms/egl_helper.cpp | 286 +++++++ .../atomic-kms/server/kms/egl_helper.h | 87 +++ .../server/kms/kms_display_configuration.h | 47 ++ .../atomic-kms/server/kms/kms_output.h | 107 +++ .../server/kms/kms_output_container.h | 53 ++ .../server/kms/kms_page_flipper.cpp | 188 +++++ .../atomic-kms/server/kms/kms_page_flipper.h | 76 ++ .../atomic-kms/server/kms/platform.cpp | 159 ++++ .../atomic-kms/server/kms/platform.h | 82 ++ .../server/kms/platform_symbols.cpp | 322 ++++++++ .../atomic-kms/server/kms/quirks.cpp | 204 +++++ src/platforms/atomic-kms/server/kms/quirks.h | 67 ++ .../kms/real_kms_display_configuration.cpp | 235 ++++++ .../kms/real_kms_display_configuration.h | 65 ++ .../server/kms/real_kms_output_container.cpp | 74 ++ .../server/kms/real_kms_output_container.h | 53 ++ .../atomic-kms/server/kms/symbols.map.in | 9 + .../atomic-kms/server/platform_common.h | 36 + tests/mir_test_doubles/CMakeLists.txt | 2 +- tests/unit-tests/CMakeLists.txt | 4 + .../graphics/test_platform_prober.cpp | 25 +- 39 files changed, 4936 insertions(+), 18 deletions(-) create mode 100644 src/platforms/atomic-kms/CMakeLists.txt create mode 100644 src/platforms/atomic-kms/include/gbm_format_conversions.h create mode 100644 src/platforms/atomic-kms/server/CMakeLists.txt create mode 100644 src/platforms/atomic-kms/server/display_helpers.cpp create mode 100644 src/platforms/atomic-kms/server/display_helpers.h create mode 100644 src/platforms/atomic-kms/server/gbm_display_allocator.cpp create mode 100644 src/platforms/atomic-kms/server/gbm_display_allocator.h create mode 100644 src/platforms/atomic-kms/server/kms/CMakeLists.txt create mode 100644 src/platforms/atomic-kms/server/kms/atomic_kms_output.cpp create mode 100644 src/platforms/atomic-kms/server/kms/atomic_kms_output.h create mode 100644 src/platforms/atomic-kms/server/kms/bypass.cpp create mode 100644 src/platforms/atomic-kms/server/kms/bypass.h create mode 100644 src/platforms/atomic-kms/server/kms/display.cpp create mode 100644 src/platforms/atomic-kms/server/kms/display.h create mode 100644 src/platforms/atomic-kms/server/kms/display_buffer.cpp create mode 100644 src/platforms/atomic-kms/server/kms/display_sink.h create mode 100644 src/platforms/atomic-kms/server/kms/egl_helper.cpp create mode 100644 src/platforms/atomic-kms/server/kms/egl_helper.h create mode 100644 src/platforms/atomic-kms/server/kms/kms_display_configuration.h create mode 100644 src/platforms/atomic-kms/server/kms/kms_output.h create mode 100644 src/platforms/atomic-kms/server/kms/kms_output_container.h create mode 100644 src/platforms/atomic-kms/server/kms/kms_page_flipper.cpp create mode 100644 src/platforms/atomic-kms/server/kms/kms_page_flipper.h create mode 100644 src/platforms/atomic-kms/server/kms/platform.cpp create mode 100644 src/platforms/atomic-kms/server/kms/platform.h create mode 100644 src/platforms/atomic-kms/server/kms/platform_symbols.cpp create mode 100644 src/platforms/atomic-kms/server/kms/quirks.cpp create mode 100644 src/platforms/atomic-kms/server/kms/quirks.h create mode 100644 src/platforms/atomic-kms/server/kms/real_kms_display_configuration.cpp create mode 100644 src/platforms/atomic-kms/server/kms/real_kms_display_configuration.h create mode 100644 src/platforms/atomic-kms/server/kms/real_kms_output_container.cpp create mode 100644 src/platforms/atomic-kms/server/kms/real_kms_output_container.h create mode 100644 src/platforms/atomic-kms/server/kms/symbols.map.in create mode 100644 src/platforms/atomic-kms/server/platform_common.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b6fa998d44..f78f843fdc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,18 +161,18 @@ pkg_check_modules(WAYLAND_EGLSTREAM wayland-eglstream IMPORTED_TARGET) if (WAYLAND_EGLSTREAM_FOUND) set( MIR_PLATFORM - gbm-kms;x11;eglstream-kms;wayland + gbm-kms;atomic-kms;x11;eglstream-kms;wayland CACHE STRING - "a list of graphics backends to build (options are 'gbm-kms', 'x11', 'eglstream-kms', or 'wayland')" + "a list of graphics backends to build (options are 'gbm-kms', 'atomic-kms', 'x11', 'eglstream-kms', or 'wayland')" ) else() set( MIR_PLATFORM - gbm-kms;x11;wayland + gbm-kms;atomic-kms;x11;wayland CACHE STRING - "a list of graphics backends to build (options are 'gbm-kms', 'x11', 'eglstream-kms', or 'wayland')" + "a list of graphics backends to build (options are 'gbm-kms', 'atomic-kms', 'x11', 'eglstream-kms', or 'wayland')" ) endif() @@ -209,6 +209,9 @@ foreach(platform IN LISTS MIR_PLATFORM) if (platform STREQUAL "gbm-kms") set(MIR_BUILD_PLATFORM_GBM_KMS TRUE) endif() + if (platform STREQUAL "atomic-kms") + set(MIR_BUILD_PLATFORM_ATOMIC_KMS TRUE) + endif() if (platform STREQUAL "x11") set(MIR_BUILD_PLATFORM_X11 TRUE) endif() @@ -263,7 +266,7 @@ if (HAVE_SYS_SDT_H) add_compile_definitions(LTTNG_UST_HAVE_SDT_INTEGRATION) endif() -if (MIR_BUILD_PLATFORM_GBM_KMS) +if (MIR_BUILD_PLATFORM_GBM_KMS OR MIR_BUILD_PLATFORM_ATOMIC_KMS) pkg_check_modules(GBM REQUIRED IMPORTED_TARGET gbm>=11.0.0) endif() diff --git a/src/platforms/CMakeLists.txt b/src/platforms/CMakeLists.txt index f2852a7550f..fd5f6a585f0 100644 --- a/src/platforms/CMakeLists.txt +++ b/src/platforms/CMakeLists.txt @@ -49,6 +49,10 @@ if (MIR_BUILD_PLATFORM_GBM_KMS) add_subdirectory(gbm-kms/) endif() +if (MIR_BUILD_PLATFORM_ATOMIC_KMS) + add_subdirectory(atomic-kms/) +endif() + if (MIR_BUILD_PLATFORM_X11) add_subdirectory(x11/) endif() diff --git a/src/platforms/atomic-kms/CMakeLists.txt b/src/platforms/atomic-kms/CMakeLists.txt new file mode 100644 index 00000000000..f510a73aec3 --- /dev/null +++ b/src/platforms/atomic-kms/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(server/) diff --git a/src/platforms/atomic-kms/include/gbm_format_conversions.h b/src/platforms/atomic-kms/include/gbm_format_conversions.h new file mode 100644 index 00000000000..42f78875ad2 --- /dev/null +++ b/src/platforms/atomic-kms/include/gbm_format_conversions.h @@ -0,0 +1,34 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + +#ifndef MIR_GRAPHICS_GBM_GBM_FORMAT_CONVERSIONS_H_ +#define MIR_GRAPHICS_GBM_GBM_FORMAT_CONVERSIONS_H_ + +#include +#include +#include + +namespace mir +{ +namespace graphics +{ +namespace atomic +{ +enum : uint32_t { invalid_atomic_format = std::numeric_limits::max() }; + +} +} +} +#endif /* MIR_GRAPHICS_GBM_GBM_FORMAT_CONVERSIONS_H_ */ diff --git a/src/platforms/atomic-kms/server/CMakeLists.txt b/src/platforms/atomic-kms/server/CMakeLists.txt new file mode 100644 index 00000000000..188a8d46f4c --- /dev/null +++ b/src/platforms/atomic-kms/server/CMakeLists.txt @@ -0,0 +1,24 @@ +add_subdirectory(kms/) + +add_library( + mirsharedatomickmscommon-static STATIC + + display_helpers.cpp + gbm_display_allocator.h + gbm_display_allocator.cpp +) + +target_include_directories( + mirsharedatomickmscommon-static + PUBLIC + ${server_common_include_dirs} + ${CMAKE_CURRENT_BINARY_DIR} +) + +target_link_libraries( + mirsharedatomickmscommon-static + + server_platform_common + kms_utils + mirplatform +) diff --git a/src/platforms/atomic-kms/server/display_helpers.cpp b/src/platforms/atomic-kms/server/display_helpers.cpp new file mode 100644 index 00000000000..ea4d29655a3 --- /dev/null +++ b/src/platforms/atomic-kms/server/display_helpers.cpp @@ -0,0 +1,241 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "display_helpers.h" +#include "one_shot_device_observer.h" + +#include "kms-utils/drm_mode_resources.h" +#include "mir/graphics/egl_error.h" +#include "kms/quirks.h" + +#include "mir/udev/wrapper.h" +#include "mir/console_services.h" + +#include + +#define MIR_LOG_COMPONENT "atomic-kms" +#include "mir/log.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mg = mir::graphics; +namespace mga = mir::graphics::atomic; +namespace mgc = mir::graphics::common; +namespace mgmh = mir::graphics::atomic::helpers; + +/************* + * DRMHelper * + *************/ + +std::vector> +mgmh::DRMHelper::open_all_devices( + std::shared_ptr const& udev, + mir::ConsoleServices& console, + mga::Quirks const& quirks) +{ + int error = ENODEV; //Default error is "there are no DRM devices" + + mir::udev::Enumerator devices(udev); + devices.match_subsystem("drm"); + devices.match_sysname("card[0-9]"); + + devices.scan_devices(); + + std::vector> opened_devices; + + for(auto& device : devices) + { + if (quirks.should_skip(device)) + { + mir::log_info("Ignoring device %s due to specified quirk", device.devnode()); + continue; + } + + mir::Fd tmp_fd; + std::unique_ptr device_handle; + try + { + device_handle = console.acquire_device( + major(device.devnum()), minor(device.devnum()), + std::make_unique(tmp_fd)) + .get(); + } + catch (std::exception const& error) + { + mir::log_warning( + "Failed to open DRM device node %s: %s", + device.devnode(), + boost::diagnostic_information(error).c_str()); + continue; + } + + // Paranoia is always justified when dealing with hardware interfaces… + if (tmp_fd == mir::Fd::invalid) + { + mir::log_critical( + "Opening the DRM device %s succeeded, but provided an invalid descriptor!", + device.devnode()); + mir::log_critical( + "This is probably a logic error in Mir, please report to https://github.com/MirServer/mir/issues"); + continue; + } + + // Check that the drm device is usable by setting the interface version we use (1.4) + drmSetVersion sv; + sv.drm_di_major = 1; + sv.drm_di_minor = 4; + sv.drm_dd_major = -1; /* Don't care */ + sv.drm_dd_minor = -1; /* Don't care */ + + if ((error = -drmSetInterfaceVersion(tmp_fd, &sv))) + { + mir::log_warning( + "Failed to set DRM interface version on device %s: %i (%s)", + device.devnode(), + error, + strerror(error)); + continue; + } + + auto busid = std::unique_ptr{ + drmGetBusid(tmp_fd), &drmFreeBusid + }; + + if (!busid) + { + mir::log_warning( + "Failed to query BusID for device %s; cannot check if KMS is available", + device.devnode()); + } + else + { + switch (auto err = -drmCheckModesettingSupported(busid.get())) + { + case 0: break; + + case ENOSYS: + if (quirks.require_modesetting_support(device)) + { + mir::log_info("Ignoring non-KMS DRM device %s", device.devnode()); + error = ENOSYS; + continue; + } + + [[fallthrough]]; + case EINVAL: + mir::log_warning( + "Failed to detect whether device %s supports KMS, but continuing anyway", + device.devnode()); + break; + + default: + mir::log_warning("Unexpected error from drmCheckModesettingSupported(): %s (%i), but continuing anyway", + strerror(err), err); + mir::log_warning("Please file a bug at https://github.com/MirServer/mir/issues containing this message"); + } + } + + // Can't use make_shared with the private constructor. + opened_devices.push_back( + std::shared_ptr{ + new DRMHelper{ + std::move(tmp_fd), + std::move(device_handle)}}); + mir::log_info("Using DRM device %s", device.devnode()); + } + + if (opened_devices.size() == 0) + { + BOOST_THROW_EXCEPTION(( + std::system_error{error, std::system_category(), "Error opening DRM device"})); + } + + return opened_devices; +} + +std::unique_ptr mgmh::DRMHelper::open_any_render_node( + std::shared_ptr const& udev) +{ + mir::Fd tmp_fd; + int error = ENODEV; //Default error is "there are no DRM devices" + + mir::udev::Enumerator devices(udev); + devices.match_subsystem("drm"); + devices.match_sysname("renderD[0-9]*"); + + devices.scan_devices(); + + for(auto& device : devices) + { + // If directly opening the DRM device is good enough for X it's good enough for us! + tmp_fd = mir::Fd{open(device.devnode(), O_RDWR | O_CLOEXEC)}; + if (tmp_fd < 0) + { + error = errno; + continue; + } + + break; + } + + if (tmp_fd < 0) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + error, + std::system_category(), + "Error opening DRM device"})); + } + + return std::unique_ptr{ + new mgmh::DRMHelper{std::move(tmp_fd), nullptr}}; +} + +mgmh::DRMHelper::DRMHelper(mir::Fd&& fd, std::unique_ptr device) + : fd{std::move(fd)}, + device_handle{std::move(device)} +{ +} + +mgmh::DRMHelper::~DRMHelper() +{ +} + +/************* + * GBMHelper * + *************/ + +mgmh::GBMHelper::GBMHelper(mir::Fd const& drm_fd) + : device{gbm_create_device(drm_fd)} +{ + if (!device) + BOOST_THROW_EXCEPTION( + std::runtime_error("Failed to create GBM device")); +} + +mgmh::GBMHelper::~GBMHelper() +{ + if (device) + gbm_device_destroy(device); +} diff --git a/src/platforms/atomic-kms/server/display_helpers.h b/src/platforms/atomic-kms/server/display_helpers.h new file mode 100644 index 00000000000..6429da51da5 --- /dev/null +++ b/src/platforms/atomic-kms/server/display_helpers.h @@ -0,0 +1,89 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_ATOMIC_DISPLAY_HELPERS_H_ +#define MIR_GRAPHICS_ATOMIC_DISPLAY_HELPERS_H_ + +#include "mir/udev/wrapper.h" +#include "mir/fd.h" + +#include +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic warning "-Wall" +#include +#pragma GCC diagnostic pop + +#include + +namespace mir +{ +class ConsoleServices; +class Device; + +namespace graphics +{ +namespace atomic +{ +class Quirks; + +typedef std::unique_ptr> GBMSurfaceUPtr; + +namespace helpers +{ + +class DRMHelper +{ +public: + ~DRMHelper(); + + DRMHelper(const DRMHelper &) = delete; + DRMHelper& operator=(const DRMHelper&) = delete; + + static std::vector> open_all_devices( + std::shared_ptr const& udev, + mir::ConsoleServices& console, + Quirks const& quirks); + + static std::unique_ptr open_any_render_node( + std::shared_ptr const& udev); + + mir::Fd fd; +private: + std::unique_ptr const device_handle; + + explicit DRMHelper(mir::Fd&& fd, std::unique_ptr device); +}; + +class GBMHelper +{ +public: + GBMHelper(mir::Fd const& drm_fd); + ~GBMHelper(); + + GBMHelper(const GBMHelper&) = delete; + GBMHelper& operator=(const GBMHelper&) = delete; + + gbm_device* const device; +}; + +} +} +} +} +#endif /* MIR_GRAPHICS_ATOMIC_DISPLAY_HELPERS_H_ */ diff --git a/src/platforms/atomic-kms/server/gbm_display_allocator.cpp b/src/platforms/atomic-kms/server/gbm_display_allocator.cpp new file mode 100644 index 00000000000..4e515b798ad --- /dev/null +++ b/src/platforms/atomic-kms/server/gbm_display_allocator.cpp @@ -0,0 +1,214 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "gbm_display_allocator.h" +#include "kms_framebuffer.h" + +#include +#include +#include + +namespace mg = mir::graphics; +namespace mga = mir::graphics::atomic; +namespace geom = mir::geometry; + +mga::GBMDisplayAllocator::GBMDisplayAllocator(mir::Fd drm_fd, std::shared_ptr gbm, geom::Size size) + : fd{std::move(drm_fd)}, + gbm{std::move(gbm)}, + size{size} +{ +} + +auto mga::GBMDisplayAllocator::supported_formats() const -> std::vector +{ + // TODO: Pull out of KMS plane info + return { DRMFormat{DRM_FORMAT_XRGB8888}, DRMFormat{DRM_FORMAT_ARGB8888}}; +} + +auto mga::GBMDisplayAllocator::modifiers_for_format(DRMFormat /*format*/) const -> std::vector +{ + // TODO: Pull out off KMS plane info + return {}; +} + +namespace +{ +using LockedFrontBuffer = std::unique_ptr>; + +class GBMBoFramebuffer : public mg::FBHandle +{ +public: + static auto framebuffer_for_frontbuffer(mir::Fd const& drm_fd, LockedFrontBuffer bo) -> std::unique_ptr + { + if (auto cached_fb = static_cast*>(gbm_bo_get_user_data(bo.get()))) + { + return std::unique_ptr{new GBMBoFramebuffer{std::move(bo), *cached_fb}}; + } + + auto fb_id = new std::shared_ptr{ + new uint32_t{0}, + [drm_fd](uint32_t* fb_id) + { + if (*fb_id) + { + drmModeRmFB(drm_fd, *fb_id); + } + delete fb_id; + }}; + uint32_t handles[4] = {gbm_bo_get_handle(bo.get()).u32, 0, 0, 0}; + uint32_t strides[4] = {gbm_bo_get_stride(bo.get()), 0, 0, 0}; + uint32_t offsets[4] = {gbm_bo_get_offset(bo.get(), 0), 0, 0, 0}; + + auto format = gbm_bo_get_format(bo.get()); + + auto const width = gbm_bo_get_width(bo.get()); + auto const height = gbm_bo_get_height(bo.get()); + + /* Create a KMS FB object with the gbm_bo attached to it. */ + auto ret = drmModeAddFB2(drm_fd, width, height, format, + handles, strides, offsets, fb_id->get(), 0); + if (ret) + return nullptr; + + gbm_bo_set_user_data(bo.get(), fb_id, [](gbm_bo*, void* fb_ptr) { delete static_cast*>(fb_ptr); }); + + return std::unique_ptr{new GBMBoFramebuffer{std::move(bo), *fb_id}}; + } + + operator uint32_t() const override + { + return *fb_id; + } + + auto size() const -> geom::Size override + { + return + geom::Size{ + gbm_bo_get_width(bo.get()), + gbm_bo_get_height(bo.get())}; + } +private: + GBMBoFramebuffer(LockedFrontBuffer bo, std::shared_ptr fb) + : bo{std::move(bo)}, + fb_id{std::move(fb)} + { + } + + LockedFrontBuffer const bo; + std::shared_ptr const fb_id; +}; + +namespace +{ +auto create_gbm_surface(gbm_device* gbm, geom::Size size, mg::DRMFormat format, std::span modifiers) + -> std::shared_ptr +{ + auto const surface = + [&]() + { + if (modifiers.empty()) + { + // If we have no no modifiers don't use the with-modifiers creation path. + auto foo = GBM_BO_USE_RENDERING | GBM_BO_USE_SCANOUT; + return gbm_surface_create( + gbm, + size.width.as_uint32_t(), size.height.as_uint32_t(), + format, + foo); + } + else + { + return gbm_surface_create_with_modifiers2( + gbm, + size.width.as_uint32_t(), size.height.as_uint32_t(), + format, + modifiers.data(), + modifiers.size(), + GBM_BO_USE_RENDERING | GBM_BO_USE_SCANOUT); + } + }(); + + if (!surface) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + errno, + std::system_category(), + "Failed to create GBM surface"})); + + } + return std::shared_ptr{ + surface, + [](auto surface) { gbm_surface_destroy(surface); }}; +} +} + +class GBMSurfaceImpl : public mga::GBMDisplayAllocator::GBMSurface +{ +public: + GBMSurfaceImpl(mir::Fd drm_fd, gbm_device* gbm, geom::Size size, mg::DRMFormat const format, std::span modifiers) + : drm_fd{std::move(drm_fd)}, + surface{create_gbm_surface(gbm, size, format, modifiers)} + { + } + + GBMSurfaceImpl(GBMSurfaceImpl const&) = delete; + auto operator=(GBMSurfaceImpl const&) -> GBMSurfaceImpl const& = delete; + + operator gbm_surface*() const override + { + return surface.get(); + } + + auto claim_framebuffer() -> std::unique_ptr override + { + if (!gbm_surface_has_free_buffers(surface.get())) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + EBUSY, + std::system_category(), + "Too many buffers consumed from GBM surface"})); + } + + LockedFrontBuffer bo{ + gbm_surface_lock_front_buffer(surface.get()), + [shared_surface = surface](gbm_bo* bo) { gbm_surface_release_buffer(shared_surface.get(), bo); }}; + + if (!bo) + { + BOOST_THROW_EXCEPTION((std::runtime_error{"Failed to acquire GBM front buffer"})); + } + + auto fb = GBMBoFramebuffer::framebuffer_for_frontbuffer(drm_fd, std::move(bo)); + if (!fb) + { + BOOST_THROW_EXCEPTION((std::system_error{errno, std::system_category(), "Failed to make DRM FB"})); + } + return fb; + } +private: + mir::Fd const drm_fd; + std::shared_ptr const surface; +}; +} + +auto mga::GBMDisplayAllocator::make_surface(DRMFormat format, std::span modifiers) + -> std::unique_ptr +{ + return std::make_unique(fd, gbm.get(), size, format, modifiers); +} + diff --git a/src/platforms/atomic-kms/server/gbm_display_allocator.h b/src/platforms/atomic-kms/server/gbm_display_allocator.h new file mode 100644 index 00000000000..e9a08b41385 --- /dev/null +++ b/src/platforms/atomic-kms/server/gbm_display_allocator.h @@ -0,0 +1,37 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "mir/graphics/platform.h" +#include "mir/fd.h" + +namespace mir::graphics::atomic +{ +class GBMDisplayAllocator : public graphics::GBMDisplayAllocator +{ +public: + GBMDisplayAllocator(mir::Fd drm_fd, std::shared_ptr atomic, geometry::Size size); + + auto supported_formats() const -> std::vector override; + + auto modifiers_for_format(DRMFormat format) const -> std::vector override; + + auto make_surface(DRMFormat format, std::span modifier) -> std::unique_ptr override; +private: + mir::Fd const fd; + std::shared_ptr const gbm; + geometry::Size const size; +}; +} diff --git a/src/platforms/atomic-kms/server/kms/CMakeLists.txt b/src/platforms/atomic-kms/server/kms/CMakeLists.txt new file mode 100644 index 00000000000..537fe92599a --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/CMakeLists.txt @@ -0,0 +1,82 @@ +include_directories( + ${server_common_include_dirs} + .. +) + +include_directories( + ${CMAKE_CURRENT_BINARY_DIR}/../ +) + +# gbm-kms.h and drm.h have trailing commas at the end of enum definitions +# This is valid C99, but g++ 4.4 flags it as an error with -pedantic +string(REPLACE "-pedantic" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) + +add_compile_definitions( + __GBM__ + MIR_LOG_COMPONENT_FALLBACK="atomic-kms" +) + +add_library( + mirplatformgraphicsatomickmsobjects OBJECT + + atomic_kms_output.cpp + atomic_kms_output.h + bypass.cpp + display.cpp + display_buffer.cpp + platform.cpp + kms_display_configuration.h + real_kms_display_configuration.cpp + kms_output.h + kms_output_container.h + real_kms_output_container.cpp + egl_helper.h + egl_helper.cpp + quirks.cpp + quirks.h +) + +target_link_libraries( + mirplatformgraphicsatomickmsobjects + + PRIVATE + mirsharedatomickmscommon-static + ${GBM_LDFLAGS} ${GBM_LIBRARIES} +) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/symbols.map.in + ${CMAKE_CURRENT_BINARY_DIR}/symbols.map + ) +set(symbol_map ${CMAKE_CURRENT_BINARY_DIR}/symbols.map) + +add_library(mirplatformgraphicsatomickms MODULE + platform_symbols.cpp +) + +target_link_libraries( + mirplatformgraphicsatomickms + + PRIVATE + mirplatform + mirplatformgraphicsatomickmsobjects + mirsharedatomickmscommon-static + Boost::program_options + Boost::iostreams + PkgConfig::DRM + PkgConfig::GBM + PkgConfig::EGL + PkgConfig::GLESv2 + PkgConfig::WAYLAND_SERVER +) + +set_target_properties( + mirplatformgraphicsatomickms PROPERTIES + OUTPUT_NAME graphics-atomic-kms + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/server-modules + PREFIX "" + SUFFIX ".so.${MIR_SERVER_GRAPHICS_PLATFORM_ABI}" + LINK_FLAGS "-Wl,--exclude-libs=ALL -Wl,--version-script,${symbol_map}" + LINK_DEPENDS ${symbol_map} +) + +install(TARGETS mirplatformgraphicsatomickms LIBRARY DESTINATION ${MIR_SERVER_PLATFORM_PATH}) diff --git a/src/platforms/atomic-kms/server/kms/atomic_kms_output.cpp b/src/platforms/atomic-kms/server/kms/atomic_kms_output.cpp new file mode 100644 index 00000000000..5d487eea541 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/atomic_kms_output.cpp @@ -0,0 +1,739 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "atomic_kms_output.h" +#include "kms-utils/drm_mode_resources.h" +#include "kms_framebuffer.h" +#include "mir/graphics/display_configuration.h" +#include "mir/graphics/gamma_curves.h" +#include "mir_toolkit/common.h" +#include "kms-utils/kms_connector.h" +#include "mir/fatal.h" +#include "mir/log.h" +#include +#include +#include // strcmp + +#include +#include +#include +#include + +namespace mg = mir::graphics; +namespace mga = mg::atomic; +namespace mgk = mg::kms; +namespace geom = mir::geometry; + + +namespace +{ +bool kms_modes_are_equal(drmModeModeInfo const* info1, drmModeModeInfo const* info2) +{ + return (info1 && info2) && + info1->clock == info2->clock && + info1->hdisplay == info2->hdisplay && + info1->hsync_start == info2->hsync_start && + info1->hsync_end == info2->hsync_end && + info1->htotal == info2->htotal && + info1->hskew == info2->hskew && + info1->vdisplay == info2->vdisplay && + info1->vsync_start == info2->vsync_start && + info1->vsync_end == info2->vsync_end && + info1->vtotal == info2->vtotal; +} + +uint32_t create_blob_returning_handle(mir::Fd const& drm_fd, void const* data, size_t len) +{ + uint32_t handle; + if (auto err = drmModeCreatePropertyBlob(drm_fd, data, len, &handle)) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + -err, + std::system_category(), + "Failed to create DRM property blob"})); + } + return handle; +} + +class PropertyBlobData +{ +public: + PropertyBlobData(mir::Fd const& drm_fd, uint32_t handle) + : ptr{drmModeGetPropertyBlob(drm_fd, handle)} + { + if (!ptr) + { + // drmModeGetPropertyBlob sets errno on failure, except on allocation failure + auto const err = errno ? errno : ENOMEM; + BOOST_THROW_EXCEPTION(( + std::system_error{ + err, + std::system_category(), + "Failed to read DRM property blob"})); + } + } + + ~PropertyBlobData() + { + drmModeFreePropertyBlob(ptr); + } + + template + auto data() const -> std::span + { + /* This is a precondition check, so technically unnecessary. + * That said, there are a bunch of moving parts here, so + * let's be nice and check what little we can. + */ + if (ptr->length % sizeof(T) != 0) + { + BOOST_THROW_EXCEPTION(( + std::runtime_error{ + std::format("DRM property size {} is not a multiple of expected object size {}", + ptr->length, + sizeof(T))})); + } + + /* We don't have to care about alignment, at least; libdrm will + * have copied the data into a suitably-aligned allocation + */ + return std::span{static_cast(ptr->data), ptr->length / sizeof(T)}; + } +private: + drmModePropertyBlobPtr const ptr; +}; + +class AtomicUpdate +{ +public: + AtomicUpdate() + : req{drmModeAtomicAlloc()} + { + if (!req) + { + BOOST_THROW_EXCEPTION(( + std::runtime_error{"Failed to allocate Atomic DRM update request"})); + } + } + + ~AtomicUpdate() + { + drmModeAtomicFree(req); + } + + operator drmModeAtomicReqPtr() const + { + return req; + } + + void add_property(mgk::ObjectProperties const& properties, char const* prop_name, uint64_t value) + { + drmModeAtomicAddProperty(req, properties.parent_id(), properties.id_for(prop_name), value); + } +private: + drmModeAtomicReqPtr const req; +}; +} + +class mga::AtomicKMSOutput::PropertyBlob +{ +public: + PropertyBlob(mir::Fd drm_fd, void const* data, size_t size) + : handle_{create_blob_returning_handle(drm_fd, data, size)}, + drm_fd{std::move(drm_fd)} + { + } + + ~PropertyBlob() + { + if (auto err = drmModeDestroyPropertyBlob(drm_fd, handle_)) + { + mir::log_warning( + "Failed to free DRM property blob: %s (%i)", + strerror(-err), + -err); + } + } + + uint32_t handle() const + { + return handle_; + } +private: + uint32_t const handle_; + mir::Fd const drm_fd; +}; + +mga::AtomicKMSOutput::AtomicKMSOutput( + mir::Fd drm_master, + kms::DRMModeConnectorUPtr connector) + : drm_fd_{drm_master}, + connector{std::move(connector)}, + mode_index{0}, + current_crtc(), + saved_crtc(), + using_saved_crtc{true} +{ + reset(); + + kms::DRMModeResources resources{drm_fd_}; + + if (this->connector->encoder_id) + { + auto encoder = resources.encoder(this->connector->encoder_id); + if (encoder->crtc_id) + { + saved_crtc = *resources.crtc(encoder->crtc_id); + } + } +} + +mga::AtomicKMSOutput::~AtomicKMSOutput() +{ + restore_saved_crtc(); +} + +uint32_t mga::AtomicKMSOutput::id() const +{ + return connector->connector_id; +} + +void mga::AtomicKMSOutput::reset() +{ + kms::DRMModeResources resources{drm_fd_}; + + /* Update the connector to ensure we have the latest information */ + try + { + connector = resources.connector(connector->connector_id); + connector_props = std::make_unique(drm_fd_, connector); + } + catch (std::exception const& e) + { + fatal_error(e.what()); + } + + /* Discard previously current crtc */ + current_crtc = nullptr; +} + +geom::Size mga::AtomicKMSOutput::size() const +{ + // Disconnected hardware has no modes: invent a size + if (connector->connection == DRM_MODE_DISCONNECTED) + return {0, 0}; + + drmModeModeInfo const& mode(connector->modes[mode_index]); + return {mode.hdisplay, mode.vdisplay}; +} + +int mga::AtomicKMSOutput::max_refresh_rate() const +{ + if (connector->connection == DRM_MODE_DISCONNECTED) + return 1; + + drmModeModeInfo const& current_mode = connector->modes[mode_index]; + return current_mode.vrefresh; +} + +void mga::AtomicKMSOutput::configure(geom::Displacement offset, size_t kms_mode_index) +{ + fb_offset = offset; + mode_index = kms_mode_index; + mode = std::make_unique(drm_fd_, &connector->modes[mode_index], sizeof(connector->modes[mode_index])); + ensure_crtc(); +} + +bool mga::AtomicKMSOutput::set_crtc(FBHandle const& fb) +{ + if (!ensure_crtc()) + { + mir::log_error("Output %s has no associated CRTC to set a framebuffer on", + mgk::connector_name(connector).c_str()); + return false; + } + + auto const width = current_crtc->width; + auto const height = current_crtc->height; + + AtomicUpdate update; + update.add_property(*crtc_props, "MODE_ID", mode->handle()); + update.add_property(*connector_props, "CRTC_ID", current_crtc->crtc_id); + + /* Source viewport. Coordinates are 16.16 fixed point format */ + update.add_property(*plane_props, "SRC_X", fb_offset.dx.as_uint32_t() << 16); + update.add_property(*plane_props, "SRC_Y", fb_offset.dy.as_uint32_t() << 16); + update.add_property(*plane_props, "SRC_W", width << 16); + update.add_property(*plane_props, "SRC_H", height << 16); + + /* Destination viewport. Coordinates are *not* 16.16 */ + update.add_property(*plane_props, "CRTC_X", 0); + update.add_property(*plane_props, "CRTC_Y", 0); + update.add_property(*plane_props, "CRTC_W", width); + update.add_property(*plane_props, "CRTC_H", height); + + /* Set a surface for the plane */ + update.add_property(*plane_props, "CRTC_ID", current_crtc->crtc_id); + update.add_property(*plane_props, "FB_ID", fb); + + auto ret = drmModeAtomicCommit(drm_fd_, update, DRM_MODE_ATOMIC_ALLOW_MODESET, nullptr); + if (ret) + { + mir::log_error("Failed to set CRTC: %s (%i)", strerror(-ret), -ret); + current_crtc = nullptr; + return false; + } + + using_saved_crtc = false; + return true; +} + +bool mga::AtomicKMSOutput::has_crtc_mismatch() +{ + if (!ensure_crtc()) + { + mir::log_error("Output %s has no associated CRTC to get ", mgk::connector_name(connector).c_str()); + return true; + } + + return !kms_modes_are_equal(¤t_crtc->mode, &connector->modes[mode_index]); +} + +void mga::AtomicKMSOutput::clear_crtc() +{ + try + { + ensure_crtc(); + } + catch (...) + { + /* + * In order to actually clear the output, we need to have a crtc + * connected to the output/connector so that we can disconnect + * it. However, not being able to get a crtc is OK, since it means + * that the output cannot be displaying anything anyway. + */ + return; + } + + AtomicUpdate update; + update.add_property(*plane_props, "FB_ID", 0); + + auto result = drmModeAtomicCommit(drm_fd_, update, DRM_MODE_ATOMIC_ALLOW_MODESET, nullptr); + if (result) + { + if (result == -EACCES || result == -EPERM) + { + /* We don't have modesetting rights. + * + * This can happen during session switching if (eg) logind has already + * revoked device access before notifying us. + * + * Whatever we're switching to can handle the CRTCs; this should not be fatal. + */ + mir::log_info("Couldn't clear output %s (drmModeSetCrtc: %s (%i))", + mgk::connector_name(connector).c_str(), + strerror(-result), + -result); + } + else + { + fatal_error("Couldn't clear output %s (drmModeSetCrtc = %d)", + mgk::connector_name(connector).c_str(), result); + } + } + + current_crtc = nullptr; +} + +bool mga::AtomicKMSOutput::schedule_page_flip(FBHandle const& fb, void const* flip_completion_userdata) +{ + if (!ensure_crtc()) + { + mir::log_error("Output %s has no associated CRTC to set a framebuffer on", + mgk::connector_name(connector).c_str()); + return false; + } + + auto const width = current_crtc->width; + auto const height = current_crtc->height; + + AtomicUpdate update; + update.add_property(*crtc_props, "MODE_ID", mode->handle()); + update.add_property(*connector_props, "CRTC_ID", current_crtc->crtc_id); + + /* Source viewport. Coordinates are 16.16 fixed point format */ + update.add_property(*plane_props, "SRC_X", fb_offset.dx.as_uint32_t() << 16); + update.add_property(*plane_props, "SRC_Y", fb_offset.dy.as_uint32_t() << 16); + update.add_property(*plane_props, "SRC_W", width << 16); + update.add_property(*plane_props, "SRC_H", height << 16); + + /* Destination viewport. Coordinates are *not* 16.16 */ + update.add_property(*plane_props, "CRTC_X", 0); + update.add_property(*plane_props, "CRTC_Y", 0); + update.add_property(*plane_props, "CRTC_W", width); + update.add_property(*plane_props, "CRTC_H", height); + + /* Set a surface for the plane */ + update.add_property(*plane_props, "CRTC_ID", current_crtc->crtc_id); + update.add_property(*plane_props, "FB_ID", fb); + + auto ret = drmModeAtomicCommit( + drm_fd_, + update, + DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT, + const_cast(flip_completion_userdata)); + if (ret) + { + mir::log_error("Failed to set CRTC: %s (%i)", strerror(-ret), -ret); + current_crtc = nullptr; + return false; + } + + using_saved_crtc = false; + return true; +} + +bool mga::AtomicKMSOutput::set_cursor(gbm_bo*) +{ + return false; +} + +void mga::AtomicKMSOutput::move_cursor(geometry::Point) +{ +} + +bool mga::AtomicKMSOutput::clear_cursor() +{ + return false; +} + +bool mga::AtomicKMSOutput::has_cursor() const +{ + return false; +} + +bool mga::AtomicKMSOutput::ensure_crtc() +{ + /* Nothing to do if we already have a crtc */ + if (current_crtc) + return true; + + /* If the output is not connected there is nothing to do */ + if (connector->connection != DRM_MODE_CONNECTED) + return false; + + // Update the connector as we may unexpectedly fail in find_crtc_and_index_for_connector() + // https://github.com/MirServer/mir/issues/2661 + connector = kms::get_connector(drm_fd_, connector->connector_id); + std::tie(current_crtc, current_plane) = mgk::find_crtc_with_primary_plane(drm_fd_, connector); + crtc_props = std::make_unique(drm_fd_, current_crtc); + plane_props = std::make_unique(drm_fd_, current_plane); + + return (current_crtc != nullptr); +} + +void mga::AtomicKMSOutput::restore_saved_crtc() +{ + if (!using_saved_crtc) + { + drmModeSetCrtc(drm_fd_, saved_crtc.crtc_id, saved_crtc.buffer_id, + saved_crtc.x, saved_crtc.y, + &connector->connector_id, 1, &saved_crtc.mode); + + using_saved_crtc = true; + } +} + +void mga::AtomicKMSOutput::set_power_mode(MirPowerMode mode) +{ + bool should_be_active = mode == mir_power_mode_on; + std::unique_ptr + request{drmModeAtomicAlloc(), &drmModeAtomicFree}; + + drmModeAtomicAddProperty(request.get(), current_crtc->crtc_id, crtc_props->id_for("ACTIVE"), should_be_active); + drmModeAtomicCommit(drm_fd(), request.get(), DRM_MODE_ATOMIC_ALLOW_MODESET, nullptr); +} + + +void mga::AtomicKMSOutput::set_gamma(mg::GammaCurves const& gamma) +{ + if (!gamma.red.size()) + { + mir::log_warning("Ignoring attempt to set zero length gamma"); + return; + } + + if (!ensure_crtc()) + { + mir::log_warning("Output %s has no associated CRTC to set gamma on", + mgk::connector_name(connector).c_str()); + return; + } + + if (gamma.red.size() != gamma.green.size() || + gamma.green.size() != gamma.blue.size()) + { + BOOST_THROW_EXCEPTION( + std::invalid_argument("set_gamma: mismatch gamma LUT sizes")); + } + + auto drm_lut = std::make_unique(gamma.red.size()); + for (size_t i = 0; i < gamma.red.size(); ++i) + { + drm_lut[i].red = gamma.red[i]; + drm_lut[i].green = gamma.green[i]; + drm_lut[i].blue = gamma.blue[i]; + } + PropertyBlob lut{drm_fd_, drm_lut.get(), sizeof(struct drm_color_lut) * gamma.red.size()}; + AtomicUpdate update; + + update.add_property(*crtc_props, "GAMMA_LUT", lut.handle()); + drmModeAtomicCommit(drm_fd(), update, DRM_MODE_ATOMIC_ALLOW_MODESET, nullptr); +} + +void mga::AtomicKMSOutput::refresh_hardware_state() +{ + connector = kms::get_connector(drm_fd_, connector->connector_id); + current_crtc = nullptr; + + if (connector->encoder_id) + { + auto encoder = kms::get_encoder(drm_fd_, connector->encoder_id); + + if (encoder->crtc_id) + { + current_crtc = kms::get_crtc(drm_fd_, encoder->crtc_id); + } + } +} + +namespace +{ +double calculate_vrefresh_hz(drmModeModeInfo const& mode) +{ + if (mode.htotal == 0 || mode.vtotal == 0) + return 0.0; + + /* mode.clock is in KHz */ + double hz = (mode.clock * 100000LL / + ((long)mode.htotal * (long)mode.vtotal) + ) / 100.0; + + // Actually we don't need floating point at all for this... + // TODO: Consider converting our structs to fixed-point ints + return hz; +} + +mg::DisplayConfigurationOutputType +kms_connector_type_to_output_type(uint32_t connector_type) +{ + return static_cast(connector_type); +} + +MirSubpixelArrangement kms_subpixel_to_mir_subpixel(uint32_t subpixel) +{ + switch (subpixel) + { + case DRM_MODE_SUBPIXEL_UNKNOWN: + return mir_subpixel_arrangement_unknown; + case DRM_MODE_SUBPIXEL_HORIZONTAL_RGB: + return mir_subpixel_arrangement_horizontal_rgb; + case DRM_MODE_SUBPIXEL_HORIZONTAL_BGR: + return mir_subpixel_arrangement_horizontal_bgr; + case DRM_MODE_SUBPIXEL_VERTICAL_RGB: + return mir_subpixel_arrangement_vertical_rgb; + case DRM_MODE_SUBPIXEL_VERTICAL_BGR: + return mir_subpixel_arrangement_vertical_bgr; + case DRM_MODE_SUBPIXEL_NONE: + return mir_subpixel_arrangement_none; + default: + return mir_subpixel_arrangement_unknown; + } +} + +std::vector edid_for_connector(mir::Fd const& drm_fd, uint32_t connector_id) +{ + std::vector edid; + + mgk::ObjectProperties connector_props{ + drm_fd, connector_id, DRM_MODE_OBJECT_CONNECTOR}; + + if (connector_props.has_property("EDID")) + { + /* + * We don't technically need the property information here, but query it + * anyway so we can detect if our assumptions about DRM behaviour + * become invalid. + */ + auto property = mgk::DRMModePropertyUPtr{ + drmModeGetProperty(drm_fd, connector_props.id_for("EDID")), + &drmModeFreeProperty}; + + if (!property) + { + mir::log_warning( + "Failed to get EDID property for connector %u: %i (%s)", + connector_id, + errno, + ::strerror(errno)); + return edid; + } + + if (!drm_property_type_is(property.get(), DRM_MODE_PROP_BLOB)) + { + mir::log_warning( + "EDID property on connector %u has unexpected type %u", + connector_id, + property->flags); + return edid; + } + + // A property value of 0 means invalid. + if (connector_props["EDID"] == 0) + { + /* + * Log a debug message only. This will trigger for broken monitors which + * don't provide an EDID, which is not as unusual as you might think... + */ + mir::log_debug("No EDID data available on connector %u", connector_id); + return edid; + } + + try + { + PropertyBlobData edid_blob{drm_fd, static_cast(connector_props["EDID"])}; + edid.reserve(edid_blob.data().size_bytes()); + edid.assign(edid_blob.data().begin(), edid_blob.data().end()); + + edid.shrink_to_fit(); + } + catch (std::system_error const& err) + { + // Failing to read the EDID property is weird, but shouldn't be fatal + mir::log_warning( + "Failed to get EDID property blob for connector %u: %i (%s)", + connector_id, + err.code().value(), + err.what()); + + return edid; + } + } + + return edid; +} +} + +void mga::AtomicKMSOutput::update_from_hardware_state( + DisplayConfigurationOutput& output) const +{ + DisplayConfigurationOutputType const type{ + kms_connector_type_to_output_type(connector->connector_type)}; + geom::Size physical_size{connector->mmWidth, connector->mmHeight}; + bool connected{connector->connection == DRM_MODE_CONNECTED}; + uint32_t const invalid_mode_index = std::numeric_limits::max(); + uint32_t current_mode_index{invalid_mode_index}; + uint32_t preferred_mode_index{invalid_mode_index}; + std::vector modes; + std::vector formats{mir_pixel_format_argb_8888, // PULL THESE OUT OF THE PROPERTIES + mir_pixel_format_xrgb_8888}; + + std::vector edid; + if (connected) + { + /* Only ask for the EDID on connected outputs. There's obviously no monitor EDID + * when there is no monitor connected! + */ + edid = edid_for_connector(drm_fd_, connector->connector_id); + } + + auto const current_mode_info = + [&]() -> drmModeModeInfo const* + { + if (current_crtc) + { + return ¤t_crtc->mode; + } + return nullptr; + }(); + + GammaCurves gamma; + if (current_crtc && crtc_props->has_property("GAMMA_LUT") && crtc_props->has_property("GAMMA_LUT_SIZE")) + { + PropertyBlobData gamma_lut{drm_fd_, static_cast((*crtc_props)["GAMMA_LUT"])}; + auto const gamma_size = gamma_lut.data().size(); + gamma.red.reserve(gamma_size); + gamma.green.reserve(gamma_size); + gamma.blue.reserve(gamma_size); + for (auto const& entry : gamma_lut.data()) + { + gamma.red.push_back(entry.red); + gamma.green.push_back(entry.green); + gamma.blue.push_back(entry.blue); + } + } + + /* Add all the available modes and find the current and preferred one */ + for (int m = 0; m < connector->count_modes; m++) { + drmModeModeInfo const& mode_info = connector->modes[m]; + + geom::Size size{mode_info.hdisplay, mode_info.vdisplay}; + + double vrefresh_hz = calculate_vrefresh_hz(mode_info); + + modes.push_back({size, vrefresh_hz}); + + if (kms_modes_are_equal(&mode_info, current_mode_info)) + current_mode_index = m; + + if ((mode_info.type & DRM_MODE_TYPE_PREFERRED) == DRM_MODE_TYPE_PREFERRED) + preferred_mode_index = m; + } + + /* Fallback for VMWare which fails to specify a matching current mode (bug:1661295) */ + if (current_mode_index == invalid_mode_index) { + for (int m = 0; m != connector->count_modes; ++m) { + drmModeModeInfo &mode_info = connector->modes[m]; + + if (strcmp(mode_info.name, "preferred") == 0) + current_mode_index = m; + } + } + + // There's no need to warn about failing to find a current display mode on a disconnected display. + if (connected && (current_mode_index == invalid_mode_index)) { + mir::log_warning( + "Unable to determine the current display mode."); + } + + output.type = type; + output.modes = modes; + output.preferred_mode_index = preferred_mode_index; + output.physical_size_mm = physical_size; + output.connected = connected; + output.current_format = mir_pixel_format_xrgb_8888; + output.current_mode_index = current_mode_index; + output.subpixel_arrangement = kms_subpixel_to_mir_subpixel(connector->subpixel); + output.gamma = gamma; + output.edid = edid; +} + +int mga::AtomicKMSOutput::drm_fd() const +{ + return drm_fd_; +} diff --git a/src/platforms/atomic-kms/server/kms/atomic_kms_output.h b/src/platforms/atomic-kms/server/kms/atomic_kms_output.h new file mode 100644 index 00000000000..e1ee2b57d48 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/atomic_kms_output.h @@ -0,0 +1,93 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_ATOMIC_KMS_OUTPUT_H_ +#define MIR_GRAPHICS_GBM_ATOMIC_KMS_OUTPUT_H_ + +#include "kms_output.h" +#include "kms-utils/drm_mode_resources.h" +#include "mir/fd.h" + +#include + +namespace mir +{ +namespace graphics +{ +namespace atomic +{ + +class PageFlipper; + +class AtomicKMSOutput : public KMSOutput +{ +public: + AtomicKMSOutput( + mir::Fd drm_master, + kms::DRMModeConnectorUPtr connector); + ~AtomicKMSOutput(); + + uint32_t id() const override; + + void reset() override; + void configure(geometry::Displacement fb_offset, size_t kms_mode_index) override; + geometry::Size size() const override; + int max_refresh_rate() const override; + + bool set_crtc(FBHandle const& fb) override; + bool has_crtc_mismatch() override; + void clear_crtc() override; + bool schedule_page_flip(FBHandle const& fb, void const* flip_completion_userdata) override; + + bool set_cursor(gbm_bo* buffer) override; + void move_cursor(geometry::Point destination) override; + bool clear_cursor() override; + bool has_cursor() const override; + + void set_power_mode(MirPowerMode mode) override; + void set_gamma(GammaCurves const& gamma) override; + + void refresh_hardware_state() override; + void update_from_hardware_state(DisplayConfigurationOutput& output) const override; + + int drm_fd() const override; + +private: + bool ensure_crtc(); + void restore_saved_crtc(); + + mir::Fd const drm_fd_; + + class PropertyBlob; + + kms::DRMModeConnectorUPtr connector; + size_t mode_index; + geometry::Displacement fb_offset; + kms::DRMModeCrtcUPtr current_crtc; + kms::DRMModePlaneUPtr current_plane; + std::unique_ptr mode; + std::unique_ptr crtc_props; + std::unique_ptr plane_props; + std::unique_ptr connector_props; + drmModeCrtc saved_crtc; + bool using_saved_crtc; +}; + +} +} +} + +#endif /* MIR_GRAPHICS_GBM_ATOMIC_KMS_OUTPUT_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/bypass.cpp b/src/platforms/atomic-kms/server/kms/bypass.cpp new file mode 100644 index 00000000000..69bb8745b78 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/bypass.cpp @@ -0,0 +1,46 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "bypass.h" + +#include "mir/graphics/renderable.h" + +using namespace mir; +namespace mga = mir::graphics::atomic; + +mga::BypassMatch::BypassMatch(geometry::Rectangle const& rect) + : view_area(rect), + bypass_is_feasible(true), + identity(1) +{ +} + +bool mga::BypassMatch::operator()(std::shared_ptr const& renderable) +{ + //we've already eliminated bypass as a possibility + if (!bypass_is_feasible) + return false; + + //offscreen surfaces don't affect if bypass is possible + if (!view_area.overlaps(renderable->screen_position())) + return false; + + auto const is_opaque = !((renderable->alpha() != 1.0f) || renderable->shaped()); + auto const fits = (renderable->screen_position() == view_area); + auto const is_orthogonal = (renderable->transformation() == identity); + bypass_is_feasible = (is_opaque && fits && is_orthogonal); + return bypass_is_feasible; +} diff --git a/src/platforms/atomic-kms/server/kms/bypass.h b/src/platforms/atomic-kms/server/kms/bypass.h new file mode 100644 index 00000000000..0637ff0c429 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/bypass.h @@ -0,0 +1,44 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_BYPASS_H_ +#define MIR_GRAPHICS_GBM_BYPASS_H_ + +#include "mir/graphics/renderable.h" + +namespace mir +{ +namespace graphics +{ +namespace atomic +{ + +class BypassMatch +{ +public: + BypassMatch(geometry::Rectangle const& rect); + bool operator()(std::shared_ptr const&); +private: + geometry::Rectangle const view_area; + bool bypass_is_feasible; + glm::mat4 const identity; +}; + +} // namespace atomic-kms +} // namespace graphics +} // namespace mir + +#endif // MIR_GRAPHICS_GBM_BYPASS_H_ diff --git a/src/platforms/atomic-kms/server/kms/display.cpp b/src/platforms/atomic-kms/server/kms/display.cpp new file mode 100644 index 00000000000..f2fa8b09c3b --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/display.cpp @@ -0,0 +1,497 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "display.h" +#include "kms-utils/threaded_drm_event_handler.h" +#include "kms/egl_helper.h" +#include "mir/graphics/platform.h" +#include "platform.h" +#include "display_sink.h" +#include "kms_display_configuration.h" +#include "kms_output.h" +#include "mir/console_services.h" +#include "mir/graphics/overlapping_output_grouping.h" +#include "mir/graphics/event_handler_register.h" + +#include "kms_framebuffer.h" +#include "mir/graphics/display_report.h" +#include "mir/graphics/display_configuration_policy.h" +#include "mir/graphics/transformation.h" +#include "mir/geometry/rectangle.h" +#include "mir/renderer/gl/context.h" +#include "mir/graphics/drm_formats.h" +#include "mir/graphics/egl_error.h" + +#include +#include + +#include +#include +#include +#include +#define MIR_LOG_COMPONENT "atomic-kms" +#include "mir/log.h" +#include "kms-utils/drm_mode_resources.h" +#include "kms-utils/kms_connector.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace mga = mir::graphics::atomic; +namespace mg = mir::graphics; +namespace geom = mir::geometry; + +namespace +{ +double calculate_vrefresh_hz(drmModeModeInfo const& mode) +{ + if (mode.htotal == 0 || mode.vtotal == 0) + return 0.0; + + /* mode.clock is in KHz */ + double hz = (mode.clock * 100000LL / + ((long)mode.htotal * (long)mode.vtotal) + ) / 100.0; + + return hz; +} + +char const* describe_connection_status(drmModeConnector const& connection) +{ + switch (connection.connection) + { + case DRM_MODE_CONNECTED: + return "connected"; + case DRM_MODE_DISCONNECTED: + return "disconnected"; + case DRM_MODE_UNKNOWNCONNECTION: + return "UNKNOWN"; + default: + return ""; + } +} + +void log_drm_details(mir::Fd const& drm_fd) +{ + mir::log_info("DRM device details:"); + auto version = std::unique_ptr{ + drmGetVersion(drm_fd), + &drmFreeVersion}; + + auto device_name = std::unique_ptr{ + drmGetDeviceNameFromFd(drm_fd), + &free + }; + + mir::log_info( + "%s: using driver %s [%s] (version: %i.%i.%i driver date: %s)", + device_name.get(), + version->name, + version->desc, + version->version_major, + version->version_minor, + version->version_patchlevel, + version->date); + + try + { + mg::kms::DRMModeResources resources{drm_fd}; + for (auto const& connector : resources.connectors()) + { + mir::log_info( + "\tOutput: %s (%s)", + mg::kms::connector_name(connector).c_str(), + describe_connection_status(*connector)); + for (auto i = 0; i < connector->count_modes; ++i) + { + mir::log_info( + "\t\tMode: %i×%i@%.2f", + connector->modes[i].hdisplay, + connector->modes[i].vdisplay, + calculate_vrefresh_hz(connector->modes[i])); + } + } + } + catch (std::exception const& error) + { + mir::log_info( + "\tKMS not supported (%s)", + error.what()); + } +} + +} + +mga::Display::Display( + mir::Fd drm_fd, + std::shared_ptr gbm, + mga::BypassOption bypass_option, + std::shared_ptr const& initial_conf_policy, + std::shared_ptr const& listener) + : drm_fd{std::move(drm_fd)}, + event_handler{std::make_shared(drm_fd)}, + gbm{std::move(gbm)}, + listener(listener), + monitor(mir::udev::Context()), + output_container{ + std::make_shared( + this->drm_fd)}, + current_display_configuration{output_container}, + dirty_configuration{false}, + bypass_option(bypass_option) +{ + monitor.filter_by_subsystem_and_type("drm", "drm_minor"); + monitor.enable(); + + log_drm_details(this->drm_fd); + + initial_conf_policy->apply_to(current_display_configuration); + + configure(current_display_configuration); +} + +// please don't remove this empty destructor, it's here for the +// unique ptr!! if you accidentally remove it you will get a not +// so relevant linker error about some missing headers +mga::Display::~Display() = default; + +void mga::Display::for_each_display_sync_group( + std::function const& f) +{ + std::lock_guard lg{configuration_mutex}; + + for (auto& db_ptr : display_sinks) + f(*db_ptr); +} + +std::unique_ptr mga::Display::configuration() const +{ + std::lock_guard lg{configuration_mutex}; + + if (dirty_configuration) + { + /* Give back a copy of the latest configuration information */ + current_display_configuration.update(); + dirty_configuration = false; + } + + return std::make_unique(current_display_configuration); +} + +void mga::Display::configure(mg::DisplayConfiguration const& conf) +{ + if (!conf.valid()) + { + BOOST_THROW_EXCEPTION( + std::logic_error("Invalid or inconsistent display configuration")); + } + + { + std::lock_guard lock{configuration_mutex}; + configure_locked(dynamic_cast(conf), lock); + } +} + +void mga::Display::register_configuration_change_handler( + EventHandlerRegister& handlers, + DisplayConfigurationChangeHandler const& conf_change_handler) +{ + handlers.register_fd_handler( + {monitor.fd()}, + this, + make_module_ptr>( + [conf_change_handler, this](int) + { + mir::log_debug("Handling UDEV events"); + monitor.process_events([conf_change_handler, this] + (mir::udev::Monitor::EventType type, mir::udev::Device const& device) + { + mir::log_debug("Processing UDEV event for %s: %i", device.syspath(), type); + dirty_configuration = true; + conf_change_handler(); + }); + })); +} + +void mga::Display::pause() +{ +} + +void mga::Display::resume() +{ + { + std::lock_guard lg{configuration_mutex}; + + /* + * After resuming (e.g. because we switched back to the display server VT) + * we need to reset the CRTCs. For active displays we schedule a CRTC reset + * on the next swap. For connected but unused outputs we clear the CRTC. + */ + for (auto& db_ptr : display_sinks) + db_ptr->schedule_set_crtc(); + + clear_connected_unused_outputs(); + } +} + +auto mga::Display::create_hardware_cursor() -> std::shared_ptr +{ + return {}; +} + +void mga::Display::clear_connected_unused_outputs() +{ + current_display_configuration.for_each_output([&](DisplayConfigurationOutput const& conf_output) + { + /* + * An output may be unused either because it's explicitly not used + * (DisplayConfigurationOutput::used) or because its power mode is + * not mir_power_mode_on. + */ + if (conf_output.connected && + (!conf_output.used || (conf_output.power_mode != mir_power_mode_on))) + { + auto kms_output = current_display_configuration.get_output_for(conf_output.id); + + kms_output->clear_crtc(); + kms_output->set_power_mode(conf_output.power_mode); + kms_output->set_gamma(conf_output.gamma); + } + }); +} + +bool mga::Display::apply_if_configuration_preserves_display_buffers( + mg::DisplayConfiguration const& conf) +{ + bool result = false; + auto const& new_kms_conf = dynamic_cast(conf); + + { + std::lock_guard lock{configuration_mutex}; + if (compatible(current_display_configuration, new_kms_conf)) + { + configure_locked(new_kms_conf, lock); + result = true; + } + } + + return result; +} + +void mga::Display::configure_locked( + mga::RealKMSDisplayConfiguration const& kms_conf, + std::lock_guard const&) +{ + // Treat the current_display_configuration as incompatible with itself, + // before it's fully constructed, to force proper initialization. + bool const comp{ + (&kms_conf != ¤t_display_configuration) && + compatible(kms_conf, current_display_configuration)}; + std::vector> display_buffers_new; + + if (!comp) + { + /* + * Notice for a little while here we will have duplicate + * DisplayBuffers attached to each output, and the display_buffers_new + * will take over the outputs before the old display_sinks are + * destroyed. So to avoid page flipping confusion in-between, make + * sure we wait for all pending page flips to finish before the + * display_buffers_new are created and take control of the outputs. + */ + for (auto& db : display_sinks) + db->wait_for_page_flip(); + + /* Reset the state of all outputs */ + kms_conf.for_each_output( + [&](DisplayConfigurationOutput const& conf_output) + { + auto kms_output = current_display_configuration.get_output_for(conf_output.id); + kms_output->clear_cursor(); + kms_output->reset(); + }); + } + + /* Set up used outputs */ + OverlappingOutputGrouping grouping{kms_conf}; + auto group_idx = 0; + + grouping.for_each_group( + [&](OverlappingOutputGroup const& group) + { + auto bounding_rect = group.bounding_rectangle(); + std::vector> kms_outputs; + glm::mat2 transformation; + geom::Size current_mode_resolution; + + group.for_each_output( + [&](DisplayConfigurationOutput const& conf_output) + { + auto kms_output = current_display_configuration.get_output_for(conf_output.id); + + auto const mode_index = kms_conf.get_kms_mode_index(conf_output.id, + conf_output.current_mode_index); + kms_output->configure(conf_output.top_left - bounding_rect.top_left, mode_index); + if (!comp) + { + kms_output->set_power_mode(conf_output.power_mode); + kms_output->set_gamma(conf_output.gamma); + kms_outputs.push_back(std::move(kms_output)); + } + + /* + * Presently OverlappingOutputGroup guarantees all grouped + * outputs have the same transformation. + */ + transformation = conf_output.transformation(); + if (conf_output.current_mode_index < conf_output.modes.size()) + current_mode_resolution = conf_output.modes[conf_output.current_mode_index].size; + }); + + if (comp) + { + display_sinks[group_idx++]->set_transformation(transformation, + bounding_rect); + } + else + { + auto db = std::make_unique( + drm_fd, + gbm, + event_handler, + bypass_option, + listener, + kms_outputs, + bounding_rect, + transformation); + + display_buffers_new.push_back(std::move(db)); + } + }); + + if (!comp) + display_sinks = std::move(display_buffers_new); + + /* Store applied configuration */ + current_display_configuration = kms_conf; + + if (!comp) + /* Clear connected but unused outputs */ + clear_connected_unused_outputs(); +} + +namespace +{ +auto gbm_create_device_checked(mir::Fd fd) -> std::shared_ptr +{ + errno = 0; + auto device = gbm_create_device(fd); + if (!device) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + errno, + std::system_category(), + "Failed to create GBM device"})); + } + return { + device, + [fd](struct gbm_device* device) // Capture shared ownership of fd to keep gdm_device functional + { + if (device) + { + gbm_device_destroy(device); + } + } + }; +} +} + +mga::GBMDisplayProvider::GBMDisplayProvider( + mir::Fd drm_fd) + : fd{std::move(drm_fd)}, + gbm{gbm_create_device_checked(fd)} +{ +} + +auto mga::GBMDisplayProvider::on_this_sink(mg::DisplaySink& sink) const -> bool +{ + if (auto gbm_display_sink = dynamic_cast(&sink)) + { + return gbm_display_sink->gbm_device() == gbm; + } + return false; +} + +auto mga::GBMDisplayProvider::gbm_device() const -> std::shared_ptr +{ + return gbm; +} + +auto mga::GBMDisplayProvider::is_same_device(mir::udev::Device const& render_device) const -> bool +{ +#ifndef MIR_DRM_HAS_GET_DEVICE_FROM_DEVID + class CStrFree + { + public: + void operator()(char* str) + { + if (str) + { + free(str); + } + } + }; + + std::unique_ptr primary_node{drmGetPrimaryDeviceNameFromFd(fd)}; + std::unique_ptr render_node{drmGetRenderDeviceNameFromFd(fd)}; + + if (primary_node) + { + if (strcmp(primary_node.get(), render_device.devnode()) == 0) + { + return true; + } + } + if (render_node) + { + if (strcmp(render_node.get(), render_device.devnode()) == 0) + { + return true; + } + } + + return false; +#else + drmDevicePtr us{nullptr}, them{nullptr}; + + drmGetDeviceFromDevId(render_device.devno(), 0, &them); + drmGetDevice2(fd, 0, &us); + + bool result = drmDevicesEqual(us, them); + + drmDeviceFree(us); + drmDeviceFree(them); + + return result; +#endif +} diff --git a/src/platforms/atomic-kms/server/kms/display.h b/src/platforms/atomic-kms/server/kms/display.h new file mode 100644 index 00000000000..ba0961a0606 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/display.h @@ -0,0 +1,131 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_DISPLAY_H_ +#define MIR_GRAPHICS_GBM_DISPLAY_H_ + +#include "kms-utils/drm_event_handler.h" +#include "mir/graphics/display.h" +#include "mir/udev/wrapper.h" +#include "real_kms_output_container.h" +#include "real_kms_display_configuration.h" +#include "platform_common.h" +#include "mir/graphics/platform.h" + +#include +#include +#include + +namespace mir +{ +namespace graphics +{ + +class DisplayInterfaceProvider; +class DisplayReport; +class DisplayConfigurationPolicy; +class EventHandlerRegister; +class GLConfig; + +namespace kms +{ +class DRMEventHandler; +} + +namespace atomic +{ + +namespace helpers +{ +class DRMHelper; +class GBMHelper; +} + +class DisplaySink; +class KMSOutput; +class Cursor; + +class Display : public graphics::Display +{ +public: + Display( + mir::Fd drm_fd, + std::shared_ptr gbm, + BypassOption bypass_option, + std::shared_ptr const& initial_conf_policy, + std::shared_ptr const& listener); + ~Display(); + + geometry::Rectangle view_area() const; + void for_each_display_sync_group( + std::function const& f) override; + + std::unique_ptr configuration() const override; + bool apply_if_configuration_preserves_display_buffers(DisplayConfiguration const& conf) override; + void configure(DisplayConfiguration const& conf) override; + + void register_configuration_change_handler( + EventHandlerRegister& handlers, + DisplayConfigurationChangeHandler const& conf_change_handler) override; + + void pause() override; + void resume() override; + + std::shared_ptr create_hardware_cursor() override; + +private: + void clear_connected_unused_outputs(); + + mutable std::mutex configuration_mutex; + mir::Fd const drm_fd; + std::shared_ptr const event_handler; + std::shared_ptr const gbm; + std::shared_ptr const listener; + mir::udev::Monitor monitor; + std::shared_ptr const output_container; + std::vector> display_sinks; + mutable RealKMSDisplayConfiguration current_display_configuration; + mutable std::atomic dirty_configuration; + + void configure_locked( + RealKMSDisplayConfiguration const& conf, + std::lock_guard const&); + + BypassOption bypass_option; + std::weak_ptr cursor; +}; + +class GBMDisplayProvider : public graphics::GBMDisplayProvider +{ +public: + explicit GBMDisplayProvider(mir::Fd drm_fd); + + auto is_same_device(mir::udev::Device const& render_device) const -> bool override; + + auto on_this_sink(graphics::DisplaySink& sink) const -> bool override; + + auto gbm_device() const -> std::shared_ptr override; + +private: + mir::Fd const fd; + std::shared_ptr const gbm; +}; + +} +} +} + +#endif /* MIR_GRAPHICS_GBM_DISPLAY_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/display_buffer.cpp b/src/platforms/atomic-kms/server/kms/display_buffer.cpp new file mode 100644 index 00000000000..f8bb2be216e --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/display_buffer.cpp @@ -0,0 +1,357 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "display_sink.h" +#include "kms_cpu_addressable_display_provider.h" +#include "kms_output.h" +#include "cpu_addressable_fb.h" +#include "gbm_display_allocator.h" +#include "mir/fd.h" +#include "mir/graphics/display_report.h" +#include "mir/graphics/platform.h" +#include "mir/graphics/transformation.h" +#include "bypass.h" +#include "mir/fatal.h" +#include "mir/log.h" +#include "display_helpers.h" +#include "egl_helper.h" +#include "mir/graphics/egl_error.h" +#include "mir/graphics/gl_config.h" +#include "mir/graphics/dmabuf_buffer.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mg = mir::graphics; +namespace mga = mir::graphics::atomic; +namespace geom = mir::geometry; +namespace mgk = mir::graphics::kms; + +mga::DisplaySink::DisplaySink( + mir::Fd drm_fd, + std::shared_ptr gbm, + std::shared_ptr event_handler, + mga::BypassOption, + std::shared_ptr const& listener, + std::vector> const& outputs, + geom::Rectangle const& area, + glm::mat2 const& transformation) + : gbm{std::move(gbm)}, + listener(listener), + outputs(outputs), + event_handler{std::move(event_handler)}, + area(area), + transform{transformation}, + needs_set_crtc{false}, + page_flips_pending{false} +{ + listener->report_successful_setup_of_native_resources(); + + // If any of the outputs have a CRTC mismatch, we will want to set all of them + // so that they're all showing the same buffer. + bool has_crtc_mismatch = false; + for (auto& output : outputs) + { + has_crtc_mismatch = output->has_crtc_mismatch(); + if (has_crtc_mismatch) + break; + } + + if (has_crtc_mismatch) + { + mir::log_info("Clearing screen due to differing encountered and target modes"); + // TODO: Pull a supported format out of KMS rather than assuming XRGB8888 + auto initial_fb = std::make_shared( + std::move(drm_fd), + false, + DRMFormat{DRM_FORMAT_XRGB8888}, + area.size); + + auto mapping = initial_fb->map_writeable(); + ::memset(mapping->data(), 24, mapping->len()); + + visible_fb = std::move(initial_fb); + for (auto &output: outputs) { + output->set_crtc(*visible_fb); + } + listener->report_successful_drm_mode_set_crtc_on_construction(); + } + listener->report_successful_display_construction(); +} + +mga::DisplaySink::~DisplaySink() = default; + +geom::Rectangle mga::DisplaySink::view_area() const +{ + return area; +} + +glm::mat2 mga::DisplaySink::transformation() const +{ + return transform; +} + +void mga::DisplaySink::set_transformation(glm::mat2 const& t, geometry::Rectangle const& a) +{ + transform = t; + area = a; +} + +bool mga::DisplaySink::overlay(std::vector const& renderable_list) +{ + // TODO: implement more than the most basic case. + if (renderable_list.size() != 1) + { + return false; + } + + if (renderable_list[0].screen_positon != view_area()) + { + return false; + } + + if (renderable_list[0].source_position.top_left != geom::PointF {0,0} || + renderable_list[0].source_position.size.width.as_value() != view_area().size.width.as_int() || + renderable_list[0].source_position.size.height.as_value() != view_area().size.height.as_int()) + { + return false; + } + + if (auto fb = std::dynamic_pointer_cast(renderable_list[0].buffer)) + { + next_swap = std::move(fb); + return true; + } + return false; +} + +void mga::DisplaySink::for_each_display_sink(std::function const& f) +{ + f(*this); +} + +void mga::DisplaySink::set_crtc(FBHandle const& forced_frame) +{ + for (auto& output : outputs) + { + /* + * Note that failure to set the CRTC is not a fatal error. This can + * happen under normal conditions when resizing VirtualBox (which + * actually removes and replaces the virtual output each time so + * sometimes it's really not there). Xorg often reports similar + * errors, and it's not fatal. + */ + if (!output->set_crtc(forced_frame)) + mir::log_error("Failed to set DRM CRTC. " + "Screen contents may be incomplete. " + "Try plugging the monitor in again."); + } +} + +void mga::DisplaySink::post() +{ + /* + * We might not have waited for the previous frame to page flip yet. + * This is good because it maximizes the time available to spend rendering + * each frame. Just remember wait_for_page_flip() must be called at some + * point before the next schedule_page_flip(). + */ + wait_for_page_flip(); + + if (!next_swap) + { + // Hey! No one has given us a next frame yet, so we don't have to change what's onscreen. + // Sweet! We can just bail. + return; + } + /* + * Otherwise, pull the next frame into the pending slot + */ + scheduled_fb = std::move(next_swap); + next_swap = nullptr; + + /* + * Try to schedule a page flip as first preference to avoid tearing. + * [will complete in a background thread] + */ + if (!needs_set_crtc && !schedule_page_flip(*scheduled_fb)) + needs_set_crtc = true; + + /* + * Fallback blitting: Not pretty, since it may tear. VirtualBox seems + * to need to do this on every frame. [will complete in this thread] + */ + if (needs_set_crtc) + { + set_crtc(*scheduled_fb); + // SetCrtc is immediate, so the FB is now visible and we have nothing pending + visible_fb = std::move(scheduled_fb); + scheduled_fb = nullptr; + + needs_set_crtc = false; + } + + using namespace std::chrono_literals; // For operator""ms() + + // Predicted worst case render time for the next frame... + auto predicted_render_time = 50ms; + + if (holding_client_buffers) + { + /* + * For composited frames we defer wait_for_page_flip till just before + * the next frame, but not for bypass frames. Deferring the flip of + * bypass frames would increase the time we held + * visible_bypass_frame unacceptably, resulting in client stuttering + * unless we allocate more buffers (which I'm trying to avoid). + * Also, bypass does not need the deferred page flip because it has + * no compositing/rendering step for which to save time for. + */ + wait_for_page_flip(); + + // It's very likely the next frame will be bypassed like this one so + // we only need time for kernel page flip scheduling... + predicted_render_time = 5ms; + } + else + { + /* + * Not in clone mode? We can afford to wait for the page flip then, + * making us double-buffered (noticeably less laggy than the triple + * buffering that clone mode requires). + */ + if (outputs.size() == 1) + wait_for_page_flip(); + + /* + * TODO: If you're optimistic about your GPU performance and/or + * measure it carefully you may wish to set predicted_render_time + * to a lower value here for lower latency. + * + *predicted_render_time = 9ms; // e.g. about the same as Weston + */ + } + + recommend_sleep = 0ms; + if (outputs.size() == 1) + { + auto const& output = outputs.front(); + auto const min_frame_interval = 1000ms / output->max_refresh_rate(); + if (predicted_render_time < min_frame_interval) + recommend_sleep = min_frame_interval - predicted_render_time; + } +} + +std::chrono::milliseconds mga::DisplaySink::recommended_sleep() const +{ + return recommend_sleep; +} + +bool mga::DisplaySink::schedule_page_flip(FBHandle const& bufobj) +{ + /* + * Schedule the current front buffer object for display. Note that + * the page flip is asynchronous and synchronized with vertical refresh. + */ + for (auto& output : outputs) + { + output->schedule_page_flip(bufobj, event_handler->drm_event_data()); + } + + return page_flips_pending; +} + +void mga::DisplaySink::wait_for_page_flip() +{ + if (page_flips_pending) + { + for (auto& pending_flip : pending_flips) + { + pending_flip.get(); + } + pending_flips.clear(); + + // The previously-scheduled FB has been page-flipped, and is now visible + visible_fb = std::move(scheduled_fb); + scheduled_fb = nullptr; + + page_flips_pending = false; + } +} + +void mga::DisplaySink::schedule_set_crtc() +{ + needs_set_crtc = true; +} + +auto mga::DisplaySink::drm_fd() const -> mir::Fd +{ + return mir::Fd{mir::IntOwnedFd{outputs.front()->drm_fd()}}; +} + +auto mga::DisplaySink::gbm_device() const -> std::shared_ptr +{ + return gbm; +} + +void mga::DisplaySink::set_next_image(std::unique_ptr content) +{ + std::vector const single_buffer = { + DisplayElement { + view_area(), + geom::RectangleF{ + {0, 0}, + {view_area().size.width.as_value(), view_area().size.height.as_value()}}, + std::move(content) + } + }; + if (!overlay(single_buffer)) + { + // Oh, oh! We should be *guaranteed* to “overlay” a single Framebuffer; this is likely a programming error + BOOST_THROW_EXCEPTION((std::runtime_error{"Failed to post buffer to display"})); + } +} + +auto mga::DisplaySink::maybe_create_allocator(DisplayAllocator::Tag const& type_tag) + -> DisplayAllocator* +{ + if (dynamic_cast(&type_tag)) + { + if (!kms_allocator) + { + kms_allocator = kms::CPUAddressableDisplayAllocator::create_if_supported(drm_fd(), outputs.front()->size()); + } + return kms_allocator.get(); + } + if (dynamic_cast(&type_tag)) + { + if (!gbm_allocator) + { + gbm_allocator = std::make_unique(drm_fd(), gbm, outputs.front()->size()); + } + return gbm_allocator.get(); + } + return nullptr; +} diff --git a/src/platforms/atomic-kms/server/kms/display_sink.h b/src/platforms/atomic-kms/server/kms/display_sink.h new file mode 100644 index 00000000000..140b58ca1af --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/display_sink.h @@ -0,0 +1,127 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_ATOMIC_DISPLAY_SINK_H_ +#define MIR_GRAPHICS_ATOMIC_DISPLAY_SINK_H_ + +#include "kms-utils/drm_event_handler.h" +#include "mir/graphics/display_sink.h" +#include "mir/graphics/display.h" +#include "display_helpers.h" +#include "egl_helper.h" +#include "mir/graphics/platform.h" +#include "platform_common.h" +#include "kms_framebuffer.h" + +#include +#include +#include +#include + +namespace mir +{ +namespace graphics +{ + +class DisplayReport; +class GLConfig; + +namespace kms +{ +class DRMEventHandler; +} + +namespace atomic +{ + +class Platform; +class KMSOutput; + +class DisplaySink : public graphics::DisplaySink, + public graphics::DisplaySyncGroup +{ +public: + DisplaySink( + mir::Fd drm_fd, + std::shared_ptr gbm, + std::shared_ptr event_handler, + BypassOption bypass_options, + std::shared_ptr const& listener, + std::vector> const& outputs, + geometry::Rectangle const& area, + glm::mat2 const& transformation); + ~DisplaySink(); + + geometry::Rectangle view_area() const override; + + void set_next_image(std::unique_ptr content) override; + + bool overlay(std::vector const& renderlist) override; + + void for_each_display_sink( + std::function const& f) override; + void post() override; + std::chrono::milliseconds recommended_sleep() const override; + + glm::mat2 transformation() const override; + + void set_transformation(glm::mat2 const& t, geometry::Rectangle const& a); + void schedule_set_crtc(); + void wait_for_page_flip(); + + auto drm_fd() const -> mir::Fd; + + auto gbm_device() const -> std::shared_ptr; + +protected: + auto maybe_create_allocator(DisplayAllocator::Tag const& type_tag) -> DisplayAllocator* override; + +private: + bool schedule_page_flip(FBHandle const& bufobj); + void set_crtc(FBHandle const&); + + std::shared_ptr const gbm; + bool holding_client_buffers{false}; + std::shared_ptr bypass_bufobj{nullptr}; + std::shared_ptr const listener; + + std::vector> outputs; + std::vector> pending_flips; + + std::shared_ptr const event_handler; + + std::shared_ptr kms_allocator; + std::unique_ptr gbm_allocator; + + // Framebuffer handling + // KMS does not take a reference to submitted framebuffers; if you destroy a framebuffer while + // it's in use, KMS treat that as submitting a null framebuffer and turn off the display. + std::shared_ptr next_swap{nullptr}; //< Next frame to submit to the hardware + std::shared_ptr scheduled_fb{nullptr}; //< Frame currently submitted to the hardware, not yet on-screen + std::shared_ptr visible_fb{nullptr}; //< Frame currently onscreen + + geometry::Rectangle area; + glm::mat2 transform; + std::atomic needs_set_crtc; + std::chrono::milliseconds recommend_sleep{0}; + bool page_flips_pending; +}; + +} +} +} + +#endif /* MIR_GRAPHICS_ATOMIC_DISPLAY_SINK_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/egl_helper.cpp b/src/platforms/atomic-kms/server/kms/egl_helper.cpp new file mode 100644 index 00000000000..b8442eae022 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/egl_helper.cpp @@ -0,0 +1,286 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "egl_helper.h" +#include "mir/graphics/gl_config.h" +#include "mir/graphics/egl_error.h" +#include +#include +#include + +#define MIR_LOG_COMPONENT "EGL" +#include "mir/log.h" + +namespace mg = mir::graphics; +namespace mgmh = mir::graphics::atomic::helpers; + +mgmh::EGLHelper::EGLHelper(GLConfig const& gl_config) + : depth_buffer_bits{gl_config.depth_buffer_bits()}, + stencil_buffer_bits{gl_config.stencil_buffer_bits()}, + egl_display{EGL_NO_DISPLAY}, egl_config{0}, + egl_context{EGL_NO_CONTEXT}, egl_surface{EGL_NO_SURFACE}, + should_terminate_egl{false} +{ +} + +mgmh::EGLHelper::EGLHelper( + GLConfig const& gl_config, + GBMHelper const& gbm, + gbm_surface* surface, + uint32_t gbm_format, + EGLContext shared_context) + : EGLHelper(gl_config) +{ + setup(gbm.device, surface, gbm_format, shared_context, false); +} + +mgmh::EGLHelper::EGLHelper(EGLHelper&& from) + : depth_buffer_bits{from.depth_buffer_bits}, + stencil_buffer_bits{from.stencil_buffer_bits}, + egl_display{from.egl_display}, + egl_config{from.egl_config}, + egl_context{from.egl_context}, + egl_surface{from.egl_surface}, + should_terminate_egl{from.should_terminate_egl} +{ + from.should_terminate_egl = false; + from.egl_display = EGL_NO_DISPLAY; + from.egl_context = EGL_NO_CONTEXT; + from.egl_surface = EGL_NO_SURFACE; +} + +namespace +{ +void initialise_egl(EGLDisplay dpy, int minimum_major_version, int minimum_minor_version) +{ + EGLint major, minor; + + if (eglInitialize(dpy, &major, &minor) == EGL_FALSE) + BOOST_THROW_EXCEPTION(mg::egl_error("Failed to initialize EGL display")); + + if ((major < minimum_major_version) || + (major == minimum_major_version && minor < minimum_minor_version)) + { + BOOST_THROW_EXCEPTION( + boost::enable_error_info(std::runtime_error("Incompatible EGL version"))); + // TODO: Insert egl version major and minor into exception + } +} + +std::vector get_matching_configs(EGLDisplay dpy, EGLint const attr[]) +{ + EGLint num_egl_configs; + + // First query the number of matching configs… + if ((eglChooseConfig(dpy, attr, nullptr, 0, &num_egl_configs) == EGL_FALSE) || + (num_egl_configs == 0)) + { + BOOST_THROW_EXCEPTION(mg::egl_error("Failed to enumerate any matching EGL configs")); + } + + std::vector matching_configs(static_cast(num_egl_configs)); + if ((eglChooseConfig(dpy, attr, matching_configs.data(), static_cast(matching_configs.size()), &num_egl_configs) == EGL_FALSE) || + (num_egl_configs == 0)) + { + BOOST_THROW_EXCEPTION(mg::egl_error("Failed to acquire matching EGL configs")); + } + + matching_configs.resize(static_cast(num_egl_configs)); + return matching_configs; +} + +} + +void mgmh::EGLHelper::setup(GBMHelper const& gbm) +{ + eglBindAPI(EGL_OPENGL_ES_API); + + static const EGLint context_attr[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + + egl_display = egl_display_for_gbm_device(gbm.device); + initialise_egl(egl_display, 1, 4); + should_terminate_egl = true; + + // This is a context solely used for sharing GL object IDs; we will not do any rendering + // with it, so we do not care what EGLconfig is used *at all*. + EGLint const no_attribs[] = {EGL_NONE}; + egl_config = get_matching_configs(egl_display, no_attribs)[0]; + + egl_context = eglCreateContext(egl_display, egl_config, EGL_NO_CONTEXT, context_attr); + if (egl_context == EGL_NO_CONTEXT) + BOOST_THROW_EXCEPTION(mg::egl_error("Failed to create EGL context")); +} + +void mgmh::EGLHelper::setup(GBMHelper const& gbm, EGLContext shared_context) +{ + eglBindAPI(EGL_OPENGL_ES_API); + + static const EGLint context_attr[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + + egl_display = egl_display_for_gbm_device(gbm.device); + + // Might as well copy the EGLConfig from shared_context + EGLint config_id; + if (eglQueryContext(egl_display, shared_context, EGL_CONFIG_ID, &config_id) != EGL_TRUE) + { + BOOST_THROW_EXCEPTION(mg::egl_error("Failed to query EGLConfig of shared EGLContext")); + } + EGLint const context_attribs[] = { + EGL_CONFIG_ID, config_id, + EGL_NONE + }; + egl_config = get_matching_configs(egl_display, context_attribs)[0]; + + egl_context = eglCreateContext(egl_display, egl_config, shared_context, context_attr); + if (egl_context == EGL_NO_CONTEXT) + BOOST_THROW_EXCEPTION(mg::egl_error("Failed to create EGL context")); +} + +void mgmh::EGLHelper::setup( + gbm_device* const device, + gbm_surface* surface_gbm, + uint32_t gbm_format, + EGLContext shared_context, + bool owns_egl) +{ + eglBindAPI(EGL_OPENGL_ES_API); + + static const EGLint context_attr[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + + egl_display = egl_display_for_gbm_device(device); + if (owns_egl) + { + initialise_egl(egl_display, 1, 4); + should_terminate_egl = owns_egl; + } + + egl_config = egl_config_for_format(static_cast(gbm_format)); + + egl_surface = platform_base.eglCreatePlatformWindowSurface( + egl_display, + egl_config, + surface_gbm, + nullptr); + if(egl_surface == EGL_NO_SURFACE) + BOOST_THROW_EXCEPTION(mg::egl_error("Failed to create EGL window surface")); + + egl_context = eglCreateContext(egl_display, egl_config, shared_context, context_attr); + if (egl_context == EGL_NO_CONTEXT) + BOOST_THROW_EXCEPTION(mg::egl_error("Failed to create EGL context")); +} + +mgmh::EGLHelper::~EGLHelper() noexcept +{ + if (egl_display != EGL_NO_DISPLAY) { + if (egl_context != EGL_NO_CONTEXT) + { + eglBindAPI(EGL_OPENGL_ES_API); + if (eglGetCurrentContext() == egl_context) + eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroyContext(egl_display, egl_context); + } + if (egl_surface != EGL_NO_SURFACE) + eglDestroySurface(egl_display, egl_surface); + if (should_terminate_egl) + eglTerminate(egl_display); + } +} + +bool mgmh::EGLHelper::swap_buffers() +{ + auto ret = eglSwapBuffers(egl_display, egl_surface); + return (ret == EGL_TRUE); +} + +bool mgmh::EGLHelper::make_current() const +{ + auto ret = eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context); + eglBindAPI(EGL_OPENGL_ES_API); + return (ret == EGL_TRUE); +} + +bool mgmh::EGLHelper::release_current() const +{ + auto ret = eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + return (ret == EGL_TRUE); +} + +auto mgmh::EGLHelper::egl_display_for_gbm_device(struct gbm_device* const device) -> EGLDisplay +{ + auto const egl_display = platform_base.eglGetPlatformDisplay( + EGL_PLATFORM_GBM_KHR, // EGL_PLATFORM_GBM_MESA has the same value. + static_cast(device), + nullptr); + if (egl_display == EGL_NO_DISPLAY) + BOOST_THROW_EXCEPTION(mg::egl_error("Failed to get EGL display")); + + return egl_display; +} + +auto mgmh::EGLHelper::egl_config_for_format(EGLint gbm_format) -> EGLConfig +{ + // TODO: Get the required EGL_{RED,GREEN,BLUE}_SIZE values out of gbm_format + EGLint const config_attr[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 0, + EGL_DEPTH_SIZE, depth_buffer_bits, + EGL_STENCIL_SIZE, stencil_buffer_bits, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_NONE + }; + + for (auto const& config : get_matching_configs(egl_display, config_attr)) + { + EGLint id; + if (eglGetConfigAttrib(egl_display, config, EGL_NATIVE_VISUAL_ID, &id) == EGL_FALSE) + { + mir::log_warning( + "Failed to query GBM format of EGLConfig: %s", + mg::egl_category().message(eglGetError()).c_str()); + continue; + } + + if (id == gbm_format) + { + // We've found our matching format, so we're done here. + return config; + } + } + BOOST_THROW_EXCEPTION((NoMatchingEGLConfig{static_cast(gbm_format)})); +} + +void mgmh::EGLHelper::report_egl_configuration(std::function f) +{ + f(egl_display, egl_config); +} + +mgmh::EGLHelper::NoMatchingEGLConfig::NoMatchingEGLConfig(uint32_t /*format*/) + : std::runtime_error("Failed to find matching EGL config") +{ + // TODO: Include the format string; need to extract from linux_dmabuf.cpp +} diff --git a/src/platforms/atomic-kms/server/kms/egl_helper.h b/src/platforms/atomic-kms/server/kms/egl_helper.h new file mode 100644 index 00000000000..ea4468fbd80 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/egl_helper.h @@ -0,0 +1,87 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_EGL_HELPER_H_ +#define MIR_GRAPHICS_GBM_EGL_HELPER_H_ + +#include "../display_helpers.h" +#include "mir/graphics/egl_extensions.h" +#include +#include + +namespace mir +{ +namespace graphics +{ +class GLConfig; +namespace atomic +{ +namespace helpers +{ +class EGLHelper +{ +public: + EGLHelper(GLConfig const& gl_config); + EGLHelper( + GLConfig const& gl_config, + GBMHelper const& gbm, + gbm_surface* surface, + uint32_t gbm_format, + EGLContext shared_context); + ~EGLHelper() noexcept; + EGLHelper(EGLHelper&& from); + + EGLHelper(const EGLHelper&) = delete; + EGLHelper& operator=(const EGLHelper&) = delete; + + void setup(GBMHelper const& gbm); + void setup(GBMHelper const& gbm, EGLContext shared_context); + void setup(gbm_device* const device, gbm_surface* surface_gbm, uint32_t gbm_format, EGLContext shared_context, bool owns_egl); + + bool swap_buffers(); + bool make_current() const; + bool release_current() const; + + EGLContext context() const { return egl_context; } + auto display() const -> EGLDisplay { return egl_display; } + + void report_egl_configuration(std::function); + + class NoMatchingEGLConfig : public std::runtime_error + { + public: + NoMatchingEGLConfig(uint32_t format); + }; +private: + auto egl_config_for_format(EGLint gbm_format) -> EGLConfig; + + auto egl_display_for_gbm_device(struct gbm_device* const device) -> EGLDisplay; + + EGLint const depth_buffer_bits; + EGLint const stencil_buffer_bits; + EGLDisplay egl_display; + EGLConfig egl_config; + EGLContext egl_context; + EGLSurface egl_surface; + bool should_terminate_egl; + EGLExtensions::PlatformBaseEXT platform_base; +}; +} +} +} +} + +#endif /* MIR_GRAPHICS_GBM_EGL_HELPER_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/kms_display_configuration.h b/src/platforms/atomic-kms/server/kms/kms_display_configuration.h new file mode 100644 index 00000000000..03ead845579 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/kms_display_configuration.h @@ -0,0 +1,47 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_KMS_DISPLAY_CONFIGURATION_H_ +#define MIR_GRAPHICS_GBM_KMS_DISPLAY_CONFIGURATION_H_ + +#include "mir/graphics/display_configuration.h" +#include + +namespace mir +{ +namespace graphics +{ +namespace atomic +{ +class KMSOutput; + +class DRMModeResources; + +class KMSDisplayConfiguration : public DisplayConfiguration +{ +public: + virtual std::shared_ptr get_output_for(DisplayConfigurationOutputId id) const = 0; + virtual size_t get_kms_mode_index( + DisplayConfigurationOutputId id, + size_t conf_mode_index) const = 0; + virtual void update() = 0; +}; + +} +} +} + +#endif /* MIR_GRAPHICS_GBM_KMS_DISPLAY_CONFIGURATION_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/kms_output.h b/src/platforms/atomic-kms/server/kms/kms_output.h new file mode 100644 index 00000000000..8e641bb3a9c --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/kms_output.h @@ -0,0 +1,107 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_KMS_OUTPUT_H_ +#define MIR_GRAPHICS_GBM_KMS_OUTPUT_H_ + +#include "mir/geometry/size.h" +#include "mir/geometry/point.h" +#include "mir/geometry/displacement.h" +#include "mir/graphics/display_configuration.h" +#include "mir/graphics/frame.h" +#include "mir/graphics/dmabuf_buffer.h" +#include "mir_toolkit/common.h" +#include "kms-utils/drm_mode_resources.h" + +#include + +namespace mir +{ +namespace graphics +{ +class DisplayConfigurationOutput; +class FBHandle; + +namespace atomic +{ + +class KMSOutput +{ +public: + virtual ~KMSOutput() = default; + + /* + * I'm not sure that DRM guarantees ID uniqueness in the presence of hotplug/unplug; + * this may want to be an opaque class Id + operator== in future. + */ + virtual uint32_t id() const = 0; + + virtual void reset() = 0; + virtual void configure(geometry::Displacement fb_offset, size_t kms_mode_index) = 0; + virtual geometry::Size size() const = 0; + + /** + * Approximate maximum refresh rate of this output to within 1Hz. + * Typically the rate is fixed (e.g. 60Hz) but it may also be variable as + * in Nvidia G-Sync/AMD FreeSync/VESA Adaptive Sync. So this function + * returns the maximum rate to expect. + */ + virtual int max_refresh_rate() const = 0; + + virtual bool set_crtc(FBHandle const& fb) = 0; + + /** + * Check if the pending call to set_crtc is compatible with the current state of the CRTC. + * @returns true if a set_crtc is required, otherwise false + */ + virtual bool has_crtc_mismatch() = 0; + virtual void clear_crtc() = 0; + virtual bool schedule_page_flip(FBHandle const& fb, void const* flip_completion_userdata) = 0; + + virtual bool set_cursor(gbm_bo* buffer) = 0; + virtual void move_cursor(geometry::Point destination) = 0; + virtual bool clear_cursor() = 0; + virtual bool has_cursor() const = 0; + + virtual void set_power_mode(MirPowerMode mode) = 0; + virtual void set_gamma(GammaCurves const& gamma) = 0; + + /** + * Re-probe the hardware state of this connector. + * + * \throws std::system_error if the underlying DRM connector has disappeared. + */ + virtual void refresh_hardware_state() = 0; + /** + * Translate and copy the cached hardware state into a Mir display configuration object. + * + * \param [out] to_update The Mir display configuration object to update with new + * hardware state. Only hardware state (modes, dimensions, etc) + * is touched. + */ + virtual void update_from_hardware_state(DisplayConfigurationOutput& to_update) const = 0; + + virtual int drm_fd() const = 0; +protected: + KMSOutput() = default; + KMSOutput(const KMSOutput&) = delete; + KMSOutput& operator=(const KMSOutput&) = delete; +}; +} +} +} + +#endif /* MIR_GRAPHICS_GBM_KMS_OUTPUT_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/kms_output_container.h b/src/platforms/atomic-kms/server/kms/kms_output_container.h new file mode 100644 index 00000000000..2d951cb56ab --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/kms_output_container.h @@ -0,0 +1,53 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_KMS_OUTPUT_CONTAINER_H_ +#define MIR_GRAPHICS_GBM_KMS_OUTPUT_CONTAINER_H_ + +#include +#include + +namespace mir +{ +namespace graphics +{ +namespace atomic +{ + +class KMSOutput; + +class KMSOutputContainer +{ +public: + virtual ~KMSOutputContainer() = default; + + virtual void for_each_output(std::function const&)> functor) const = 0; + + /** + * Re-probe hardware state and update output list. + */ + virtual void update_from_hardware_state() = 0; +protected: + KMSOutputContainer() = default; + KMSOutputContainer(KMSOutputContainer const&) = delete; + KMSOutputContainer& operator=(KMSOutputContainer const&) = delete; +}; + +} +} +} + +#endif /* MIR_GRAPHICS_GBM_KMS_OUTPUT_CONTAINER_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/kms_page_flipper.cpp b/src/platforms/atomic-kms/server/kms/kms_page_flipper.cpp new file mode 100644 index 00000000000..a6720541ee3 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/kms_page_flipper.cpp @@ -0,0 +1,188 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "kms_page_flipper.h" +#include "mir/graphics/display_report.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace mg = mir::graphics; +namespace mgg = mir::graphics::atomic; + +namespace +{ + +void page_flip_handler(int /*fd*/, unsigned int seq, + unsigned int sec, unsigned int usec, + void* data) +{ + auto page_flip_data = static_cast(data); + std::chrono::nanoseconds ns{sec*1000000000LL + usec*1000LL}; + page_flip_data->flipper->notify_page_flip(page_flip_data->crtc_id, + seq, ns); +} + +} + +mgg::KMSPageFlipper::KMSPageFlipper( + int drm_fd, + std::shared_ptr const& report) : + drm_fd{drm_fd}, + report{report}, + pending_page_flips(), + worker_tid() +{ + uint64_t mono = 0; + if (drmGetCap(drm_fd, DRM_CAP_TIMESTAMP_MONOTONIC, &mono) || !mono) + clock_id = CLOCK_REALTIME; + else + clock_id = CLOCK_MONOTONIC; +} + +bool mgg::KMSPageFlipper::schedule_flip(uint32_t crtc_id, + uint32_t fb_id, + uint32_t connector_id) +{ + std::unique_lock lock{pf_mutex}; + + if (pending_page_flips.find(crtc_id) != pending_page_flips.end()) + BOOST_THROW_EXCEPTION(std::logic_error("Page flip for crtc_id is already scheduled")); + + pending_page_flips[crtc_id] = PageFlipEventData{crtc_id, connector_id, this}; + + /* + * It appears we can't tell the difference between flipping being + * unsupported or failing for other reasons. On VirtualBox this always + * fails with -22 (Invalid argument) despite the arguments being + * apparently valid. + */ + auto ret = drmModePageFlip(drm_fd, crtc_id, fb_id, + DRM_MODE_PAGE_FLIP_EVENT, + &pending_page_flips[crtc_id]); + + if (ret) + pending_page_flips.erase(crtc_id); + + return (ret == 0); +} + +mg::Frame mgg::KMSPageFlipper::wait_for_flip(uint32_t crtc_id) +{ + drmEventContext evctx; + memset(&evctx, 0, sizeof evctx); + evctx.version = 2; // We only support the old v2 page_flip_handler + evctx.page_flip_handler = &page_flip_handler; + + static std::thread::id const invalid_tid; + + { + std::unique_lock lock{pf_mutex}; + + /* + * While another thread is the worker (it is controlling the + * page flip event loop) and our event has not arrived, wait. + */ + while (worker_tid != invalid_tid && !page_flip_is_done(crtc_id)) + pf_cv.wait(lock); + + /* If the page flip we are waiting for has arrived we are done. */ + if (page_flip_is_done(crtc_id)) + return completed_page_flips[crtc_id]; + + /* ...otherwise we become the worker */ + worker_tid = std::this_thread::get_id(); + } + + /* Only the worker thread reaches this point */ + bool done{false}; + + while (!done) + { + fd_set fds; + FD_ZERO(&fds); + FD_SET(drm_fd, &fds); + + /* + * Wait for a page flip event. When we get a page flip event, + * page_flip_handler(), called through drmHandleEvent(), will update + * the pending_page_flips map. + */ + auto ret = select(drm_fd + 1, &fds, nullptr, nullptr, nullptr); + + { + std::unique_lock lock{pf_mutex}; + + if (ret > 0) + { + drmHandleEvent(drm_fd, &evctx); + } + else if (ret < 0 && errno != EINTR) + { + std::string const msg("Error while waiting for page-flip event"); + BOOST_THROW_EXCEPTION( + boost::enable_error_info( + std::runtime_error(msg)) << boost::errinfo_errno(errno)); + } + + done = page_flip_is_done(crtc_id); + /* Give up loop control if we are done */ + if (done) + worker_tid = invalid_tid; + } + + /* + * Wake up other (non-worker) threads, so they can check whether + * their page-flip events have arrived, or whether they can become + * the worker (see pf_cv.wait(lock) above). + */ + pf_cv.notify_all(); + } + return completed_page_flips[crtc_id]; +} + +std::thread::id mgg::KMSPageFlipper::debug_get_worker_tid() +{ + std::unique_lock lock{pf_mutex}; + + return worker_tid; +} + +/* This method should be called with the 'pf_mutex' locked */ +bool mgg::KMSPageFlipper::page_flip_is_done(uint32_t crtc_id) +{ + return pending_page_flips.find(crtc_id) == pending_page_flips.end(); +} + +void mgg::KMSPageFlipper::notify_page_flip(uint32_t crtc_id, int64_t msc, + std::chrono::nanoseconds ust) +{ + auto pending = pending_page_flips.find(crtc_id); + if (pending != pending_page_flips.end()) + { + auto& frame = completed_page_flips[crtc_id]; + frame.msc = msc; + frame.ust = {clock_id, ust}; + report->report_vsync(pending->second.connector_id, frame); + pending_page_flips.erase(pending); + } +} diff --git a/src/platforms/atomic-kms/server/kms/kms_page_flipper.h b/src/platforms/atomic-kms/server/kms/kms_page_flipper.h new file mode 100644 index 00000000000..4cd287ab6f3 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/kms_page_flipper.h @@ -0,0 +1,76 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_KMS_PAGE_FLIPPER_H_ +#define MIR_GRAPHICS_GBM_KMS_PAGE_FLIPPER_H_ + +#include "page_flipper.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace mir +{ +namespace graphics +{ + +class DisplayReport; + +namespace atomic +{ + +class KMSPageFlipper; +struct PageFlipEventData +{ + uint32_t crtc_id; + uint32_t connector_id; + KMSPageFlipper* flipper; +}; + +class KMSPageFlipper : public PageFlipper +{ +public: + KMSPageFlipper(int drm_fd, std::shared_ptr const& report); + + bool schedule_flip(uint32_t crtc_id, uint32_t fb_id, uint32_t connector_id) override; + Frame wait_for_flip(uint32_t crtc_id) override; + + std::thread::id debug_get_worker_tid(); + + void notify_page_flip(uint32_t crtc_id, int64_t msc, std::chrono::nanoseconds ust); +private: + bool page_flip_is_done(uint32_t crtc_id); + + int const drm_fd; + std::shared_ptr const report; + std::unordered_map pending_page_flips; + std::unordered_map completed_page_flips; + std::mutex pf_mutex; + std::condition_variable pf_cv; + std::thread::id worker_tid; + clockid_t clock_id; +}; + +} +} +} + +#endif /* MIR_GRAPHICS_GBM_KMS_PAGE_FLIPPER_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/platform.cpp b/src/platforms/atomic-kms/server/kms/platform.cpp new file mode 100644 index 00000000000..4b4578fd045 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/platform.cpp @@ -0,0 +1,159 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "platform.h" +#include "display.h" +#include "mir/console_services.h" +#include "mir/emergency_cleanup_registry.h" +#include "mir/graphics/platform.h" +#include "mir/udev/wrapper.h" +#include "one_shot_device_observer.h" +#include +#include +#include + +#define MIR_LOG_COMPONENT "atomic-kms" +#include "mir/log.h" + +namespace mg = mir::graphics; +namespace mga = mg::atomic; + +namespace +{ +auto master_fd_for_device(mir::udev::Device const& device, mir::ConsoleServices& vt) -> std::tuple, mir::Fd> +{ + mir::Fd drm_fd; + auto device_handle = vt.acquire_device( + major(device.devnum()), minor(device.devnum()), + std::make_unique(drm_fd)) + .get(); + + if (drm_fd == mir::Fd::invalid) + { + BOOST_THROW_EXCEPTION((std::runtime_error{"Failed to acquire DRM fd"})); + } + + if (auto err = drmSetClientCap(drm_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + -err, + std::system_category(), + "Failed to enable DRM Universal Planes support"})); + } + + if (auto err = drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1)) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + -err, + std::system_category(), + "Failed to enable Atomic KMS support"})); + } + + return std::make_tuple(std::move(device_handle), std::move(drm_fd)); +} + +auto maybe_make_gbm_provider(mir::Fd drm_fd) -> std::shared_ptr +{ + try + { + return std::make_shared(std::move(drm_fd)); + } + catch (std::exception const& err) + { + mir::log_info("Failed to create GBM device for direct buffer submission"); + mir::log_info("Output will use CPU buffer copies"); + return {}; + } +} +} + +mga::Platform::Platform( + udev::Device const& device, + std::shared_ptr const& listener, + ConsoleServices& vt, + EmergencyCleanupRegistry& registry, + BypassOption bypass_option) + : Platform(master_fd_for_device(device, vt), listener, registry, bypass_option) +{ +} + +mga::Platform::Platform( + std::tuple, mir::Fd> drm, + std::shared_ptr const& listener, + EmergencyCleanupRegistry&, + BypassOption bypass_option) + : udev{std::make_shared()}, + listener{listener}, + device_handle{std::move(std::get<0>(drm))}, + drm_fd{std::move(std::get<1>(drm))}, + gbm_display_provider{maybe_make_gbm_provider(drm_fd)}, + bypass_option_{bypass_option} +{ + if (drm_fd == mir::Fd::invalid) + { + BOOST_THROW_EXCEPTION((std::logic_error{"Invalid DRM device FD"})); + } +} + +mga::Platform::~Platform() = default; + +namespace +{ +auto gbm_device_from_provider(std::shared_ptr const& provider) + -> std::shared_ptr +{ + if (provider) + { + return provider->gbm_device(); + } + return nullptr; +} +} + +mir::UniqueModulePtr mga::Platform::create_display( + std::shared_ptr const& initial_conf_policy, std::shared_ptr const&) +{ + return make_module_ptr( + drm_fd, + gbm_device_from_provider(gbm_display_provider), + bypass_option_, + initial_conf_policy, + listener); +} + +auto mga::Platform::maybe_create_provider(DisplayProvider::Tag const& type_tag) + -> std::shared_ptr +{ + if (dynamic_cast(&type_tag)) + { + return gbm_display_provider; + } + if (dynamic_cast(&type_tag)) + { + /* There's no implementation behind it, but we want to know during probe time + * that the DisplayBuffers will support it. + */ + return std::make_shared(); + } + return {}; +} + +mga::BypassOption mga::Platform::bypass_option() const +{ + return bypass_option_; +} diff --git a/src/platforms/atomic-kms/server/kms/platform.h b/src/platforms/atomic-kms/server/kms/platform.h new file mode 100644 index 00000000000..348294d411d --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/platform.h @@ -0,0 +1,82 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_PLATFORM_H_ +#define MIR_GRAPHICS_GBM_PLATFORM_H_ + +#include "mir/graphics/platform.h" +#include "platform_common.h" +#include "display_helpers.h" + +#include + +namespace mir +{ +class EmergencyCleanupRegistry; +class ConsoleServices; + +namespace graphics +{ +namespace atomic +{ + +class Quirks; + +class Platform : public graphics::DisplayPlatform +{ +public: + Platform( + udev::Device const& device, + std::shared_ptr const& reporter, + ConsoleServices& vt, + EmergencyCleanupRegistry& emergency_cleanup_registry, + BypassOption bypass_option); + + ~Platform() override; + + /* From Platform */ + UniqueModulePtr create_display( + std::shared_ptr const& initial_conf_policy, + std::shared_ptr const& gl_config) override; + + std::shared_ptr udev; + + std::shared_ptr const listener; + +protected: + auto maybe_create_provider(DisplayProvider::Tag const& type_tag) + -> std::shared_ptr override; + +public: + BypassOption bypass_option() const; +private: + Platform( + std::tuple, mir::Fd> drm, + std::shared_ptr const& reporter, + EmergencyCleanupRegistry& emergency_cleanup_registry, + BypassOption bypass_option); + + std::unique_ptr const device_handle; + mir::Fd const drm_fd; + + std::shared_ptr gbm_display_provider; + + BypassOption const bypass_option_; +}; +} +} +} +#endif /* MIR_GRAPHICS_GBM_PLATFORM_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/platform_symbols.cpp b/src/platforms/atomic-kms/server/kms/platform_symbols.cpp new file mode 100644 index 00000000000..486ba73f30a --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/platform_symbols.cpp @@ -0,0 +1,322 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "mir/graphics/platform.h" +#include +#define MIR_LOG_COMPONENT "atomic-kms" +#include "mir/log.h" + +#include "platform.h" +#include "display_helpers.h" +#include "quirks.h" +#include "kms-utils/drm_mode_resources.h" +#include "mir/options/program_option.h" +#include "mir/options/option.h" +#include "mir/options/configuration.h" +#include "mir/udev/wrapper.h" +#include "mir/module_deleter.h" +#include "mir/assert_module_entry_point.h" +#include "mir/libname.h" +#include "mir/console_services.h" +#include "one_shot_device_observer.h" +#include "mir/graphics/egl_error.h" +#include "mir/graphics/gl_config.h" +#include "mir/graphics/egl_logger.h" + +#include +#include +#include "egl_helper.h" +#include +#include + +namespace mg = mir::graphics; +namespace mga = mg::atomic; +namespace mgc = mir::graphics::common; +namespace mo = mir::options; +namespace mgk = mir::graphics::kms; + +namespace +{ +char const* bypass_option_name{"bypass"}; + +} + +mir::UniqueModulePtr create_display_platform( + mg::SupportedDevice const& device, + std::shared_ptr const& options, + std::shared_ptr const& emergency_cleanup_registry, + std::shared_ptr const& console, + std::shared_ptr const& report) +{ + mir::assert_entry_point_signature(&create_display_platform); + // ensure atomic-kms finds the atomic-kms mir-platform symbols + + if (options->is_set(mir::options::debug_opt)) + { + mg::initialise_egl_logger(); + } + + auto bypass_option = mga::BypassOption::allowed; + if (!options->get(bypass_option_name)) + bypass_option = mga::BypassOption::prohibited; + + return mir::make_module_ptr( + *device.device, report, *console, *emergency_cleanup_registry, bypass_option); +} + +void add_graphics_platform_options(boost::program_options::options_description& config) +{ + mir::assert_entry_point_signature(&add_graphics_platform_options); + config.add_options() + (bypass_option_name, + boost::program_options::value()->default_value(false), + "[platform-specific] utilize the bypass optimization for fullscreen surfaces."); + mga::Quirks::add_quirks_option(config); +} + +namespace +{ +class MinimalGLConfig : public mir::graphics::GLConfig +{ +public: + int depth_buffer_bits() const override + { + return 0; + } + + int stencil_buffer_bits() const override + { + return 0; + } +}; +} + +auto probe_display_platform( + std::shared_ptr const& console, + std::shared_ptr const& udev, + mir::options::ProgramOption const& options) -> std::vector +{ + mir::assert_entry_point_signature(&probe_display_platform); + + mga::Quirks quirks{options}; + + mir::udev::Enumerator drm_devices{udev}; + drm_devices.match_subsystem("drm"); + drm_devices.match_sysname("card[0-9]*"); + drm_devices.scan_devices(); + + if (drm_devices.begin() == drm_devices.end()) + { + mir::log_info("Unsupported: No DRM devices detected"); + return {}; + } + + // We also require GBM EGL platform + auto const* client_extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + if (!client_extensions) + { + // Doesn't support EGL client extensions; Mesa does, so this is unlikely to be atomic-kms. + mir::log_info("Unsupported: EGL platform does not support client extensions."); + return {}; + } + if (strstr(client_extensions, "EGL_KHR_platform_atomic") == nullptr) + { + // Doesn't support the Khronos-standardised GBM platform… + mir::log_info("EGL platform does not support EGL_KHR_platform_atomic extension"); + // …maybe we support the old pre-standardised Mesa GBM platform? + if (strstr(client_extensions, "EGL_MESA_platform_atomic") == nullptr) + { + mir::log_info( + "Unsupported: EGL platform supports neither EGL_KHR_platform_atomic nor EGL_MESA_platform_atomic"); + return {}; + } + } + + + // Check for suitability + std::vector supported_devices; + mir::Fd tmp_fd; + for (auto& device : drm_devices) + { + if (quirks.should_skip(device)) + { + mir::log_info("Not probing device %s due to specified quirk", device.devnode()); + continue; + } + + auto const devnum = device.devnum(); + if (devnum == makedev(0, 0)) + { + /* The display connectors attached to the card appear as subdevices + * of the card[0-9] node. + * These won't have a device node, so pass on anything that doesn't have + * a /dev/dri/card* node + */ + continue; + } + + try + { + // Rely on the console handing us a DRM master... + auto const device_cleanup = console->acquire_device( + major(devnum), minor(devnum), + std::make_unique(tmp_fd)).get(); + + // We have a device. We don't know if it works yet, but it's a device we *could* support + supported_devices.emplace_back( + mg::SupportedDevice{ + device.clone(), + mg::probe::unsupported, + nullptr + }); + + if (tmp_fd != mir::Fd::invalid) + { + // Check that the drm device is usable by setting the interface version we use (1.4) + drmSetVersion sv; + sv.drm_di_major = 1; + sv.drm_di_minor = 4; + sv.drm_dd_major = -1; /* Don't care */ + sv.drm_dd_minor = -1; /* Don't care */ + + if (auto error = -drmSetInterfaceVersion(tmp_fd, &sv)) + { + BOOST_THROW_EXCEPTION((std::system_error{ + error, + std::system_category(), + std::string{"Failed to set DRM interface version on device "} + device.devnode()})); + } + + if (!mgk::get_cap_checked(tmp_fd, DRM_CLIENT_CAP_ATOMIC)) + { + mir::log_info("KMS device %s does not support Atomic KMS", device.devnode()); + continue; + } + + // For now, we *also* require our DisplayPlatform to support creating a HW EGL context + mga::helpers::GBMHelper atomic_device{tmp_fd}; + mga::helpers::EGLHelper egl{MinimalGLConfig()}; + + egl.setup(atomic_device); + + egl.make_current(); + + auto const renderer_string = reinterpret_cast(glGetString(GL_RENDERER)); + if (!renderer_string) + { + throw mg::gl_error( + "Probe failed to query GL renderer"); + } + + using namespace std::literals::string_literals; + if ("llvmpipe"s == renderer_string) + { + mir::log_info("KMS device only has associated software renderer: %s, device unsuitable", renderer_string); + supported_devices.back().support_level = mg::probe::unsupported; + continue; + } + + /* Check if modesetting is supported on this DRM node + * This must be done after drmSetInterfaceVersion() as, for Hysterical Raisins, + * drmGetBusid() will return nullptr unless drmSetInterfaceVersion() has already been called + */ + auto const busid = std::unique_ptr{ + drmGetBusid(tmp_fd), + &drmFreeBusid + }; + + if (!busid) + { + mir::log_warning( + "Failed to query BusID for device %s; cannot check if KMS is available", + device.devnode()); + supported_devices.back().support_level = mg::probe::supported; + } + else + { + mg::kms::DRMModeResources kms_resources{tmp_fd}; + switch (auto err = -drmCheckModesettingSupported(busid.get())) + { + case 0: + // We've got a DRM device that supports KMS. Let's see if it's got any output hardware! + if ((kms_resources.num_connectors() > 0) && + (kms_resources.num_crtcs() > 0) && + (kms_resources.num_encoders() > 0)) + { + // It supports KMS *and* can drive at least one physical output! Top hole! + supported_devices.back().support_level = mg::probe::best; + } + else + { + mir::log_info("KMS support found, but device has no output hardware."); + mir::log_info("This is probably a render-only hybrid graphics device"); + } + break; + + case ENOSYS: + if (quirks.require_modesetting_support(device)) + { + throw std::runtime_error{std::string{"Device "}+device.devnode()+" does not support KMS"}; + } + + [[fallthrough]]; + case EINVAL: + mir::log_warning( + "Failed to detect whether device %s supports KMS, continuing with lower confidence", + device.devnode()); + supported_devices.back().support_level = mg::probe::supported; + break; + + default: + mir::log_warning("Unexpected error from drmCheckModesettingSupported(): %s (%i), " + "but continuing anyway", strerror(err), err); + mir::log_warning("Please file a bug at " + "https://github.com/MirServer/mir/issues containing this message"); + supported_devices.back().support_level = mg::probe::supported; + } + } + } + } + catch (std::exception const& e) + { + mir::log( + mir::logging::Severity::informational, + MIR_LOG_COMPONENT, + std::current_exception(), + "Failed to probe DRM device"); + } + } + + return supported_devices; +} + +namespace +{ +mir::ModuleProperties const description = { + "mir:atomic-kms", + MIR_VERSION_MAJOR, + MIR_VERSION_MINOR, + MIR_VERSION_MICRO, + mir::libname() +}; +} + +mir::ModuleProperties const* describe_graphics_module() +{ + mir::assert_entry_point_signature(&describe_graphics_module); + return &description; +} + diff --git a/src/platforms/atomic-kms/server/kms/quirks.cpp b/src/platforms/atomic-kms/server/kms/quirks.cpp new file mode 100644 index 00000000000..cf4740d203e --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/quirks.cpp @@ -0,0 +1,204 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "quirks.h" + +#include "mir/log.h" +#include "mir/options/option.h" +#include "mir/udev/wrapper.h" + +#include +#include + +namespace mga = mir::graphics::atomic; +namespace mo = mir::options; + +namespace +{ +char const* quirks_option_name = "driver-quirks"; +} + +namespace +{ +auto value_or(char const* maybe_null_string, char const* value_if_null) -> char const* +{ + if (maybe_null_string) + { + return maybe_null_string; + } + else + { + return value_if_null; + } +} +} + +class mga::Quirks::Impl +{ +public: + explicit Impl(mo::Option const& options) + { + if (!options.is_set(quirks_option_name)) + { + return; + } + + for (auto const& quirk : options.get>(quirks_option_name)) + { + auto const disable_kms_probe = "disable-kms-probe:"; + auto const skip_devnode = "skip:devnode:"; + auto const skip_driver = "skip:driver:"; + auto const allow_devnode = "allow:devnode:"; + auto const allow_driver = "allow:driver:"; + + if (quirk.starts_with(skip_devnode)) + { + devnodes_to_skip.insert(quirk.substr(strlen(skip_devnode))); + continue; + } + else if (quirk.starts_with(skip_driver)) + { + drivers_to_skip.insert(quirk.substr(strlen(skip_driver))); + continue; + } + else if (quirk.starts_with(allow_devnode)) + { + devnodes_to_skip.erase(quirk.substr(strlen(allow_devnode))); + continue; + } + else if (quirk.starts_with(allow_driver)) + { + drivers_to_skip.erase(quirk.substr(strlen(allow_driver))); + continue; + } + else if (quirk.starts_with(disable_kms_probe)) + { + // Quirk format is disable-kms-probe:value + skip_modesetting_support.emplace(quirk.substr(strlen(disable_kms_probe))); + continue; + } + + // If we didn't `continue` above, we're ignoring... + mir::log_warning( + "Ignoring unexpected value for %s option: %s " + "(expects value of the form “skip::”, “allow::” or ”disable-kms-probe:”)", + quirks_option_name, + quirk.c_str()); + } + } + + auto should_skip(udev::Device const& device) const -> bool + { + auto const devnode = value_or(device.devnode(), ""); + auto const parent_device = device.parent(); + auto const driver = + [&]() + { + if (parent_device) + { + return value_or(parent_device->driver(), ""); + } + mir::log_warning("udev device has no parent! Unable to determine driver for quirks."); + return ""; + }(); + mir::log_debug("Quirks: checking device with devnode: %s, driver %s", device.devnode(), driver); + bool const should_skip_driver = drivers_to_skip.count(driver); + bool const should_skip_devnode = devnodes_to_skip.count(devnode); + if (should_skip_driver) + { + mir::log_info("Quirks: skipping device %s (matches driver quirk %s)", devnode, driver); + } + if (should_skip_devnode) + { + mir::log_info("Quirks: skipping device %s (matches devnode quirk %s)", devnode, devnode); + } + return should_skip_driver || should_skip_devnode; + } + + auto require_modesetting_support(mir::udev::Device const& device) const -> bool + { + auto const devnode = value_or(device.devnode(), ""); + auto const parent_device = device.parent(); + auto const driver = + [&]() + { + if (parent_device) + { + return value_or(parent_device->driver(), ""); + } + mir::log_warning("udev device has no parent! Unable to determine driver for quirks."); + return ""; + }(); + mir::log_debug("Quirks: checking device with devnode: %s, driver %s", device.devnode(), driver); + + bool const should_skip_modesetting_support = skip_modesetting_support.count(driver); + if (should_skip_modesetting_support) + { + mir::log_info("Quirks: skipping modesetting check %s (matches driver quirk %s)", devnode, driver); + } + return !should_skip_modesetting_support; + } + +private: + /* AST is a simple 2D output device, built into some motherboards. + * They do not have any 3D engine associated, so were quirked off to avoid https://github.com/canonical/mir/issues/2678 + * + * At least as of drivers ≤ version 550, the NVIDIA atomic implementation is buggy in a way that prevents + * Mir from working. Quirk off atomic-kms on NVIDIA. + */ + std::unordered_set drivers_to_skip = { "nvidia", "ast" }; + std::unordered_set devnodes_to_skip; + // We know this is currently useful for virtio_gpu, vc4-drm and v3d + std::unordered_set skip_modesetting_support = { "virtio_gpu", "vc4-drm", "v3d" }; +}; + +mga::Quirks::Quirks(const options::Option& options) + : impl{std::make_unique(options)} +{ +} + +mga::Quirks::~Quirks() = default; + +auto mga::Quirks::should_skip(udev::Device const& device) const -> bool +{ + return impl->should_skip(device); +} + +void mga::Quirks::add_quirks_option(boost::program_options::options_description& config) +{ + config.add_options() + (quirks_option_name, + boost::program_options::value>(), + "[platform-specific] Driver quirks to apply (may be specified multiple times; multiple quirks are combined)"); +} + +auto mir::graphics::atomic::Quirks::require_modesetting_support(mir::udev::Device const& device) const -> bool +{ + if (getenv("MIR_MESA_KMS_DISABLE_MODESET_PROBE") != nullptr) + { + mir::log_debug("MIR_MESA_KMS_DISABLE_MODESET_PROBE is set"); + return false; + } + else if (getenv("MIR_GBM_KMS_DISABLE_MODESET_PROBE") != nullptr) + { + mir::log_debug("MIR_GBM_KMS_DISABLE_MODESET_PROBE is set"); + return false; + } + else + { + return impl->require_modesetting_support(device); + } +} diff --git a/src/platforms/atomic-kms/server/kms/quirks.h b/src/platforms/atomic-kms/server/kms/quirks.h new file mode 100644 index 00000000000..0e3427c4d72 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/quirks.h @@ -0,0 +1,67 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_KMS_QUIRKS_H_ +#define MIR_GRAPHICS_GBM_KMS_QUIRKS_H_ + +#include +#include + +namespace mir +{ +namespace options +{ +class Option; +} +namespace udev +{ +class Device; +} + +namespace graphics::atomic +{ +/** + * Interface for querying device-specific quirks + */ +class Quirks +{ +public: + explicit Quirks(options::Option const& options); + ~Quirks(); + + /** + * Should this device be skipped entirely from use and probing? + */ + [[nodiscard]] + auto should_skip(udev::Device const& device) const -> bool; + + /** + * Should we require this device to have modesetting support? + */ + [[nodiscard]] + auto require_modesetting_support(udev::Device const& device) const -> bool; + + static void add_quirks_option(boost::program_options::options_description& config); + +private: + class Impl; + std::unique_ptr const impl; +}; +} +} + + +#endif //MIR_GRAPHICS_GBM_KMS_QUIRKS_H_ diff --git a/src/platforms/atomic-kms/server/kms/real_kms_display_configuration.cpp b/src/platforms/atomic-kms/server/kms/real_kms_display_configuration.cpp new file mode 100644 index 00000000000..1e02c883f3e --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/real_kms_display_configuration.cpp @@ -0,0 +1,235 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "real_kms_display_configuration.h" +#include "kms_output.h" +#include "kms_output_container.h" +#include "mir/graphics/pixel_format_utils.h" +#include "mir/log.h" +#include "mir/output_type_names.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace mg = mir::graphics; +namespace mga = mir::graphics::atomic; +namespace mgk = mir::graphics::kms; +namespace geom = mir::geometry; + +mga::RealKMSDisplayConfiguration::RealKMSDisplayConfiguration( + std::shared_ptr const& displays) + : displays{displays}, + card{mg::DisplayConfigurationCardId{0}, 0} +{ + update(); +} + +mga::RealKMSDisplayConfiguration::RealKMSDisplayConfiguration( + RealKMSDisplayConfiguration const& conf) + : KMSDisplayConfiguration(), + displays{conf.displays}, + card{conf.card}, + outputs{conf.outputs} +{ +} + +mga::RealKMSDisplayConfiguration& mga::RealKMSDisplayConfiguration::operator=( + RealKMSDisplayConfiguration const& conf) +{ + if (&conf != this) + { + displays = conf.displays; + card = conf.card; + outputs = conf.outputs; + } + + return *this; +} + +void mga::RealKMSDisplayConfiguration::for_each_output( + std::function f) const +{ + for (auto const& output_pair : outputs) + f(output_pair.first); +} + +void mga::RealKMSDisplayConfiguration::for_each_output( + std::function f) +{ + for (auto& output_pair : outputs) + { + UserDisplayConfigurationOutput user(output_pair.first); + f(user); + } +} + +std::unique_ptr mga::RealKMSDisplayConfiguration::clone() const +{ + return std::make_unique(*this); +} + +mga::RealKMSDisplayConfiguration::Output const& +mga::RealKMSDisplayConfiguration::output(DisplayConfigurationOutputId id) const +{ + return outputs.at(id.as_value() - 1); +} + +std::shared_ptr mga::RealKMSDisplayConfiguration::get_output_for( + DisplayConfigurationOutputId id) const +{ + return output(id).second; +} + +size_t mga::RealKMSDisplayConfiguration::get_kms_mode_index( + DisplayConfigurationOutputId id, + size_t conf_mode_index) const +{ + if (static_cast(id.as_value()) > outputs.size()) + { + BOOST_THROW_EXCEPTION(std::invalid_argument("Request for KMS mode index of invalid output ID")); + } + if (conf_mode_index > output(id).first.modes.size()) + { + BOOST_THROW_EXCEPTION(std::invalid_argument("Request for out-of-bounds KMS mode index")); + } + + return conf_mode_index; +} + +namespace +{ +void populate_default_mir_config(mg::DisplayConfigurationOutput& to_populate) +{ + to_populate.card_id = mg::DisplayConfigurationCardId{0}; + to_populate.gamma_supported = mir_output_gamma_supported; + to_populate.orientation = mir_orientation_normal; + to_populate.form_factor = mir_form_factor_monitor; + to_populate.scale = 1.0f; + to_populate.top_left = geom::Point{}; + to_populate.used = false; + to_populate.pixel_formats = {mir_pixel_format_xrgb_8888, mir_pixel_format_argb_8888}; + to_populate.current_format = mir_pixel_format_xrgb_8888; + to_populate.current_mode_index = std::numeric_limits::max(); +} + +void name_outputs(std::vector>>& outputs) +{ + std::map> card_map; + + for (auto& output_pair : outputs) + { + auto& conf_output = output_pair.first; + auto const type = conf_output.type; + auto const index_by_type = ++card_map[conf_output.card_id][type]; + + std::ostringstream out; + + out << mir::output_type_name(static_cast(type)); + if (conf_output.card_id.as_value() > 0) + out << '-' << conf_output.card_id.as_value(); + out << '-' << index_by_type; + + conf_output.name = out.str(); + } +} +} + +void mga::RealKMSDisplayConfiguration::update() +{ + decltype(outputs) new_outputs; + + displays->update_from_hardware_state(); + displays->for_each_output( + [this, &new_outputs](auto const& output) mutable + { + DisplayConfigurationOutput mir_config; + + auto const existing_output = std::find_if( + outputs.begin(), + outputs.end(), + [&output](auto const& candidate) + { + // Pointer comparison; is this KMSOutput object already present? + return candidate.second == output; + }); + if (existing_output == outputs.end()) + { + populate_default_mir_config(mir_config); + } + else + { + mir_config = existing_output->first; + } + + output->update_from_hardware_state(mir_config); + mir_config.id = DisplayConfigurationOutputId{int(new_outputs.size() + 1)}; + + new_outputs.emplace_back(mir_config, output); + }); + + name_outputs(new_outputs); + outputs = new_outputs; + + /* + * This is not the true max simultaneous outputs, but it's unclear whether it's possible + * to provide a max_simultaneous_outputs value that is useful to clients. + */ + card.max_simultaneous_outputs = outputs.size(); +} + +// Compatibility means conf1 can be attained from conf2 (and vice versa) +// without recreating the display buffers (e.g. conf1 and conf2 are identical +// except one of the outputs of conf1 is rotated w.r.t. that of conf2). If +// the two outputs differ in their power state, the display buffers would need +// to be allocated/destroyed, and hence should not be considered compatible. +bool mga::compatible(mga::RealKMSDisplayConfiguration const& conf1, mga::RealKMSDisplayConfiguration const& conf2) +{ + bool compatible{ + (conf1.card == conf2.card) && + (conf1.outputs.size() == conf2.outputs.size())}; + + if (compatible) + { + unsigned int const count = conf1.outputs.size(); + + for (unsigned int i = 0; i < count; ++i) + { + compatible &= (conf1.outputs[i].first.power_mode == conf2.outputs[i].first.power_mode); + if (compatible) + { + auto clone = conf2.outputs[i].first; + + // ignore difference in orientation, scale factor, form factor, subpixel arrangement + clone.orientation = conf1.outputs[i].first.orientation; + clone.subpixel_arrangement = conf1.outputs[i].first.subpixel_arrangement; + clone.scale = conf1.outputs[i].first.scale; + clone.form_factor = conf1.outputs[i].first.form_factor; + clone.custom_logical_size = conf1.outputs[i].first.custom_logical_size; + compatible &= (conf1.outputs[i].first == clone); + } + else + break; + } + } + + return compatible; +} diff --git a/src/platforms/atomic-kms/server/kms/real_kms_display_configuration.h b/src/platforms/atomic-kms/server/kms/real_kms_display_configuration.h new file mode 100644 index 00000000000..b58c37d5c26 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/real_kms_display_configuration.h @@ -0,0 +1,65 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_ATOMIC_REAL_KMS_DISPLAY_CONFIGURATION_H_ +#define MIR_GRAPHICS_ATOMIC_REAL_KMS_DISPLAY_CONFIGURATION_H_ + +#include "kms_display_configuration.h" + +#include + +namespace mir +{ +namespace graphics +{ +namespace atomic +{ +class KMSOutput; +class KMSOutputContainer; + +class RealKMSDisplayConfiguration : public KMSDisplayConfiguration +{ +friend bool compatible(RealKMSDisplayConfiguration const& conf1, RealKMSDisplayConfiguration const& conf2); + +public: + RealKMSDisplayConfiguration(std::shared_ptr const& displays); + RealKMSDisplayConfiguration(RealKMSDisplayConfiguration const& conf); + RealKMSDisplayConfiguration& operator=(RealKMSDisplayConfiguration const& conf); + + void for_each_output(std::function f) const override; + void for_each_output(std::function f) override; + std::unique_ptr clone() const override; + + std::shared_ptr get_output_for(DisplayConfigurationOutputId id) const override; + size_t get_kms_mode_index(DisplayConfigurationOutputId id, size_t conf_mode_index) const override; + void update() override; + +private: + + std::shared_ptr displays; + DisplayConfigurationCard card; + typedef std::pair> Output; + Output const& output(DisplayConfigurationOutputId id) const; + std::vector outputs; +}; + +bool compatible(RealKMSDisplayConfiguration const& conf1, RealKMSDisplayConfiguration const& conf2); + +} +} +} + +#endif /* MIR_GRAPHICS_ATOMIC_REAL_KMS_DISPLAY_CONFIGURATION_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/real_kms_output_container.cpp b/src/platforms/atomic-kms/server/kms/real_kms_output_container.cpp new file mode 100644 index 00000000000..abd4d6d2fe4 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/real_kms_output_container.cpp @@ -0,0 +1,74 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include +#include "real_kms_output_container.h" +#include "atomic_kms_output.h" +#include "kms-utils/drm_mode_resources.h" + +namespace mga = mir::graphics::atomic; + +mga::RealKMSOutputContainer::RealKMSOutputContainer( + mir::Fd drm_fd) + : drm_fd{std::move(drm_fd)} +{ +} + +void mga::RealKMSOutputContainer::for_each_output(std::function const&)> functor) const +{ + for(auto& output: outputs) + functor(output); +} + +void mga::RealKMSOutputContainer::update_from_hardware_state() +{ + decltype(outputs) new_outputs; + + auto const resources = std::make_unique(drm_fd); + + for (auto &&connector : resources->connectors()) + { + // Caution: O(n²) here, but n is the number of outputs, so should + // conservatively be << 100. + auto existing_output = std::find_if( + outputs.begin(), + outputs.end(), + [&connector, this](auto const &candidate) + { + return + connector->connector_id == candidate->id() && + drm_fd == candidate->drm_fd(); + }); + + if (existing_output != outputs.end()) + { + // We could drop this down to O(n) by being smarter about moving out + // of the outputs vector. + // + // That's a bit of a faff, so just do the simple thing for now. + new_outputs.push_back(*existing_output); + new_outputs.back()->refresh_hardware_state(); + } + else + { + new_outputs.push_back(std::make_shared( + drm_fd, + std::move(connector))); + } + } + + outputs = new_outputs; +} diff --git a/src/platforms/atomic-kms/server/kms/real_kms_output_container.h b/src/platforms/atomic-kms/server/kms/real_kms_output_container.h new file mode 100644 index 00000000000..a8250054a16 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/real_kms_output_container.h @@ -0,0 +1,53 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_REAL_KMS_OUTPUT_CONTAINER_H_ +#define MIR_GRAPHICS_GBM_REAL_KMS_OUTPUT_CONTAINER_H_ + +#include "kms_output_container.h" +#include "mir/fd.h" +#include + +namespace mir +{ +namespace graphics +{ +namespace kms +{ +class DRMEventHandler; +} + +namespace atomic +{ + +class RealKMSOutputContainer : public KMSOutputContainer +{ +public: + explicit RealKMSOutputContainer(mir::Fd drm_fd); + + void for_each_output(std::function const&)> functor) const override; + + void update_from_hardware_state() override; +private: + mir::Fd const drm_fd; + std::vector> outputs; +}; + +} +} +} + +#endif /* MIR_GRAPHICS_GBM_REAL_KMS_OUTPUT_CONTAINER_H_ */ diff --git a/src/platforms/atomic-kms/server/kms/symbols.map.in b/src/platforms/atomic-kms/server/kms/symbols.map.in new file mode 100644 index 00000000000..30dad63f1b7 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/symbols.map.in @@ -0,0 +1,9 @@ +@MIR_SERVER_GRAPHICS_PLATFORM_VERSION@ { + global: + add_graphics_platform_options; + probe_display_platform; + describe_graphics_module; + create_display_platform; + local: + *; +}; diff --git a/src/platforms/atomic-kms/server/platform_common.h b/src/platforms/atomic-kms/server/platform_common.h new file mode 100644 index 00000000000..cd7488fca09 --- /dev/null +++ b/src/platforms/atomic-kms/server/platform_common.h @@ -0,0 +1,36 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_GRAPHICS_GBM_PLATFORM_COMMON_H_ +#define MIR_GRAPHICS_GBM_PLATFORM_COMMON_H_ + +namespace mir +{ +namespace graphics +{ +namespace atomic +{ + +enum class BypassOption +{ + allowed, + prohibited +}; + +} +} +} +#endif /* MIR_GRAPHICS_GBM_PLATFORM_COMMON_H_ */ diff --git a/tests/mir_test_doubles/CMakeLists.txt b/tests/mir_test_doubles/CMakeLists.txt index e24a66c5941..2943c401277 100644 --- a/tests/mir_test_doubles/CMakeLists.txt +++ b/tests/mir_test_doubles/CMakeLists.txt @@ -75,7 +75,7 @@ if (MIR_BUILD_PLATFORM_X11) ) endif() -if (MIR_BUILD_PLATFORM_GBM_KMS) +if (MIR_BUILD_PLATFORM_GBM_KMS OR MIR_BUILD_PLATFORM_ATOMIC_KMS) include_directories( ${PROJECT_SOURCE_DIR}/src/platforms/gbm-kms/server ${CMAKE_SOURCE_DIR} diff --git a/tests/unit-tests/CMakeLists.txt b/tests/unit-tests/CMakeLists.txt index f297e1fb503..e3e474a9cfd 100644 --- a/tests/unit-tests/CMakeLists.txt +++ b/tests/unit-tests/CMakeLists.txt @@ -8,6 +8,10 @@ if (MIR_BUILD_PLATFORM_GBM_KMS) add_compile_definitions(MIR_BUILD_PLATFORM_GBM_KMS) endif() +if (MIR_BUILD_PLATFORM_ATOMIC_KMS) + add_compile_definitions(MIR_BUILD_PLATFORM_ATOMIC_KMS) +endif() + if (MIR_BUILD_PLATFORM_X11) add_compile_definitions(MIR_BUILD_PLATFORM_X11) endif() diff --git a/tests/unit-tests/graphics/test_platform_prober.cpp b/tests/unit-tests/graphics/test_platform_prober.cpp index af833f2801f..69f24ba1706 100644 --- a/tests/unit-tests/graphics/test_platform_prober.cpp +++ b/tests/unit-tests/graphics/test_platform_prober.cpp @@ -43,7 +43,7 @@ #include "mir/test/doubles/null_logger.h" #include "mir/test/doubles/mock_egl.h" -#if defined(MIR_BUILD_PLATFORM_GBM_KMS) +#if defined(MIR_BUILD_PLATFORM_GBM_KMS) || defined(MIR_BUILD_PLATFORM_ATOMIC_KMS) #include "mir/test/doubles/mock_drm.h" #include "mir/test/doubles/mock_gbm.h" #include "mir/test/doubles/mock_gl.h" @@ -69,6 +69,9 @@ std::vector> available_platforms() #ifdef MIR_BUILD_PLATFORM_GBM_KMS modules.push_back(std::make_shared(mtf::server_platform("graphics-gbm-kms"))); +#endif +#ifdef MIR_BUILD_PLATFORM_ATOMIC_KMS + modules.push_back(std::make_shared(mtf::server_platform("graphics-atomic-kms"))); #endif return modules; } @@ -96,12 +99,12 @@ std::shared_ptr ensure_mesa_probing_succeeds() struct MockEnvironment { mtf::UdevEnvironment udev; testing::NiceMock egl; -#ifdef MIR_BUILD_PLATFORM_GBM_KMS +#if defined(MIR_BUILD_PLATFORM_GBM_KMS) || defined(MIR_BUILD_PLATFORM_ATOMIC_KMS) testing::NiceMock gbm; testing::NiceMock gl; #endif }; -#ifdef MIR_BUILD_PLATFORM_GBM_KMS +#if defined (MIR_BUILD_PLATFORM_GBM_KMS) || defined(MIR_BUILD_PLATFORM_ATOMIC_KMS) static auto const fake_gbm_device = reinterpret_cast(0xa1b2c3d4); #endif static auto const fake_egl_display = reinterpret_cast(0xeda); @@ -110,7 +113,7 @@ std::shared_ptr ensure_mesa_probing_succeeds() env->udev.add_standard_device("standard-drm-devices"); ON_CALL(env->egl, eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS)) .WillByDefault(Return("EGL_MESA_platform_gbm EGL_EXT_platform_base")); -#ifdef MIR_BUILD_PLATFORM_GBM_KMS +#if defined(MIR_BUILD_PLATFORM_GBM_KMS) || defined(MIR_BUILD_PLATFORM_ATOMIC_KMS) ON_CALL(env->gbm, gbm_create_device(_)) .WillByDefault(Return(fake_gbm_device)); ON_CALL(env->egl, eglGetDisplay(fake_gbm_device)) @@ -122,7 +125,7 @@ std::shared_ptr ensure_mesa_probing_succeeds() SetArgPointee<1>(1), SetArgPointee<2>(4), Return(EGL_TRUE))); -#ifdef MIR_BUILD_PLATFORM_GBM_KMS +#if defined(MIR_BUILD_PLATFORM_GBM_KMS) || defined(MIR_BUILD_PLATFORM_ATOMIC_KMS) ON_CALL(env->egl, eglGetConfigAttrib(_, env->egl.fake_configs[0], EGL_NATIVE_VISUAL_ID, _)) .WillByDefault( DoAll( @@ -175,7 +178,7 @@ class StubConsoleServices : public mir::ConsoleServices class ServerPlatformProbeMockDRM : public ::testing::Test { -#if defined(MIR_BUILD_PLATFORM_GBM_KMS) +#if defined(MIR_BUILD_PLATFORM_GBM_KMS) || defined(MIR_BUILD_PLATFORM_ATOMIC_KMS) public: ::testing::NiceMock mock_drm; #endif @@ -192,7 +195,7 @@ TEST(ServerPlatformProbe, ConstructingWithNoModulesIsAnError) std::runtime_error); } -#ifdef MIR_BUILD_PLATFORM_GBM_KMS +#if defined(MIR_BUILD_PLATFORM_GBM_KMS) || defined(MIR_BUILD_PLATFORM_ATOMIC_KMS) TEST_F(ServerPlatformProbeMockDRM, LoadsMesaPlatformWhenDrmMasterCanBeAcquired) { using namespace testing; @@ -522,11 +525,11 @@ class FullProbeStack : public testing::Test .WillByDefault([this](auto, auto) { return egl_client_extensions.c_str(); }); } - auto add_kms_device(std::string const& driver_name = "i915") -> std::unique_ptr + auto add_kms_device(std::string const& driver_name [[maybe_unused]] = "i915") -> std::unique_ptr { using namespace std::string_literals; using namespace testing; -#ifndef MIR_BUILD_PLATFORM_GBM_KMS +#if !defined(MIR_BUILD_PLATFORM_GBM_KMS) && !defined(MIR_BUILD_PLATFORM_ATOMIC_KMS) return nullptr; #else @@ -666,10 +669,8 @@ class FullProbeStack : public testing::Test testing::NiceMock x11; testing::NiceMock egl; testing::NiceMock gl; -#ifdef MIR_BUILD_PLATFORM_GBM_KMS +#if defined(MIR_BUILD_PLATFORM_GBM_KMS) || defined(MIR_BUILD_PLATFORM_ATOMIC_KMS) testing::NiceMock gbm; -#endif -#if defined(MIR_BUILD_PLATFORM_GBM_KMS) int drm_device_count{0}; testing::NiceMock drm; #endif