From 129b07481d7607fbc41cfe4057fa348993d36a66 Mon Sep 17 00:00:00 2001 From: Igor Demyanov Date: Fri, 23 Oct 2026 18:45:57 +0300 Subject: [PATCH] support dpms v2 --- CMakeLists.txt | 1 + README.md | 5 + src/compositor_ng.c | 12 ++ src/compositor_ng.h | 5 + src/flutter-pi.c | 5 + src/flutter-pi.h | 2 + src/modesetting.c | 102 ++++++++++++- src/modesetting.h | 13 +- src/plugins/dpms_plugin.c | 25 ++++ src/plugins/dpms_plugin.h | 17 +++ src/window.c | 99 ++++++++++++- src/window.h | 9 ++ test/CMakeLists.txt | 23 ++- test/dpms_test.c | 296 ++++++++++++++++++++++++++++++++++++++ 14 files changed, 603 insertions(+), 11 deletions(-) create mode 100644 src/plugins/dpms_plugin.c create mode 100644 src/plugins/dpms_plugin.h create mode 100644 test/dpms_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 734aba515..bd842c5b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,7 @@ add_library( src/window.c src/dummy_render_surface.c src/plugins/services.c + src/plugins/dpms_plugin.c ) target_link_libraries(flutterpi_module PUBLIC diff --git a/README.md b/README.md index 220eecf2f..a03b2472a 100644 --- a/README.md +++ b/README.md @@ -432,6 +432,11 @@ There are several things you need to keep in mind: - One of the common reasons is outdated ALSA config in which case you should delete existing config and replace it with up to date one - Finally, if you want to verify your audio setup is good, you can use `gst-launch` command to invoke `playbin` on audio file directly. +### Display power management (DPMS) + +The `dpms` Dart package can turn the selected KMS display off and back on. +See [DPMS usage, implementation and hardware checks](docs/DPMS.md). + ## 📊 Performance ### Graphics Performance Graphics performance is actually pretty good. With most of the apps inside the `flutter SDK -> examples -> catalog` directory I get smooth 50-60fps on the Pi 4 2GB and Pi 3 A+. diff --git a/src/compositor_ng.c b/src/compositor_ng.c index d6f6718b4..eae938ae0 100644 --- a/src/compositor_ng.c +++ b/src/compositor_ng.c @@ -581,3 +581,15 @@ void compositor_set_cursor( compositor_unlock(compositor); } + +int compositor_get_dpms(struct compositor *compositor) { + return window_get_dpms(compositor->main_window); +} + +int compositor_set_dpms(struct compositor *compositor, bool enabled) { + return window_set_dpms(compositor->main_window, enabled); +} + +bool compositor_supports_dpms(struct compositor *compositor) { + return window_supports_dpms(compositor->main_window); +} diff --git a/src/compositor_ng.h b/src/compositor_ng.h index d73609648..ce9036b3d 100644 --- a/src/compositor_ng.h +++ b/src/compositor_ng.h @@ -203,4 +203,9 @@ DECLARE_REF_OPS(fl_layer_composition) size_t fl_layer_composition_get_n_layers(struct fl_layer_composition *composition); struct fl_layer *fl_layer_composition_peek_layer(struct fl_layer_composition *composition, int layer); +/** DPMS applies to the output selected by the main window. */ +bool compositor_supports_dpms(struct compositor *compositor); +int compositor_get_dpms(struct compositor *compositor); +int compositor_set_dpms(struct compositor *compositor, bool enabled); + #endif // _FLUTTERPI_SRC_COMPOSITOR_NG_H diff --git a/src/flutter-pi.c b/src/flutter-pi.c index e1246b705..45d6607e8 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -960,6 +960,11 @@ int flutterpi_respond_to_platform_message( return 0; } +struct compositor *flutterpi_get_compositor(struct flutterpi *flutterpi) { + ASSERT_NOT_NULL(flutterpi); + return flutterpi->compositor; +} + struct texture_registry *flutterpi_get_texture_registry(struct flutterpi *flutterpi) { ASSERT_NOT_NULL(flutterpi); ASSERT_NOT_NULL(flutterpi->texture_registry); diff --git a/src/flutter-pi.h b/src/flutter-pi.h index c264493a8..75c7a4151 100644 --- a/src/flutter-pi.h +++ b/src/flutter-pi.h @@ -177,6 +177,8 @@ struct texture_registry *flutterpi_get_texture_registry(struct flutterpi *flutte struct plugin_registry *flutterpi_get_plugin_registry(struct flutterpi *flutterpi); +struct compositor *flutterpi_get_compositor(struct flutterpi *flutterpi); + FlutterPlatformMessageResponseHandle * flutterpi_create_platform_message_response_handle(struct flutterpi *flutterpi, FlutterDataCallback data_callback, void *userdata); diff --git a/src/modesetting.c b/src/modesetting.c index 8ad4a24de..85758a34d 100644 --- a/src/modesetting.c +++ b/src/modesetting.c @@ -79,6 +79,8 @@ struct kms_req_builder { int n_layers; struct kms_req_layer layers[32]; + bool is_dpms_req; + bool dpms_on; bool unset_mode; bool has_mode; drmModeModeInfo mode; @@ -423,6 +425,12 @@ static int fetch_crtc(int drm_fd, int crtc_index, uint32_t crtc_id, struct drm_c crtc_out->index = crtc_index; crtc_out->bitmask = 1u << crtc_index; crtc_out->ids = ids; + crtc_out->committed_state.active = crtc->mode_valid; + for (uint32_t i = 0; i < props->count_props; i++) { + if (props->props[i] == ids.active) { + crtc_out->committed_state.active = props->prop_values[i] != 0; + } + } crtc_out->committed_state.has_mode = crtc->mode_valid; crtc_out->committed_state.mode = crtc->mode; crtc_out->committed_state.mode_blob = NULL; @@ -1235,6 +1243,10 @@ int drmdev_get_event_fd(struct drmdev *drmdev) { return drmdev->master_fd; } +bool drmdev_supports_dpms(struct drmdev *drmdev, const struct drm_crtc *crtc, const struct drm_connector *connector) { + return drmdev->supports_atomic_modesetting ? crtc->ids.active != 0 : connector->ids.dpms != 0; +} + bool drmdev_supports_dumb_buffers(struct drmdev *drmdev) { return drmdev->supports_dumb_buffers; } @@ -2273,6 +2285,7 @@ struct kms_req_builder *drmdev_create_request_builder(struct drmdev *drmdev, uin if (crtc == NULL) { LOG_ERROR("Invalid CRTC id: %" PRId32 "\n", crtc_id); + errno = EINVAL; goto fail_unlock; } @@ -2288,9 +2301,6 @@ struct kms_req_builder *drmdev_create_request_builder(struct drmdev *drmdev, uin if (req == NULL) { goto fail_free_builder; } - - // set the CRTC to active - drmModeAtomicAddProperty(req, crtc->id, crtc->ids.active, 1); } else { req = NULL; } @@ -2322,6 +2332,8 @@ struct kms_req_builder *drmdev_create_request_builder(struct drmdev *drmdev, uin builder->n_layers = 0; builder->has_mode = false; builder->unset_mode = false; + builder->is_dpms_req = false; + builder->dpms_on = true; return builder; fail_free_builder: @@ -2332,6 +2344,33 @@ struct kms_req_builder *drmdev_create_request_builder(struct drmdev *drmdev, uin return NULL; } +struct kms_req *drmdev_create_dpms_req(struct drmdev *drmdev, uint32_t crtc_id, uint32_t connector_id, bool enabled) { + struct kms_req_builder *builder; + struct kms_req *req; + int ok; + + builder = drmdev_create_request_builder(drmdev, crtc_id); + if (builder == NULL) { + return NULL; + } + + ok = kms_req_builder_set_connector(builder, connector_id); + if (ok == 0 && (builder->use_legacy ? builder->connector->ids.dpms == 0 : builder->crtc->ids.active == 0)) { + ok = ENOTSUP; + } + if (ok != 0) { + kms_req_builder_unref(builder); + errno = ok; + return NULL; + } + + builder->is_dpms_req = true; + builder->dpms_on = enabled; + req = kms_req_builder_build(builder); + kms_req_builder_unref(builder); + return req; +} + static void kms_req_builder_destroy(struct kms_req_builder *builder) { /// TODO: Is this complete? for (int i = 0; i < builder->n_layers; i++) { @@ -2784,6 +2823,40 @@ kms_req_commit_common(struct kms_req *req, bool blocking, kms_scanout_cb_t scano goto fail_unlock; } + if (builder->is_dpms_req) { + // A disabled CRTC produces no page-flip event. Do not enter the normal + // scanout path or replace last_flipped: its buffers remain attached. + if (!blocking) { + ok = ENOTSUP; + goto fail_unlock; + } + if (builder->use_legacy) { + ok = drmModeConnectorSetProperty( + builder->drmdev->master_fd, + builder->connector->id, + builder->connector->ids.dpms, + builder->dpms_on ? DRM_MODE_DPMS_ON : DRM_MODE_DPMS_OFF + ); + } else { + ok = drmModeAtomicAddProperty(builder->req, builder->crtc->id, builder->crtc->ids.active, builder->dpms_on); + if (ok < 0) { + ok = -ok; + goto fail_unlock; + } + ok = drmModeAtomicCommit(builder->drmdev->master_fd, builder->req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL); + } + if (ok != 0) { + ok = errno; + goto fail_unlock; + } + builder->crtc->committed_state.active = builder->dpms_on; + if (scanout_cb != NULL) { + scanout_cb(builder->drmdev, 0, userdata); + } + drmdev_unlock(builder->drmdev); + return 0; + } + // only change the mode if the new mode differs from the old one /// TOOD: If this is not a standard mode reported by connector/CRTC, @@ -2816,6 +2889,18 @@ kms_req_commit_common(struct kms_req *req, bool blocking, kms_scanout_cb_t scano } if (builder->use_legacy) { + if (!builder->crtc->committed_state.active && builder->connector != NULL && builder->connector->ids.dpms != 0) { + ok = drmModeConnectorSetProperty( + builder->drmdev->master_fd, + builder->connector->id, + builder->connector->ids.dpms, + DRM_MODE_DPMS_ON + ); + if (ok != 0) { + ok = errno; + goto fail_maybe_destroy_mode_blob; + } + } ASSERT_EQUALS(builder->layers[0].layer.dst_x, 0); ASSERT_EQUALS(builder->layers[0].layer.dst_y, 0); ASSERT_EQUALS(builder->layers[0].layer.dst_w, builder->mode.hdisplay); @@ -2915,9 +3000,17 @@ kms_req_commit_common(struct kms_req *req, bool blocking, kms_scanout_cb_t scano /// TODO: Call drmModeSetPlane for all other layers /// TODO: Assert here } else { + // Every frame request implicitly enables the display. Power-only + // requests take the separate path above and never add framebuffer layers. + ok = drmModeAtomicAddProperty(builder->req, builder->crtc->id, builder->crtc->ids.active, !builder->unset_mode); + if (ok < 0) { + ok = -ok; + goto fail_maybe_destroy_mode_blob; + } /// TODO: If we can do explicit fencing, don't use the page flip event. /// TODO: Can we set OUT_FENCE_PTR even though we didn't set any IN_FENCE_FDs? - flags = DRM_MODE_PAGE_FLIP_EVENT | (blocking ? 0 : DRM_MODE_ATOMIC_NONBLOCK) | (update_mode ? DRM_MODE_ATOMIC_ALLOW_MODESET : 0); + flags = DRM_MODE_PAGE_FLIP_EVENT | (blocking ? 0 : DRM_MODE_ATOMIC_NONBLOCK) | + ((update_mode || !builder->crtc->committed_state.active) ? DRM_MODE_ATOMIC_ALLOW_MODESET : 0); // All planes that are not used by us and are connected to our CRTC // should be disabled. @@ -2988,6 +3081,7 @@ kms_req_commit_common(struct kms_req *req, bool blocking, kms_scanout_cb_t scano } // update struct drm_crtc.committed_state + builder->crtc->committed_state.active = !builder->unset_mode; if (update_mode) { // destroy the old mode blob if (builder->crtc->committed_state.mode_blob != NULL) { diff --git a/src/modesetting.h b/src/modesetting.h index 4afda0bd0..b34769323 100644 --- a/src/modesetting.h +++ b/src/modesetting.h @@ -394,6 +394,7 @@ struct drm_crtc { struct drm_crtc_prop_ids ids; struct { + bool active; bool has_mode; drmModeModeInfo mode; struct drm_mode_blob *mode_blob; @@ -635,6 +636,7 @@ struct _drmModeModeInfo; int drmdev_get_fd(struct drmdev *drmdev); int drmdev_get_event_fd(struct drmdev *drmdev); bool drmdev_supports_dumb_buffers(struct drmdev *drmdev); +bool drmdev_supports_dpms(struct drmdev *drmdev, const struct drm_crtc *crtc, const struct drm_connector *connector); int drmdev_create_dumb_buffer( struct drmdev *drmdev, int width, @@ -752,6 +754,13 @@ struct kms_req_builder; struct kms_req_builder *drmdev_create_request_builder(struct drmdev *drmdev, uint32_t crtc_id); +/** + * Create a power-only request for the selected output. Keeps its mode, planes and + * framebuffer references intact. Commit with kms_req_commit_blocking; there is + * no page-flip event and its returned vblank timestamp is zero. + */ +struct kms_req *drmdev_create_dpms_req(struct drmdev *drmdev, uint32_t crtc_id, uint32_t connector_id, bool enabled); + DECLARE_REF_OPS(kms_req_builder); /** @@ -901,8 +910,8 @@ int kms_req_builder_push_zpos_placeholder_layer(struct kms_req_builder *builder, * @brief A KMS request (atomic or legacy modesetting) that can be committed to * change the state of a single CRTC. * - * Only way to construct this is by building a KMS request using - * @ref kms_req_builder and then calling @ref kms_req_builder_build. + * Construct a frame request using @ref kms_req_builder_build, or a power-only + * request using @ref drmdev_create_dpms_req. */ struct kms_req; diff --git a/src/plugins/dpms_plugin.c b/src/plugins/dpms_plugin.c new file mode 100644 index 000000000..cfc0f1283 --- /dev/null +++ b/src/plugins/dpms_plugin.c @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +#include "plugins/dpms_plugin.h" + +#include + +#include "compositor_ng.h" +#include "flutter-pi.h" + +int32_t flutterpi_dpms_is_available(void) { + return flutterpi != NULL && compositor_supports_dpms(flutterpi_get_compositor(flutterpi)); +} + +int32_t flutterpi_dpms_get(void) { + if (flutterpi == NULL) { + return -ENODEV; + } + return compositor_get_dpms(flutterpi_get_compositor(flutterpi)); +} + +int32_t flutterpi_dpms_set(bool enabled) { + if (flutterpi == NULL) { + return ENODEV; + } + return compositor_set_dpms(flutterpi_get_compositor(flutterpi), enabled); +} diff --git a/src/plugins/dpms_plugin.h b/src/plugins/dpms_plugin.h new file mode 100644 index 000000000..65269c288 --- /dev/null +++ b/src/plugins/dpms_plugin.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +#ifndef _FLUTTERPI_PLUGINS_DPMS_PLUGIN_H +#define _FLUTTERPI_PLUGINS_DPMS_PLUGIN_H + +#include +#include + +/** FFI ABI: 0 unavailable, 1 available. */ +int32_t flutterpi_dpms_is_available(void); + +/** FFI ABI: 0 off, 1 on, negative errno on error. */ +int32_t flutterpi_dpms_get(void); + +/** FFI ABI: 0 success, positive errno on error. May block for a modeset. */ +int32_t flutterpi_dpms_set(bool enabled); + +#endif // _FLUTTERPI_PLUGINS_DPMS_PLUGIN_H diff --git a/src/window.c b/src/window.c index 1dec153af..db9dec465 100644 --- a/src/window.c +++ b/src/window.c @@ -156,6 +156,8 @@ struct window { */ struct fl_layer_composition *composition; + bool active; + /** * @brief KMS-specific fields if this is a KMS window. * @@ -226,6 +228,7 @@ struct window { */ struct vec2i cursor_pos; + int (*set_dpms_locked)(struct window *window, bool enabled); int (*push_composition)(struct window *window, struct fl_layer_composition *composition); struct render_surface *(*get_render_surface)(struct window *window, struct vec2i size); @@ -407,6 +410,8 @@ static int window_init( window->has_forced_pixel_format = has_forced_pixel_format; window->forced_pixel_format = forced_pixel_format; window->composition = NULL; + window->active = true; + window->set_dpms_locked = NULL; window->renderer_type = kOpenGL_RendererType; window->gl_renderer = NULL; window->vk_renderer = NULL; @@ -450,6 +455,37 @@ int window_push_composition(struct window *window, struct fl_layer_composition * return window->push_composition(window, composition); } +bool window_supports_dpms(struct window *window) { + ASSERT_NOT_NULL(window); + return window->set_dpms_locked != NULL; +} + +int window_get_dpms(struct window *window) { + int mode; + + ASSERT_NOT_NULL(window); + window_lock(window); + mode = window_supports_dpms(window) ? window->active : -ENOTSUP; + window_unlock(window); + return mode; +} + +int window_set_dpms(struct window *window, bool enabled) { + int ok; + + ASSERT_NOT_NULL(window); + window_lock(window); + if (!window_supports_dpms(window)) { + ok = ENOTSUP; + } else if (window->active == enabled) { + ok = 0; + } else { + ok = window->set_dpms_locked(window, enabled); + } + window_unlock(window); + return ok; +} + struct view_geometry window_get_view_geometry(struct window *window) { ASSERT_NOT_NULL(window); @@ -874,6 +910,7 @@ static int select_mode( return 0; } +static int kms_window_set_dpms_locked(struct window *window, bool enabled); static int kms_window_push_composition(struct window *window, struct fl_layer_composition *composition); static struct render_surface *kms_window_get_render_surface(struct window *window, struct vec2i size); @@ -1022,6 +1059,9 @@ MUST_CHECK struct window *kms_window_new( } else { window->vk_renderer = NULL; } + if (drmdev_supports_dpms(drmdev, selected_crtc, selected_connector)) { + window->set_dpms_locked = kms_window_set_dpms_locked; + } window->push_composition = kms_window_push_composition; window->get_render_surface = kms_window_get_render_surface; #ifdef HAVE_EGL_GLES2 @@ -1133,7 +1173,7 @@ static void on_cancel_frame(void *userdata) { free(frame); } -static int kms_window_push_composition_locked(struct window *window, struct fl_layer_composition *composition) { +static int kms_window_present_composition_locked(struct window *window, struct fl_layer_composition *composition, bool synchronous) { struct kms_req_builder *builder; struct kms_req *req; struct frame *frame; @@ -1159,11 +1199,13 @@ static int kms_window_push_composition_locked(struct window *window, struct fl_l /// TODO: If we don't have new revisions, we don't need to scanout anything. fl_layer_composition_swap_ptrs(&window->composition, composition); + if (!window->active) { + return 0; + } builder = drmdev_create_request_builder(window->kms.drmdev, window->kms.crtc->id); if (builder == NULL) { - ok = ENOMEM; - goto fail_unref_builder; + return ENOMEM; } // We only set the mode once, at the first atomic request. @@ -1235,14 +1277,22 @@ static int kms_window_push_composition_locked(struct window *window, struct fl_l req = kms_req_builder_build(builder); if (req == NULL) { + ok = ENOMEM; goto fail_unref_builder; } kms_req_builder_unref(builder); builder = NULL; + if (synchronous) { + ok = kms_req_commit_blocking(req, NULL); + kms_req_unref(req); + return ok; + } + frame = malloc(sizeof *frame); if (frame == NULL) { + ok = ENOMEM; goto fail_unref_req; } @@ -1315,6 +1365,47 @@ static int kms_window_push_composition_locked(struct window *window, struct fl_l return ok; } +static int kms_window_push_composition_locked(struct window *window, struct fl_layer_composition *composition) { + return kms_window_present_composition_locked(window, composition, false); +} + +static int kms_window_commit_dpms_locked(struct window *window, bool enabled) { + struct kms_req *req = drmdev_create_dpms_req(window->kms.drmdev, window->kms.crtc->id, window->kms.connector->id, enabled); + if (req == NULL) { + return errno; + } + int ok = kms_req_commit_blocking(req, NULL); + kms_req_unref(req); + return ok; +} + +static int kms_window_set_dpms_locked(struct window *window, bool enabled) { + int ok; + + if (enabled && window->composition != NULL) { + // Normal frame requests implicitly enable the CRTC. Replay the newest + // composition, including updates received while the screen was off. + window->active = true; + ok = kms_window_present_composition_locked(window, window->composition, true); + if (ok != 0) { + window->active = false; + // Legacy modesetting (or a failed event read after a successful + // atomic commit) may already have enabled the output. Restore off. + int rollback = kms_window_commit_dpms_locked(window, false); + if (rollback != 0) { + LOG_ERROR("Could not restore DPMS off after failed frame replay: %s\n", strerror(rollback)); + } + } + return ok; + } + + ok = kms_window_commit_dpms_locked(window, enabled); + if (ok == 0) { + window->active = enabled; + } + return ok; +} + static int kms_window_push_composition(struct window *window, struct fl_layer_composition *composition) { int ok; @@ -1596,7 +1687,7 @@ static int kms_window_set_cursor_locked( // cursor movements in the same vsync interval we couldn't show on screen anymore. // // Mutter (GNOME's compositor) has an extra KMS cursor thread with high priority for that purpose. - if (window->kms.has_cursor_plane) { + if (window->active && window->kms.has_cursor_plane) { drmdev_move_cursor(window->kms.drmdev, window->kms.crtc->id, vec2i_sub(pos, window->kms.cursor->hotspot)); } else { // for now just don't do anything. diff --git a/src/window.h b/src/window.h index 342f2ecd3..b6d505155 100644 --- a/src/window.h +++ b/src/window.h @@ -109,6 +109,15 @@ MUST_CHECK struct window *dummy_window_new( */ int window_push_composition(struct window *window, struct fl_layer_composition *composition); +/** Whether the selected window supports display power management. */ +bool window_supports_dpms(struct window *window); + +/** Last successfully requested mode: 0 off, 1 on, or a negative errno. */ +int window_get_dpms(struct window *window); + +/** Set display power; returns zero on success, otherwise a positive errno. */ +int window_set_dpms(struct window *window, bool enabled); + /** * @brief Get the current view geometry of this window. * diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 687746d24..a59a5089c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,4 +20,25 @@ target_link_libraries( Unity ) -add_test(flutterpi_test flutterpi_test) \ No newline at end of file +add_test(flutterpi_test flutterpi_test) + +# dpms_test includes modesetting.c/window.c to exercise their private state with +# mock DRM ioctls; compile the remaining module sources with the same settings. +get_target_property(dpms_sources flutterpi_module SOURCES) +list(FILTER dpms_sources EXCLUDE REGEX "src/(modesetting|window)\\.c$") +list(TRANSFORM dpms_sources PREPEND "${PROJECT_SOURCE_DIR}/") +add_executable(dpms_test dpms_test.c ${dpms_sources}) +target_include_directories(dpms_test PRIVATE $) +target_compile_options(dpms_test PRIVATE $) +target_compile_definitions(dpms_test PRIVATE $) +target_link_libraries(dpms_test PRIVATE $ Unity) +target_link_options(dpms_test PRIVATE + $ + -Wl,--wrap=drmModeMoveCursor + -Wl,--wrap=drmAuthMagic + -Wl,--wrap=drmModeAtomicAddProperty + -Wl,--wrap=drmModeAtomicCommit + -Wl,--wrap=drmModeConnectorSetProperty + -Wl,--wrap=drmHandleEvent +) +add_test(dpms_test dpms_test) diff --git a/test/dpms_test.c b/test/dpms_test.c new file mode 100644 index 000000000..b717aeb1d --- /dev/null +++ b/test/dpms_test.c @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: MIT +#define _GNU_SOURCE + +// Exercise private KMS/window state against mocked DRM ioctls. The other +// flutterpi_module sources are linked normally by this test's CMake target. +#include + +#include "../src/modesetting.c" +#include "../src/window.c" +#include "plugins/dpms_plugin.h" + +static struct drmdev device; +static struct drm_crtc crtc; +static struct drm_connector connector; +static struct window output; +static drmModeModeInfo mode; +static int commit_error, property_error; +static int atomic_commits, legacy_commits, event_reads, cursor_moves; +static uint32_t last_flags; +static uint64_t last_active, last_legacy_dpms; +static void *pending_flip; + +int __real_drmModeAtomicAddProperty(drmModeAtomicReq *req, uint32_t object, uint32_t property, uint64_t value); + +int __wrap_drmModeAtomicAddProperty(drmModeAtomicReq *req, uint32_t object, uint32_t property, uint64_t value) { + if (property_error != 0) { + return -property_error; + } + TEST_ASSERT_NOT_EQUAL(connector.ids.dpms, property); + if (object == crtc.id && property == crtc.ids.active) { + last_active = value; + } + return __real_drmModeAtomicAddProperty(req, object, property, value); +} + +int __wrap_drmAuthMagic(int fd, drm_magic_t magic) { + (void) fd; + (void) magic; + return 0; +} + +int __wrap_drmModeAtomicCommit(int fd, const drmModeAtomicReq *req, uint32_t flags, void *userdata) { + (void) fd; + (void) req; + atomic_commits++; + last_flags = flags; + if (commit_error != 0) { + errno = commit_error; + return -1; + } + if (flags & DRM_MODE_PAGE_FLIP_EVENT) { + pending_flip = userdata; + } + return 0; +} + +int __wrap_drmModeConnectorSetProperty(int fd, uint32_t connector_id, uint32_t property, uint64_t value) { + (void) fd; + TEST_ASSERT_EQUAL_UINT32(connector.id, connector_id); + TEST_ASSERT_EQUAL_UINT32(connector.ids.dpms, property); + legacy_commits++; + if (commit_error != 0) { + errno = commit_error; + return -1; + } + last_legacy_dpms = value; + return 0; +} + +int __wrap_drmModeMoveCursor(int fd, uint32_t crtc_id, int x, int y) { + (void) fd; + (void) crtc_id; + (void) x; + (void) y; + cursor_moves++; + return 0; +} + +int __wrap_drmHandleEvent(int fd, drmEventContext *context) { + void *userdata = pending_flip; + event_reads++; + // No event is produced by a power-only commit. Catch an accidental read + // here instead of hanging the test as it would hang on an actual device. + if (userdata == NULL) { + return -1; + } + pending_flip = NULL; + context->page_flip_handler2(fd, 1, 1, 0, crtc.id, userdata); + return 0; +} + +void setUp(void) { + memset(&device, 0, sizeof device); + memset(&crtc, 0, sizeof crtc); + memset(&connector, 0, sizeof connector); + memset(&output, 0, sizeof output); + memset(&mode, 0, sizeof mode); + pthread_mutex_init(&device.mutex, NULL); + pthread_mutex_init(&output.lock, NULL); + device.n_refs = REFCOUNT_INIT_1; + device.fd = 42; + device.master_fd = 42; + device.supports_atomic_modesetting = true; + device.n_crtcs = 1; + device.crtcs = &crtc; + device.n_connectors = 1; + device.connectors = &connector; + crtc.id = 12; + crtc.bitmask = 1; + crtc.ids.active = 101; + crtc.ids.mode_id = 102; + crtc.committed_state.active = true; + crtc.committed_state.has_mode = true; + connector.id = 15; + connector.ids.crtc_id = 103; + connector.ids.dpms = 104; + output.active = true; + output.set_dpms_locked = kms_window_set_dpms_locked; + output.kms.drmdev = &device; + output.kms.connector = &connector; + output.kms.crtc = &crtc; + output.kms.mode = &mode; + output.kms.should_apply_mode = true; + atomic_commits = legacy_commits = event_reads = cursor_moves = 0; + commit_error = property_error = 0; + pending_flip = NULL; + last_flags = 0; + last_active = last_legacy_dpms = UINT64_MAX; +} + +void tearDown(void) { + if (output.composition != NULL) { + fl_layer_composition_unref(output.composition); + } + if (device.per_crtc_state[0].last_flipped != NULL) { + kms_req_unref(device.per_crtc_state[0].last_flipped); + } + TEST_ASSERT_EQUAL_INT(1, refcount_get_for_debug(&device.n_refs)); + pthread_mutex_destroy(&output.lock); + pthread_mutex_destroy(&device.mutex); +} + +static void test_power_commit_keeps_scanout_buffers_and_has_no_flip_event(void) { + struct kms_req_builder *builder = drmdev_create_request_builder(&device, crtc.id); + struct kms_req *previous = kms_req_builder_build(builder); + kms_req_builder_unref(builder); + device.per_crtc_state[0].last_flipped = previous; + + struct kms_req *req = drmdev_create_dpms_req(&device, crtc.id, connector.id, false); + TEST_ASSERT_NOT_NULL(req); + uint64_t timestamp = UINT64_MAX; + TEST_ASSERT_EQUAL_INT(0, kms_req_commit_blocking(req, ×tamp)); + TEST_ASSERT_EQUAL_UINT64(0, timestamp); + TEST_ASSERT_EQUAL_UINT64(0, last_active); + TEST_ASSERT_EQUAL_UINT32(DRM_MODE_ATOMIC_ALLOW_MODESET, last_flags); + TEST_ASSERT_EQUAL_INT(0, event_reads); + TEST_ASSERT_EQUAL_PTR(previous, device.per_crtc_state[0].last_flipped); + TEST_ASSERT_TRUE(crtc.committed_state.has_mode); + TEST_ASSERT_FALSE(crtc.committed_state.active); + kms_req_unref(req); +} + +static void test_off_buffers_newest_composition_and_on_replays_it(void) { + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + struct fl_layer_composition *first = fl_layer_composition_new(0); + struct fl_layer_composition *latest = fl_layer_composition_new(0); + TEST_ASSERT_EQUAL_INT(0, kms_window_push_composition(&output, first)); + TEST_ASSERT_EQUAL_INT(0, kms_window_push_composition(&output, latest)); + TEST_ASSERT_EQUAL_INT(1, atomic_commits); + TEST_ASSERT_EQUAL_PTR(latest, output.composition); + TEST_ASSERT_EQUAL_INT(1, refcount_get_for_debug(&first->n_refs)); + TEST_ASSERT_EQUAL_INT(2, refcount_get_for_debug(&latest->n_refs)); + TEST_ASSERT_EQUAL_INT(0, window_get_dpms(&output)); + + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, true)); + TEST_ASSERT_EQUAL_INT(1, window_get_dpms(&output)); + TEST_ASSERT_EQUAL_INT(2, atomic_commits); + TEST_ASSERT_EQUAL_UINT64(1, last_active); + TEST_ASSERT_TRUE(last_flags & DRM_MODE_ATOMIC_ALLOW_MODESET); + TEST_ASSERT_EQUAL_INT(1, event_reads); + TEST_ASSERT_EQUAL_PTR(latest, output.composition); + fl_layer_composition_unref(first); + fl_layer_composition_unref(latest); +} + +static void test_failed_disable_preserves_mode(void) { + commit_error = EIO; + TEST_ASSERT_EQUAL_INT(EIO, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_INT(1, window_get_dpms(&output)); + TEST_ASSERT_TRUE(crtc.committed_state.active); + TEST_ASSERT_EQUAL_INT(0, event_reads); +} + +static void test_failed_replay_stays_off_and_can_be_retried(void) { + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + struct fl_layer_composition *composition = fl_layer_composition_new(0); + TEST_ASSERT_EQUAL_INT(0, kms_window_push_composition(&output, composition)); + commit_error = EIO; + TEST_ASSERT_EQUAL_INT(EIO, window_set_dpms(&output, true)); + TEST_ASSERT_EQUAL_INT(0, window_get_dpms(&output)); + TEST_ASSERT_FALSE(crtc.committed_state.active); + commit_error = 0; + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, true)); + TEST_ASSERT_EQUAL_INT(1, window_get_dpms(&output)); + fl_layer_composition_unref(composition); +} + +static void test_legacy_power_uses_selected_connector(void) { + device.supports_atomic_modesetting = false; + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_UINT64(DRM_MODE_DPMS_OFF, last_legacy_dpms); + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, true)); + TEST_ASSERT_EQUAL_UINT64(DRM_MODE_DPMS_ON, last_legacy_dpms); + TEST_ASSERT_EQUAL_INT(2, legacy_commits); + TEST_ASSERT_EQUAL_INT(0, atomic_commits); + TEST_ASSERT_EQUAL_INT(0, event_reads); +} + +static void test_repeated_modes_and_toggle_before_first_frame(void) { + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, true)); + TEST_ASSERT_EQUAL_INT(0, atomic_commits); + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_INT(1, atomic_commits); + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, true)); + TEST_ASSERT_EQUAL_INT(2, atomic_commits); + TEST_ASSERT_EQUAL_UINT64(1, last_active); + TEST_ASSERT_EQUAL_INT(0, event_reads); +} + +static void test_dummy_and_missing_properties_are_unsupported(void) { + output.set_dpms_locked = NULL; + TEST_ASSERT_FALSE(window_supports_dpms(&output)); + TEST_ASSERT_EQUAL_INT(-ENOTSUP, window_get_dpms(&output)); + TEST_ASSERT_EQUAL_INT(ENOTSUP, window_set_dpms(&output, false)); + crtc.ids.active = 0; + TEST_ASSERT_FALSE(drmdev_supports_dpms(&device, &crtc, &connector)); + errno = 0; + TEST_ASSERT_NULL(drmdev_create_dpms_req(&device, crtc.id, connector.id, false)); + TEST_ASSERT_EQUAL_INT(ENOTSUP, errno); +} + +static void test_lost_drm_master_propagates_error_without_changing_mode(void) { + device.master_fd = -1; + TEST_ASSERT_EQUAL_INT(EBUSY, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_INT(1, window_get_dpms(&output)); + TEST_ASSERT_EQUAL_INT(0, atomic_commits); +} + +static void test_property_allocation_failure_propagates_errno(void) { + property_error = ENOMEM; + errno = 0; + TEST_ASSERT_EQUAL_INT(ENOMEM, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_INT(1, window_get_dpms(&output)); + TEST_ASSERT_EQUAL_INT(0, atomic_commits); +} + +static void test_ffi_without_an_embedder_returns_errors(void) { + TEST_ASSERT_NULL(flutterpi); + TEST_ASSERT_EQUAL_INT(0, flutterpi_dpms_is_available()); + TEST_ASSERT_EQUAL_INT(-ENODEV, flutterpi_dpms_get()); + TEST_ASSERT_EQUAL_INT(ENODEV, flutterpi_dpms_set(false)); +} + +static void test_cursor_movement_while_off_does_not_touch_hardware(void) { + struct cursor_buffer cursor = { 0 }; + output.set_cursor_locked = kms_window_set_cursor_locked; + output.cursor_enabled = true; + output.kms.has_cursor_plane = true; + output.kms.pointer_icon = pointer_icon_for_details(POINTER_KIND_BASIC, 1.0); + cursor.icon = output.kms.pointer_icon; + output.kms.cursor = &cursor; + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_INT(0, window_set_cursor(&output, false, false, false, POINTER_KIND_NONE, true, VEC2I(30, 40))); + TEST_ASSERT_EQUAL_INT(30, output.cursor_pos.x); + TEST_ASSERT_EQUAL_INT(40, output.cursor_pos.y); + TEST_ASSERT_EQUAL_INT(0, cursor_moves); + TEST_ASSERT_EQUAL_INT(1, atomic_commits); + output.kms.cursor = NULL; +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_power_commit_keeps_scanout_buffers_and_has_no_flip_event); + RUN_TEST(test_off_buffers_newest_composition_and_on_replays_it); + RUN_TEST(test_failed_disable_preserves_mode); + RUN_TEST(test_failed_replay_stays_off_and_can_be_retried); + RUN_TEST(test_legacy_power_uses_selected_connector); + RUN_TEST(test_repeated_modes_and_toggle_before_first_frame); + RUN_TEST(test_dummy_and_missing_properties_are_unsupported); + RUN_TEST(test_lost_drm_master_propagates_error_without_changing_mode); + RUN_TEST(test_property_allocation_failure_propagates_errno); + RUN_TEST(test_ffi_without_an_embedder_returns_errors); + RUN_TEST(test_cursor_movement_while_off_does_not_touch_hardware); + return UNITY_END(); +}