diff --git a/CMakeLists.txt b/CMakeLists.txt index 734aba51..bd842c5b 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 220eecf2..a03b2472 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 d6f6718b..eae938ae 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 d7360964..ce9036b3 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 e1246b70..45d6607e 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 c264493a..75c7a415 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 4d3dc9cf..16d42a2a 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; @@ -425,6 +427,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; @@ -1237,6 +1245,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; } @@ -2307,6 +2319,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; } @@ -2322,9 +2335,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; } @@ -2356,6 +2366,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: @@ -2366,6 +2378,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++) { @@ -2804,10 +2843,6 @@ kms_req_commit_locked(struct kms_req *req, bool blocking, kms_scanout_cb_t scano ASSERT_NOT_NULL(req); builder = (struct kms_req_builder *) req; - if (builder->drmdev->per_crtc_state[builder->crtc->index].flip_pending) { - return EBUSY; - } - if (builder->drmdev->master_fd < 0) { LOG_ERROR("Commit requested, but drmdev doesn't have a DRM master fd right now.\n"); ok = EBUSY; @@ -2820,6 +2855,55 @@ kms_req_commit_locked(struct kms_req *req, bool blocking, kms_scanout_cb_t scano goto fail_unlock; } + if (builder->is_dpms_req && !blocking) { + return ENOTSUP; + } + + if (blocking) { + // A blocking update supersedes the queued frame. Drop it before + // draining so the flip handler cannot submit it and enable the CRTC + // after a power change. + kms_req_swap_ptrs(&builder->drmdev->per_crtc_state[builder->crtc->index].queued, NULL); + while (builder->drmdev->per_crtc_state[builder->crtc->index].flip_pending) { + ok = drmdev_on_modesetting_fd_ready_locked(builder->drmdev); + if (ok != 0) { + LOG_ERROR("Could not drain page flips before blocking commit.\n"); + goto fail_unlock; + } + } + } else if (builder->drmdev->per_crtc_state[builder->crtc->index].flip_pending) { + return EBUSY; + } + + 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 (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); + } + 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, @@ -2852,6 +2936,18 @@ kms_req_commit_locked(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); @@ -2951,9 +3047,17 @@ kms_req_commit_locked(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. @@ -3024,6 +3128,7 @@ kms_req_commit_locked(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 ca49dea3..338cd77c 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; @@ -920,6 +929,11 @@ DECLARE_REF_OPS(kms_req); */ struct kms_req *kms_req_builder_build(struct kms_req_builder *builder); +/** + * Discard the queued frame for this CRTC and wait for its outstanding flip + * before committing the request. Frame requests also wait for their own flip; + * power-only requests produce no flip and return a zero vblank timestamp. + */ int kms_req_commit_blocking(struct kms_req *req, uint64_t *vblank_ns_out); int kms_req_commit_nonblocking(struct kms_req *req, kms_scanout_cb_t scanout_cb, void *userdata, void_callback_t destroy_cb); diff --git a/src/plugins/dpms_plugin.c b/src/plugins/dpms_plugin.c new file mode 100644 index 00000000..cfc0f128 --- /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 00000000..65269c28 --- /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 bd9185b0..4b1bc834 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 342f2ecd..b6d50515 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 1dfb1988..963b8bf9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -33,3 +33,26 @@ target_compile_options(kms_present_test PRIVATE -UNDEBUG -ffunction-sections -fd target_link_options(kms_present_test PRIVATE -Wl,--gc-sections) target_link_libraries(kms_present_test PRIVATE PkgConfig::DRM PkgConfig::GBM Threads::Threads m) add_test(kms_present_test kms_present_test) +set_tests_properties(kms_present_test PROPERTIES TIMEOUT 30) + +# 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) +set_tests_properties(dpms_test PROPERTIES TIMEOUT 30) diff --git a/test/dpms_test.c b/test/dpms_test.c new file mode 100644 index 00000000..065091da --- /dev/null +++ b/test/dpms_test.c @@ -0,0 +1,447 @@ +// 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, event_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) { + TEST_ASSERT_NULL(pending_flip); + pending_flip = userdata; + } else { + // Power changes must follow the outstanding flip and must not leave a + // queued frame that could enable the CRTC again. + TEST_ASSERT_NULL(pending_flip); + TEST_ASSERT_FALSE(device.per_crtc_state[0].flip_pending); + TEST_ASSERT_NULL(device.per_crtc_state[0].queued); + } + 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 (event_error != 0 || 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); + // Match the debug DRM device mutex so an extra unlock fails immediately. + pthread_mutexattr_t mutex_attrs; + TEST_ASSERT_EQUAL_INT(0, pthread_mutexattr_init(&mutex_attrs)); + TEST_ASSERT_EQUAL_INT(0, pthread_mutexattr_settype(&mutex_attrs, PTHREAD_MUTEX_ERRORCHECK)); + TEST_ASSERT_EQUAL_INT(0, pthread_mutex_init(&device.mutex, &mutex_attrs)); + TEST_ASSERT_EQUAL_INT(0, pthread_mutexattr_destroy(&mutex_attrs)); + 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 = event_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 struct kms_req *present_frame(void) { + struct kms_req_builder *builder = drmdev_create_request_builder(&device, crtc.id); + TEST_ASSERT_NOT_NULL(builder); + TEST_ASSERT_EQUAL_INT(0, kms_req_builder_set_connector(builder, connector.id)); + struct kms_req *req = kms_req_builder_build(builder); + kms_req_builder_unref(builder); + TEST_ASSERT_EQUAL_INT(0, kms_req_present(req)); + return req; +} + +static void test_disable_drains_flip_discards_queue_and_can_replay(void) { + struct kms_req *pending = present_frame(); + struct kms_req *queued = present_frame(); + TEST_ASSERT_EQUAL_INT(1, atomic_commits); + TEST_ASSERT_TRUE(device.per_crtc_state[0].flip_pending); + TEST_ASSERT_EQUAL_PTR(queued, device.per_crtc_state[0].queued); + + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_INT(0, window_get_dpms(&output)); + TEST_ASSERT_FALSE(crtc.committed_state.active); + TEST_ASSERT_EQUAL_INT(2, atomic_commits); + TEST_ASSERT_EQUAL_INT(1, event_reads); + TEST_ASSERT_EQUAL_UINT64(0, last_active); + TEST_ASSERT_EQUAL_UINT32(DRM_MODE_ATOMIC_ALLOW_MODESET, last_flags); + TEST_ASSERT_NULL(pending_flip); + TEST_ASSERT_FALSE(device.per_crtc_state[0].flip_pending); + TEST_ASSERT_NULL(device.per_crtc_state[0].queued); + TEST_ASSERT_EQUAL_PTR(pending, device.per_crtc_state[0].last_flipped); + TEST_ASSERT_EQUAL_INT(2, refcount_get_for_debug(&((struct kms_req_builder *) pending)->n_refs)); + TEST_ASSERT_EQUAL_INT(1, refcount_get_for_debug(&((struct kms_req_builder *) queued)->n_refs)); + + struct fl_layer_composition *latest = fl_layer_composition_new(0); + TEST_ASSERT_EQUAL_INT(0, kms_window_push_composition(&output, latest)); + TEST_ASSERT_EQUAL_INT(2, atomic_commits); + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, true)); + TEST_ASSERT_EQUAL_INT(1, window_get_dpms(&output)); + TEST_ASSERT_TRUE(crtc.committed_state.active); + TEST_ASSERT_EQUAL_INT(3, atomic_commits); + TEST_ASSERT_EQUAL_INT(2, event_reads); + TEST_ASSERT_EQUAL_UINT64(1, last_active); + TEST_ASSERT_EQUAL_PTR(latest, output.composition); + TEST_ASSERT_EQUAL_INT(1, refcount_get_for_debug(&((struct kms_req_builder *) pending)->n_refs)); + fl_layer_composition_unref(latest); + kms_req_unref(queued); + kms_req_unref(pending); +} + +static void test_failed_flip_drain_preserves_mode_and_can_be_retried(void) { + struct kms_req *pending = present_frame(); + struct kms_req *queued = present_frame(); + event_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(1, atomic_commits); + TEST_ASSERT_EQUAL_INT(1, event_reads); + TEST_ASSERT_EQUAL_PTR(pending, pending_flip); + TEST_ASSERT_TRUE(device.per_crtc_state[0].flip_pending); + TEST_ASSERT_NULL(device.per_crtc_state[0].queued); + TEST_ASSERT_EQUAL_INT(1, refcount_get_for_debug(&((struct kms_req_builder *) queued)->n_refs)); + + event_error = 0; + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_INT(0, window_get_dpms(&output)); + TEST_ASSERT_EQUAL_INT(2, atomic_commits); + TEST_ASSERT_EQUAL_INT(2, event_reads); + TEST_ASSERT_NULL(pending_flip); + kms_req_unref(queued); + kms_req_unref(pending); +} + +static void test_failed_disable_after_flip_drain_preserves_mode(void) { + struct kms_req *pending = present_frame(); + struct kms_req *queued = present_frame(); + 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(2, atomic_commits); + TEST_ASSERT_EQUAL_INT(1, event_reads); + TEST_ASSERT_NULL(pending_flip); + TEST_ASSERT_FALSE(device.per_crtc_state[0].flip_pending); + TEST_ASSERT_NULL(device.per_crtc_state[0].queued); + TEST_ASSERT_EQUAL_INT(1, refcount_get_for_debug(&((struct kms_req_builder *) queued)->n_refs)); + + commit_error = 0; + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + TEST_ASSERT_EQUAL_INT(0, window_get_dpms(&output)); + TEST_ASSERT_EQUAL_INT(3, atomic_commits); + TEST_ASSERT_EQUAL_INT(1, event_reads); + kms_req_unref(queued); + kms_req_unref(pending); +} + +static void test_blocking_frame_supersedes_queued_frame(void) { + struct kms_req *pending = present_frame(); + struct kms_req *queued = present_frame(); + struct kms_req_builder *builder = drmdev_create_request_builder(&device, crtc.id); + TEST_ASSERT_NOT_NULL(builder); + TEST_ASSERT_EQUAL_INT(0, kms_req_builder_set_connector(builder, connector.id)); + struct kms_req *replacement = kms_req_builder_build(builder); + kms_req_builder_unref(builder); + + uint64_t timestamp = 0; + TEST_ASSERT_EQUAL_INT(0, kms_req_commit_blocking(replacement, ×tamp)); + TEST_ASSERT_EQUAL_UINT64(1000000000, timestamp); + TEST_ASSERT_EQUAL_INT(2, atomic_commits); + TEST_ASSERT_EQUAL_INT(2, event_reads); + TEST_ASSERT_EQUAL_PTR(replacement, device.per_crtc_state[0].last_flipped); + TEST_ASSERT_FALSE(device.per_crtc_state[0].flip_pending); + TEST_ASSERT_NULL(device.per_crtc_state[0].queued); + TEST_ASSERT_EQUAL_INT(1, refcount_get_for_debug(&((struct kms_req_builder *) pending)->n_refs)); + TEST_ASSERT_EQUAL_INT(1, refcount_get_for_debug(&((struct kms_req_builder *) queued)->n_refs)); + kms_req_unref(replacement); + kms_req_unref(queued); + kms_req_unref(pending); +} + +static void test_nonblocking_power_request_does_not_drain_frames(void) { + struct kms_req *pending = present_frame(); + struct kms_req *queued = present_frame(); + struct kms_req *off = drmdev_create_dpms_req(&device, crtc.id, connector.id, false); + TEST_ASSERT_NOT_NULL(off); + TEST_ASSERT_EQUAL_INT(ENOTSUP, kms_req_commit_nonblocking(off, NULL, NULL, NULL)); + TEST_ASSERT_EQUAL_INT(0, event_reads); + TEST_ASSERT_EQUAL_INT(1, atomic_commits); + TEST_ASSERT_EQUAL_PTR(pending, pending_flip); + TEST_ASSERT_EQUAL_PTR(queued, device.per_crtc_state[0].queued); + TEST_ASSERT_EQUAL_INT(0, window_set_dpms(&output, false)); + kms_req_unref(off); + kms_req_unref(queued); + kms_req_unref(pending); +} + +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_disable_drains_flip_discards_queue_and_can_replay); + RUN_TEST(test_failed_flip_drain_preserves_mode_and_can_be_retried); + RUN_TEST(test_failed_disable_after_flip_drain_preserves_mode); + RUN_TEST(test_blocking_frame_supersedes_queued_frame); + RUN_TEST(test_nonblocking_power_request_does_not_drain_frames); + 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(); +} diff --git a/test/kms_present_test.c b/test/kms_present_test.c index ef650486..c856ed27 100644 --- a/test/kms_present_test.c +++ b/test/kms_present_test.c @@ -87,8 +87,8 @@ static void flip(struct drmdev *dev) { } int main(void) { - struct drm_crtc crtc = { .id = 1, .index = 0 }; - struct drm_connector connector = {0}; + struct drm_crtc crtc = { .id = 1, .index = 0, .ids.active = 2 }; + struct drm_connector connector = { .id = 3, .ids.crtc_id = 4 }; struct drm_plane plane = {0}; struct drmdev dev = { .n_refs = REFCOUNT_INIT_1,