backend/drm: implement KMS state snapshot/restore

Following ideas from [1], snapshot the entire KMS state when the VT
is switched away, and restore it when the VT is switched back.

> Well the neat trick is that userspace doesn’t need to be able to
> understand properties to save and restore them - the actual property
> value transport between kernel and userspace is fully generic.

That way, even if another DRM master changes a property we don't
understand like CTM or HDR_OUTPUT_METADATA, we can switch it back
and avoid getting garbage on screen.

[1]: https://blog.ffwll.ch/2016/01/vt-switching-with-atomic-modeset.html
This commit is contained in:
Simon Ser 2022-10-18 18:23:55 +02:00
parent e0b2bf2a6b
commit f0bb53c4ab
7 changed files with 155 additions and 20 deletions

View file

@ -342,6 +342,33 @@ static bool atomic_crtc_commit(struct wlr_drm_connector *conn,
return ok;
}
static bool atomic_snapshot_state(struct wlr_drm_backend *drm) {
drmModeAtomicFree(drm->atomic_snapshot);
drm->atomic_snapshot = snapshot_drm_state(drm->fd);
return drm->atomic_snapshot != NULL;
}
static bool atomic_restore_state(struct wlr_drm_backend *drm) {
if (drm->atomic_snapshot == NULL) {
wlr_log(WLR_DEBUG, "No atomic snapshot, falling back to legacy state restoration");
return legacy_restore_state(drm);
}
uint32_t flags = DRM_MODE_ATOMIC_ALLOW_MODESET;
int ret = drmModeAtomicCommit(drm->fd, drm->atomic_snapshot, flags, NULL);
if (ret != 0) {
wlr_log_errno(WLR_ERROR, "drmModeAtomicCommit failed");
return false;
}
drmModeAtomicFree(drm->atomic_snapshot);
drm->atomic_snapshot = NULL;
return true;
}
const struct wlr_drm_interface atomic_iface = {
.crtc_commit = atomic_crtc_commit,
.snapshot_state = atomic_snapshot_state,
.restore_state = atomic_restore_state,
};

View file

