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 a1130e9e478..912f937abdb 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..2553e8d2110 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/atomic_kms_output.cpp @@ -0,0 +1,749 @@ +/* + * 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_event_handler.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, + std::shared_ptr event_handler) + : drm_fd_{drm_master}, + event_handler{std::move(event_handler)}, + 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) +{ + 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); + + pending_page_flip = event_handler->expect_flip_event(current_crtc->crtc_id, [](auto, auto){}); + auto ret = drmModeAtomicCommit( + drm_fd_, + update, + DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT, + const_cast(event_handler->drm_event_data())); + if (ret) + { + mir::log_error("Failed to set CRTC: %s (%i)", strerror(-ret), -ret); + current_crtc = nullptr; + event_handler->cancel_flip_events(current_crtc->crtc_id); + return false; + } + + using_saved_crtc = false; + return true; +} + +void mga::AtomicKMSOutput::wait_for_page_flip() +{ + pending_page_flip.get(); +} + +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..c10a406479c --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/atomic_kms_output.h @@ -0,0 +1,103 @@ +/* + * 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 +#include + +namespace mir +{ +namespace graphics +{ +namespace kms +{ +class DRMEventHandler; +} +namespace atomic +{ + +class PageFlipper; + +class AtomicKMSOutput : public KMSOutput +{ +public: + AtomicKMSOutput( + mir::Fd drm_master, + kms::DRMModeConnectorUPtr connector, + std::shared_ptr event_handler); + ~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) override; + void wait_for_page_flip() 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_; + std::shared_ptr const event_handler; + + std::future pending_page_flip; + + 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..dc0aa42734b --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/display.cpp @@ -0,0 +1,498 @@ +/* + * 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(this->drm_fd)}, + gbm{std::move(gbm)}, + listener(listener), + monitor(mir::udev::Context()), + output_container{ + std::make_shared( + this->drm_fd, + event_handler)}, + 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..4c5c17777df --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/display_buffer.cpp @@ -0,0 +1,358 @@ +/* + * 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 + +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} +{ + 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. + */ + /* TODO: This works badly if *some* outputs successfully flipped and + * others did not. We should instead have exactly one KMSOutput per DisplaySink + */ + for (auto& output : outputs) + { + if (output->schedule_page_flip(bufobj)) + { + pending_flips.push_back(output.get()); + } + } + + return !pending_flips.empty(); +} + +void mga::DisplaySink::wait_for_page_flip() +{ + if (!pending_flips.empty()) + { + for (auto pending_flip : pending_flips) + { + pending_flip->wait_for_page_flip(); + } + pending_flips.clear(); + + // The previously-scheduled FB has been page-flipped, and is now visible + visible_fb = std::move(scheduled_fb); + scheduled_fb = nullptr; + } +} + +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..7257ce5263b --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/display_sink.h @@ -0,0 +1,126 @@ +/* + * 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> const 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}; +}; + +} +} +} + +#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..6772556d240 --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/kms_output.h @@ -0,0 +1,109 @@ +/* + * 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) = 0; + virtual void wait_for_page_flip() = 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..fbc4c07513b --- /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::Option 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_gbm") == nullptr) + { + // Doesn't support the Khronos-standardised GBM platform… + mir::log_info("EGL platform does not support EGL_KHR_platform_gbm extension"); + // …maybe we support the old pre-standardised Mesa GBM platform? + if (strstr(client_extensions, "EGL_MESA_platform_gbm") == nullptr) + { + mir::log_info( + "Unsupported: EGL platform supports neither EGL_KHR_platform_gbm nor EGL_MESA_platform_gbm"); + 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..7e355f13f5a --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/real_kms_output_container.cpp @@ -0,0 +1,78 @@ +/* + * 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_event_handler.h" +#include "kms-utils/drm_mode_resources.h" + +namespace mga = mir::graphics::atomic; + +mga::RealKMSOutputContainer::RealKMSOutputContainer( + mir::Fd drm_fd, + std::shared_ptr event_handler) + : drm_fd{std::move(drm_fd)}, + event_handler{std::move(event_handler)} +{ +} + +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), + event_handler)); + } + } + + 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..97641bd876e --- /dev/null +++ b/src/platforms/atomic-kms/server/kms/real_kms_output_container.h @@ -0,0 +1,54 @@ +/* + * 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: + RealKMSOutputContainer(mir::Fd drm_fd, std::shared_ptr event_handler); + + void for_each_output(std::function const&)> functor) const override; + + void update_from_hardware_state() override; +private: + mir::Fd const drm_fd; + std::shared_ptr const event_handler; + 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/src/platforms/common/server/kms-utils/threaded_drm_event_handler.cpp b/src/platforms/common/server/kms-utils/threaded_drm_event_handler.cpp index fc68ab7cb68..4de5556d0f0 100644 --- a/src/platforms/common/server/kms-utils/threaded_drm_event_handler.cpp +++ b/src/platforms/common/server/kms-utils/threaded_drm_event_handler.cpp @@ -15,8 +15,6 @@ */ #include "threaded_drm_event_handler.h" -#include "mir/log.h" -#include "mir/logging/logger.h" #include #include 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 e607abfa613..6a260cdc464 100644 --- a/tests/unit-tests/graphics/test_platform_prober.cpp +++ b/tests/unit-tests/graphics/test_platform_prober.cpp @@ -44,7 +44,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" @@ -70,6 +70,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; } @@ -97,12 +100,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); @@ -111,7 +114,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)) @@ -123,7 +126,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( @@ -176,7 +179,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 @@ -210,7 +213,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; @@ -540,11 +543,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 @@ -693,10 +696,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