@ -55,6 +55,7 @@ static void backend_destroy(struct wlr_backend *backend) {
finish_drm_renderer(&drm->mgpu_renderer);
}
drmModeAtomicFree(drm->atomic_snapshot);
finish_drm_resources(drm);
free(drm->name);
@ -101,30 +102,18 @@ static void handle_session_active(struct wl_listener *listener, void *data) {
if (session->active) {
wlr_log(WLR_INFO, "DRM fd resumed");
scan_drm_connectors(drm, NULL);
struct wlr_drm_connector *conn;
wl_list_for_each(conn, &drm->connectors, link) {
struct wlr_output_mode *mode = NULL;
uint32_t committed = WLR_OUTPUT_STATE_ENABLED;
if (conn->status != DRM_MODE_DISCONNECTED && conn->output.enabled
&& conn->output.current_mode != NULL) {
committed |= WLR_OUTPUT_STATE_MODE;
mode = conn->output.current_mode;
}
struct wlr_output_state state = {
.committed = committed,
.allow_artifacts = true,
.enabled = mode != NULL,
.mode_type = WLR_OUTPUT_STATE_MODE_FIXED,
.mode = mode,
};
if (!drm_connector_commit_state(conn, &state)) {
wlr_drm_conn_log(conn, WLR_ERROR, "Failed to restore state after VT switch");
}
if (!drm->iface->restore_state(drm)) {
wlr_log(WLR_ERROR, "Failed to restore KMS state after VT switch");
}
scan_drm_connectors(drm, NULL);
} else {
wlr_log(WLR_INFO, "DRM fd paused");
if (drm->iface->snapshot_state && !drm->iface->snapshot_state(drm)) {
wlr_log(WLR_ERROR, "Failed to snapshot KMS state");
}
}
}

View file

@ -227,6 +227,34 @@ bool drm_legacy_crtc_set_gamma(struct wlr_drm_backend *drm,
return true;
}
bool legacy_restore_state(struct wlr_drm_backend *drm) {
// Best-effort state restoration
bool ok = true;
struct wlr_drm_connector *conn;
wl_list_for_each(conn, &drm->connectors, link) {
struct wlr_output_mode *mode = NULL;
uint32_t committed = WLR_OUTPUT_STATE_ENABLED;
if (conn->status != DRM_MODE_DISCONNECTED && conn->output.enabled
&& conn->output.current_mode != NULL) {
committed |= WLR_OUTPUT_STATE_MODE;
mode = conn->output.current_mode;
}
struct wlr_output_state state = {
.committed = committed,
.allow_artifacts = true,
.enabled = mode != NULL,
.mode_type = WLR_OUTPUT_STATE_MODE_FIXED,
.mode = mode,
};
if (!drm_connector_commit_state(conn, &state)) {
wlr_drm_conn_log(conn, WLR_ERROR, "Failed to restore state");
ok = false;
}
}
return ok;
}
const struct wlr_drm_interface legacy_iface = {
.crtc_commit = legacy_crtc_commit,
.restore_state = legacy_restore_state,
};

View file

@ -276,3 +276,87 @@ size_t match_obj(size_t num_objs, const uint32_t objs[static restrict num_objs],
match_obj_(&st, 0, 0, 0, 0);
return st.score;
}
static bool snapshot_drm_object(int drm_fd, drmModeAtomicReq *req,
uint32_t object_id) {
drmModeObjectProperties *props =
drmModeObjectGetProperties(drm_fd, object_id, DRM_MODE_OBJECT_ANY);
if (props == NULL) {
wlr_log_errno(WLR_ERROR, "drmModeObjectGetProperties failed");
return false;
}
bool ok = true;
for (uint32_t i = 0; i < props->count_props; i++) {
uint32_t prop_id = props->props[i];
uint64_t value = props->prop_values[i];
drmModePropertyRes *prop = drmModeGetProperty(drm_fd, prop_id);
if (prop == NULL) {
wlr_log_errno(WLR_ERROR, "drmModeGetProperty failed");
ok = false;
break;
}
bool immutable = prop->flags & DRM_MODE_PROP_IMMUTABLE;
bool dpms = strcmp(prop->name, "DPMS") == 0;
drmModeFreeProperty(prop);
if (immutable || dpms) {
// DPMS is a bit special: it can only be set from the legacy uAPI. Restoring it from the
// atomic uAPI will fail.
continue;
}
if (drmModeAtomicAddProperty(req, object_id, prop_id, value) < 0) {
wlr_log_errno(WLR_ERROR, "drmModeAtomicAddProperty failed");
ok = false;
break;
}
}
drmModeFreeObjectProperties(props);
return ok;
}
drmModeAtomicReq *snapshot_drm_state(int drm_fd) {
drmModeAtomicReq *req = drmModeAtomicAlloc();
if (req == NULL) {
wlr_log_errno(WLR_ERROR, "drmModeAtomicAlloc failed");
return false;
}
bool ok = true;
drmModeRes *res = drmModeGetResources(drm_fd);
if (res == NULL) {
wlr_log_errno(WLR_ERROR, "drmModeGetResources failed");
goto error;
}
for (int i = 0; i < res->count_crtcs; i++) {
ok = ok && snapshot_drm_object(drm_fd, req, res->crtcs[i]);
}
for (int i = 0; i < res->count_connectors; i++) {
ok = ok && snapshot_drm_object(drm_fd, req, res->connectors[i]);
}
drmModeFreeResources(res);
if (!ok) {
goto error;
}
drmModePlaneRes *planes = drmModeGetPlaneResources(drm_fd);
if (planes == NULL) {
wlr_log_errno(WLR_ERROR, "drmModeGetPlaneResources failed");
return false;
}
for (uint32_t i = 0; i < planes->count_planes; i++) {
ok = ok && snapshot_drm_object(drm_fd, req, planes->planes[i]);
}
drmModeFreePlaneResources(planes);
if (!ok) {
goto error;
}
return req;
error:
drmModeAtomicFree(req);
return NULL;
}

View file

@ -85,6 +85,8 @@ struct wlr_drm_backend {
uint64_t cursor_width, cursor_height;
struct wlr_drm_format_set mgpu_formats;
drmModeAtomicReq *atomic_snapshot;
};
struct wlr_drm_mode {

View file

@ -17,6 +17,8 @@ struct wlr_drm_interface {
bool (*crtc_commit)(struct wlr_drm_connector *conn,
const struct wlr_drm_connector_state *state, uint32_t flags,
bool test_only);
bool (*snapshot_state)(struct wlr_drm_backend *drm);
bool (*restore_state)(struct wlr_drm_backend *drm);
};
extern const struct wlr_drm_interface atomic_iface;
@ -24,5 +26,6 @@ extern const struct wlr_drm_interface legacy_iface;
bool drm_legacy_crtc_set_gamma(struct wlr_drm_backend *drm,
struct wlr_drm_crtc *crtc, size_t size, uint16_t *lut);
bool legacy_restore_state(struct wlr_drm_backend *drm);
#endif

View file

@ -16,6 +16,8 @@ const char *get_pnp_manufacturer(uint16_t code);
void parse_edid(struct wlr_drm_connector *conn, size_t len, const uint8_t *data);
const char *drm_connector_status_str(drmModeConnection status);
drmModeAtomicReq *snapshot_drm_state(int drm_fd);
// Part of match_obj
enum {
UNMATCHED = (uint32_t)-1,