diff --git a/platform/drm/cog-drm-modeset-renderer.c b/platform/drm/cog-drm-modeset-renderer.c index 4b778941..a5f6f36d 100644 --- a/platform/drm/cog-drm-modeset-renderer.c +++ b/platform/drm/cog-drm-modeset-renderer.c @@ -7,6 +7,7 @@ #include "../../core/cog.h" #include "cog-drm-renderer.h" +#include "cursor-drm.h" #include #include #include @@ -74,6 +75,7 @@ struct buffer_object { uint32_t fb_id; struct gbm_bo *bo; struct wl_resource *buffer_resource; + void *renderer; /* owning CogDrmModesetRenderer */ struct { struct wl_resource *resource; @@ -105,6 +107,22 @@ typedef struct { drmModeObjectProperties *props; drmModePropertyRes **props_info; } connector_props, crtc_props, plane_props; + + /* Software cursor: composited into the scanout dumb buffer. + * size is the (square) cursor edge in pixels - the 16x16 artwork + * times the device scale factor; buffers hold the maximum. */ + struct { + bool enabled; + int x, y; + int size; + uint32_t image[COG_DRM_CURSOR_IMAGE_SIZE * COG_DRM_CURSOR_IMAGE_MAX_SCALE * + COG_DRM_CURSOR_IMAGE_SIZE * COG_DRM_CURSOR_IMAGE_MAX_SCALE]; + uint32_t saved[COG_DRM_CURSOR_IMAGE_SIZE * COG_DRM_CURSOR_IMAGE_MAX_SCALE * + COG_DRM_CURSOR_IMAGE_SIZE * COG_DRM_CURSOR_IMAGE_MAX_SCALE]; + struct gbm_bo *drawn_bo; /* cursor currently painted on this bo */ + int drawn_x, drawn_y, drawn_w, drawn_h; + gint64 last_move_us; + } sw_cursor; } CogDrmModesetRenderer; static inline int @@ -114,9 +132,177 @@ get_drm_fd(CogDrmModesetRenderer *self) return s->pfd.fd; } +static void +sw_cursor_restore(CogDrmModesetRenderer *self) +{ + struct gbm_bo *bo = self->sw_cursor.drawn_bo; + if (!bo) + return; + self->sw_cursor.drawn_bo = NULL; + + uint32_t stride = 0; + void *map = NULL; + gbm_bo_map(bo, 0, 0, gbm_bo_get_width(bo), gbm_bo_get_height(bo), GBM_BO_TRANSFER_WRITE, &stride, &map); + if (!map) + return; + for (int r = 0; r < self->sw_cursor.drawn_h; r++) + memcpy((uint8_t *) map + (self->sw_cursor.drawn_y + r) * stride + self->sw_cursor.drawn_x * 4, + self->sw_cursor.saved + r * self->sw_cursor.size, + self->sw_cursor.drawn_w * 4); + gbm_bo_unmap(bo, map); +} + +static void +sw_cursor_paint(CogDrmModesetRenderer *self, struct gbm_bo *bo) +{ + if (!self->sw_cursor.enabled || !bo) + return; + + const int S = self->sw_cursor.size; + int bw = gbm_bo_get_width(bo), bh = gbm_bo_get_height(bo); + int x = self->sw_cursor.x, y = self->sw_cursor.y; + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x >= bw) x = bw - 1; + if (y >= bh) y = bh - 1; + int w = MIN(S, bw - x), h = MIN(S, bh - y); + if (w <= 0 || h <= 0) + return; + + uint32_t stride = 0; + void *map = NULL; + gbm_bo_map(bo, 0, 0, bw, bh, GBM_BO_TRANSFER_READ_WRITE, &stride, &map); + if (!map) + return; + for (int r = 0; r < h; r++) { + uint32_t *dst = (uint32_t *) ((uint8_t *) map + (y + r) * stride) + x; + uint32_t *sav = self->sw_cursor.saved + r * S; + const uint32_t *img = self->sw_cursor.image + r * S; + for (int c = 0; c < w; c++) { + sav[c] = dst[c]; + uint32_t px = img[c], a = px >> 24; + if (a == 0xff) + dst[c] = px; + else if (a) { + uint32_t d = dst[c]; + uint32_t rr = ((px >> 16) & 0xff) + (((d >> 16) & 0xff) * (255 - a)) / 255; + uint32_t gg = ((px >> 8) & 0xff) + (((d >> 8) & 0xff) * (255 - a)) / 255; + uint32_t bb = (px & 0xff) + ((d & 0xff) * (255 - a)) / 255; + dst[c] = 0xff000000 | (rr << 16) | (gg << 8) | bb; + } + } + } + gbm_bo_unmap(bo, map); + + self->sw_cursor.drawn_bo = bo; + self->sw_cursor.drawn_x = x; + self->sw_cursor.drawn_y = y; + self->sw_cursor.drawn_w = w; + self->sw_cursor.drawn_h = h; +} + +bool +cog_drm_modeset_renderer_sw_cursor_enable(CogDrmRenderer *renderer, unsigned scale, unsigned *screen_w, + unsigned *screen_h) +{ + CogDrmModesetRenderer *self = (CogDrmModesetRenderer *) renderer; + + scale = CLAMP(scale, 1, COG_DRM_CURSOR_IMAGE_MAX_SCALE); + self->sw_cursor.size = COG_DRM_CURSOR_IMAGE_SIZE * scale; + cog_drm_cursor_image_argb_premult(self->sw_cursor.image, scale); + self->sw_cursor.enabled = true; + self->sw_cursor.x = self->mode.hdisplay / 2; + self->sw_cursor.y = self->mode.vdisplay / 2; + if (screen_w) + *screen_w = self->mode.hdisplay; + if (screen_h) + *screen_h = self->mode.vdisplay; + return true; +} + +void +cog_drm_modeset_renderer_sw_cursor_move(CogDrmRenderer *renderer, int x, int y) +{ + CogDrmModesetRenderer *self = (CogDrmModesetRenderer *) renderer; + + if (!self->sw_cursor.enabled) + return; + self->sw_cursor.x = x; + self->sw_cursor.y = y; + + /* Throttle to display rate: input events arrive much faster than + * frames; repainting on every event multiplies the raster's chance + * to catch a half-updated cursor (flicker). The final position is + * never lost - each event updates x/y and the next repaint uses it. */ + gint64 now = g_get_monotonic_time(); + if (now - self->sw_cursor.last_move_us < 16000) + return; + self->sw_cursor.last_move_us = now; + + /* Repaint in place ONLY on a bo the cursor was already baked into + * via the SHM frame path - those are CPU-writable dumb buffers. + * The committed buffer may be an IMPORTED DMABUF (hardware-rendered + * frames); mapping and scribbling on those crashes or corrupts - + * with no SHM frames the software cursor simply stays hidden. Use a + * single map session for restore + paint so the raster cannot catch + * the cursor-less gap between two separate map cycles. */ + struct gbm_bo *bo = self->sw_cursor.drawn_bo; + if (!bo) + return; + + const int S = self->sw_cursor.size; + int bw = gbm_bo_get_width(bo), bh = gbm_bo_get_height(bo); + uint32_t stride = 0; + void *map = NULL; + gbm_bo_map(bo, 0, 0, bw, bh, GBM_BO_TRANSFER_READ_WRITE, &stride, &map); + if (!map) + return; + + if (self->sw_cursor.drawn_bo == bo) { + for (int r = 0; r < self->sw_cursor.drawn_h; r++) + memcpy((uint8_t *) map + (self->sw_cursor.drawn_y + r) * stride + self->sw_cursor.drawn_x * 4, + self->sw_cursor.saved + r * S, self->sw_cursor.drawn_w * 4); + self->sw_cursor.drawn_bo = NULL; + } + + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x >= bw) x = bw - 1; + if (y >= bh) y = bh - 1; + int w = MIN(S, bw - x), h = MIN(S, bh - y); + if (w > 0 && h > 0) { + for (int r = 0; r < h; r++) { + uint32_t *dst = (uint32_t *) ((uint8_t *) map + (y + r) * stride) + x; + uint32_t *sav = self->sw_cursor.saved + r * S; + const uint32_t *img = self->sw_cursor.image + r * S; + for (int c = 0; c < w; c++) { + sav[c] = dst[c]; + uint32_t px = img[c], a = px >> 24; + if (a == 0xff) + dst[c] = px; + else if (a) { + uint32_t d = dst[c]; + uint32_t rr = ((px >> 16) & 0xff) + (((d >> 16) & 0xff) * (255 - a)) / 255; + uint32_t gg = ((px >> 8) & 0xff) + (((d >> 8) & 0xff) * (255 - a)) / 255; + uint32_t bb = (px & 0xff) + ((d & 0xff) * (255 - a)) / 255; + dst[c] = 0xff000000 | (rr << 16) | (gg << 8) | bb; + } + } + } + self->sw_cursor.drawn_bo = bo; + self->sw_cursor.drawn_x = x; + self->sw_cursor.drawn_y = y; + self->sw_cursor.drawn_w = w; + self->sw_cursor.drawn_h = h; + } + gbm_bo_unmap(bo, map); +} + static void destroy_buffer(CogDrmModesetRenderer *renderer, struct buffer_object *buffer) { + if (renderer->sw_cursor.drawn_bo == buffer->bo) + renderer->sw_cursor.drawn_bo = NULL; drmModeRmFB(get_drm_fd(renderer), buffer->fb_id); gbm_bo_destroy(buffer->bo); @@ -138,14 +324,13 @@ static void destroy_buffer_notify(struct wl_listener *listener, void *data) { struct buffer_object *buffer = wl_container_of(listener, buffer, destroy_listener); - CogDrmModesetRenderer *renderer = wl_resource_get_user_data(buffer->buffer_resource); + CogDrmModesetRenderer *renderer = buffer->renderer; if (renderer->committed_buffer == buffer) renderer->committed_buffer = NULL; wl_list_remove(&buffer->link); - wl_resource_set_user_data(buffer->buffer_resource, NULL); destroy_buffer(renderer, buffer); } @@ -216,7 +401,7 @@ drm_create_buffer_for_bo(CogDrmModesetRenderer *self, wl_list_insert(&self->buffer_list, &buffer->link); buffer->destroy_listener.notify = destroy_buffer_notify; wl_resource_add_destroy_listener(buffer_resource, &buffer->destroy_listener); - wl_resource_set_user_data(buffer_resource, self); + buffer->renderer = self; buffer->fb_id = fb_id; buffer->bo = bo; @@ -271,12 +456,14 @@ drm_create_buffer_for_shm_buffer(CogDrmModesetRenderer *self, wl_list_insert(&self->buffer_list, &buffer->link); buffer->destroy_listener.notify = destroy_buffer_notify; wl_resource_add_destroy_listener(buffer_resource, &buffer->destroy_listener); - wl_resource_set_user_data(buffer_resource, self); + buffer->renderer = self; buffer->fb_id = fb_id; buffer->bo = bo; buffer->buffer_resource = buffer_resource; + g_message("created shm bo %dx%d for resource %p", width, height, (void *) buffer_resource); + return buffer; } @@ -287,6 +474,19 @@ drm_copy_shm_buffer_into_bo(struct wl_shm_buffer *shm_buffer, struct gbm_bo *bo) int32_t height = wl_shm_buffer_get_height(shm_buffer); int32_t stride = wl_shm_buffer_get_stride(shm_buffer); + /* DEBUG/hardening: never write past the bo - clamp and report any + * size mismatch (stale buffer_object?) instead of crashing. */ + uint32_t pre_bo_width = gbm_bo_get_width(bo); + uint32_t pre_bo_height = gbm_bo_get_height(bo); + if ((uint32_t) width > pre_bo_width || (uint32_t) height > pre_bo_height) { + g_warning("SHM->bo size mismatch: shm %dx%d (stride %d) vs bo %ux%u - clamping", + width, height, stride, pre_bo_width, pre_bo_height); + if ((uint32_t) width > pre_bo_width) + width = pre_bo_width; + if ((uint32_t) height > pre_bo_height) + height = pre_bo_height; + } + uint32_t bo_stride = 0; void *map_data = NULL; gbm_bo_map(bo, 0, 0, width, height, GBM_BO_TRANSFER_WRITE, &bo_stride, &map_data); @@ -296,6 +496,11 @@ drm_copy_shm_buffer_into_bo(struct wl_shm_buffer *shm_buffer, struct gbm_bo *bo) wl_shm_buffer_begin_access(shm_buffer); uint8_t *src = wl_shm_buffer_get_data(shm_buffer); + if (!src) { + wl_shm_buffer_end_access(shm_buffer); + gbm_bo_unmap(bo, map_data); + return; + } uint8_t *dst = map_data; uint32_t bo_width = gbm_bo_get_width(bo); @@ -522,6 +727,8 @@ on_export_dmabuf_resource(void *data, struct wpe_view_backend_exportable_fdo_dma } } +static unsigned bogus_streak = 0; + static void on_export_shm_buffer(void *data, struct wpe_fdo_shm_exported_buffer *exported_buffer) { @@ -530,20 +737,57 @@ on_export_shm_buffer(void *data, struct wpe_fdo_shm_exported_buffer *exported_bu struct wl_resource *exported_resource = wpe_fdo_shm_exported_buffer_get_resource(exported_buffer); struct wl_shm_buffer *exported_shm_buffer = wpe_fdo_shm_exported_buffer_get_shm_buffer(exported_buffer); - struct buffer_object *buffer = drm_buffer_for_resource(self, exported_resource); - if (buffer) { - drm_copy_shm_buffer_into_bo(exported_shm_buffer, buffer->bo); - - buffer->export.shm_buffer = exported_buffer; - drm_commit_buffer(self, buffer); - return; + /* Trace + guard: wpebackend-fdo (<= 1.16.1) never clears + * Surface::shmBuffer, so a commit without a fresh attach exports a + * stale - potentially dangling - wl_shm_buffer whose metadata reads + * as garbage (negative width/stride). Never touch buffers with + * impossible geometry; skip the frame instead of crashing. */ + { + int32_t w = exported_shm_buffer ? wl_shm_buffer_get_width(exported_shm_buffer) : -1; + int32_t h = exported_shm_buffer ? wl_shm_buffer_get_height(exported_shm_buffer) : -1; + int32_t st = exported_shm_buffer ? wl_shm_buffer_get_stride(exported_shm_buffer) : -1; + g_debug("shm export: resource %p shm %p %dx%d stride %d", + (void *) exported_resource, (void *) exported_shm_buffer, w, h, st); + if (!exported_resource || w <= 0 || h <= 0 || w > 16384 || h > 16384 || st < w * 4) { + g_warning("shm export with bogus geometry (resource %p, shm %p, %dx%d stride %d) - frame skipped (streak %u)", + (void *) exported_resource, (void *) exported_shm_buffer, w, h, st, ++bogus_streak); + /* Do NOT release the wrapper: dispatch_release would send a + * wl_buffer.release through the (dangling) resource, poking + * freed protocol state on every skipped frame. Leaking the + * small wrapper is the lesser evil in an already-broken + * session. Keep pacing alive for a few frames in case the + * client recovers; after that, exit and let procd respawn a + * clean process (bounded by the respawn limit). */ + if (bogus_streak >= 4) { + g_critical("persistent bogus SHM exports - exiting for a clean respawn"); + exit(70); + } + wpe_view_backend_exportable_fdo_dispatch_frame_complete(self->exportable); + return; + } + bogus_streak = 0; } - buffer = drm_create_buffer_for_shm_buffer(self, exported_resource, exported_shm_buffer); + struct buffer_object *buffer = drm_buffer_for_resource(self, exported_resource); + if (!buffer) + buffer = drm_create_buffer_for_shm_buffer(self, exported_resource, exported_shm_buffer); if (buffer) { drm_copy_shm_buffer_into_bo(exported_shm_buffer, buffer->bo); - buffer->export.shm_buffer = exported_buffer; + /* the copy replaced the whole bo content - the previously saved + * background patch is void; bake the cursor into the new frame */ + if (self->sw_cursor.drawn_bo == buffer->bo) + self->sw_cursor.drawn_bo = NULL; + sw_cursor_paint(self, buffer->bo); + + /* The dumb buffer owns the pixels now: release the client's SHM + * buffer right away instead of parking it until the frame retires. + * Holding it keeps an external reference on the wl_shm pool, which + * turns client-side pool resizes into deferred remaps - the next + * copy would then use a stale mapping (libwayland warns "Buffer + * address requested when its parent pool has an external reference + * and a deferred resize pending.") and walk off its end. */ + wpe_view_backend_exportable_fdo_dispatch_release_shm_exported_buffer(self->exportable, exported_buffer); drm_commit_buffer(self, buffer); } } diff --git a/platform/drm/cog-drm-renderer.h b/platform/drm/cog-drm-renderer.h index 4308f558..efa37f04 100644 --- a/platform/drm/cog-drm-renderer.h +++ b/platform/drm/cog-drm-renderer.h @@ -65,6 +65,13 @@ CogDrmRenderer *cog_drm_modeset_renderer_new(struct gbm_device *dev, const drmModeModeInfo *mode, bool atomic_modesetting); +/* Software cursor, modeset renderer only: composited into the scanout + * buffer (hardware cursors are broken on some display blocks, e.g. + * radeon DCE4.1 - see RADEON_DCE41_CURSOR_NOTES). */ +bool cog_drm_modeset_renderer_sw_cursor_enable(CogDrmRenderer *, unsigned scale, unsigned *screen_w, + unsigned *screen_h); +void cog_drm_modeset_renderer_sw_cursor_move(CogDrmRenderer *, int x, int y); + CogDrmRenderer *cog_drm_gles_renderer_new(struct gbm_device *dev, EGLDisplay display, uint32_t plane_id, diff --git a/platform/drm/cog-platform-drm.c b/platform/drm/cog-platform-drm.c index 26858972..c83d2be0 100644 --- a/platform/drm/cog-platform-drm.c +++ b/platform/drm/cog-platform-drm.c @@ -154,8 +154,13 @@ static struct { .mode_set = false, }; +static CogDrmRenderer *sw_cursor_renderer = NULL; + static struct { gboolean enabled; + gboolean software; /* composited by the modeset renderer */ + gboolean legacy; /* no universal cursor plane: drmModeSetCursor path */ + gboolean armed; /* legacy cursor uploaded to the CRTC */ struct kms_device *device; struct kms_plane *plane; struct kms_framebuffer *cursor; @@ -424,6 +429,15 @@ find_crtc_for_encoder(const drmModeRes *resources, const drmModeEncoder *encoder } } + /* Cold start: with no prior modeset (no fbcon/firmware POST) the + * encoder is bound to no CRTC yet (crtc_id == 0), so the match on + * the CURRENT binding above finds nothing. Fall back to the first + * CRTC this encoder can physically drive. */ + for (int i = 0; i < resources->count_crtcs; i++) { + if (encoder->possible_crtcs & (1 << i)) + return resources->crtcs[i]; + } + /* no match found */ return -1; } @@ -570,21 +584,35 @@ init_drm(void) (long)((drm_data.mode - drm_data.connector.obj->modes) / sizeof(drmModeModeInfo *)), drm_data.mode->name, drm_data.mode->vrefresh); - /* Try the currently connected encoder+crtc */ - for (int i = 0; i < drm_data.base_resources->count_encoders; ++i) { - drm_data.encoder = drmModeGetEncoder(drm_data.fd, drm_data.base_resources->encoders[i]); - if (!drm_data.encoder) { - /* cannot retrieve encoder, ignoring... */ + /* Prefer THIS connector's own encoders: scanning all encoders can + * pick another output's encoder whose CRTC does not drive our + * connector. find_crtc_for_encoder() handles the cold-start case + * (no active CRTC binding yet) via its possible_crtcs fallback. */ + for (int i = 0; i < drm_data.connector.obj->count_encoders && !drm_data.encoder; ++i) { + drmModeEncoder *enc = drmModeGetEncoder(drm_data.fd, drm_data.connector.obj->encoders[i]); + if (!enc) continue; + const int32_t crtc_id = find_crtc_for_encoder(drm_data.base_resources, enc); + if (crtc_id != -1) { + drm_data.encoder = enc; + drm_data.crtc.obj_id = crtc_id; + break; } + drmModeFreeEncoder(enc); + } - const int32_t crtc_id = find_crtc_for_encoder(drm_data.base_resources, drm_data.encoder); + /* Last resort: any encoder that yields a CRTC. */ + for (int i = 0; i < drm_data.base_resources->count_encoders && !drm_data.encoder; ++i) { + drmModeEncoder *enc = drmModeGetEncoder(drm_data.fd, drm_data.base_resources->encoders[i]); + if (!enc) + continue; + const int32_t crtc_id = find_crtc_for_encoder(drm_data.base_resources, enc); if (crtc_id != -1) { + drm_data.encoder = enc; drm_data.crtc.obj_id = crtc_id; break; } - - g_clear_pointer (&drm_data.encoder, drmModeFreeEncoder); + drmModeFreeEncoder(enc); } if (!drm_data.encoder) { @@ -670,11 +698,23 @@ choose_format (struct kms_plane *plane) static void clear_cursor (void) { + if (cursor.legacy && cursor.armed) + drmModeSetCursor(drm_data.fd, drm_data.crtc.obj_id, 0, 0, 0); g_clear_pointer(&cursor.cursor, kms_framebuffer_free); g_clear_pointer(&cursor.device, kms_device_free); cursor.plane = NULL; } +static unsigned +cursor_scale (void) +{ + /* Track the view's device scale factor (init_config ran before any + * cursor setup) so the pointer keeps its apparent size next to the + * scaled UI; nearest integer, bounded by the 64x64 cursor buffer. */ + unsigned scale = (unsigned) (drm_data.device_scale + 0.5); + return CLAMP(scale, 1, COG_DRM_CURSOR_IMAGE_MAX_SCALE); +} + static gboolean init_cursor (void) { @@ -685,32 +725,47 @@ init_cursor (void) } cursor.device = kms_device_open(drm_data.fd); - if (!cursor.device) + if (!cursor.device) { + g_warning("cursor: kms_device_open failed"); return FALSE; + } cursor.plane = kms_device_find_plane_by_type(cursor.device, DRM_PLANE_TYPE_CURSOR, 0); if (!cursor.plane) { - g_clear_pointer(&cursor.device, kms_device_free); - return FALSE; + /* Legacy (non-atomic) drivers - radeon, older nouveau - expose no + * universal cursor plane; their hardware cursor is driven with + * drmModeSetCursor()/drmModeMoveCursor() on the CRTC instead. */ + cursor.legacy = TRUE; } - uint32_t format = choose_format(cursor.plane); + uint32_t format = cursor.legacy ? DRM_FORMAT_ARGB8888 : choose_format(cursor.plane); if (!format) { + g_warning("cursor: no supported plane format"); g_clear_pointer(&cursor.device, kms_device_free); return FALSE; } - cursor.cursor = create_cursor_framebuffer(cursor.device, format); + cursor.cursor = create_cursor_framebuffer(cursor.device, format, cursor_scale()); if (!cursor.cursor) { + g_warning("cursor: framebuffer creation failed"); g_clear_pointer(&cursor.device, kms_device_free); return FALSE; } - if (kms_plane_set(cursor.plane, cursor.cursor, cursor.x, cursor.y)) { - g_clear_pointer(&cursor.device, kms_device_free); - g_clear_pointer(&cursor.cursor, kms_framebuffer_free); - return FALSE; - } + /* The CRTC is usually not lit yet at platform setup time - the + * renderer performs the first modeset on the first frame commit - + * so this initial cursor upload may fail (display still off). Not + * fatal: the pointer-motion handler retries on every move and + * succeeds once a mode is active. */ + if (cursor.legacy) { + if (drmModeSetCursor(drm_data.fd, drm_data.crtc.obj_id, cursor.cursor->handle, + cursor.cursor->width, cursor.cursor->height) == 0) { + cursor.armed = TRUE; + drmModeMoveCursor(drm_data.fd, drm_data.crtc.obj_id, cursor.x, cursor.y); + } else + g_message("legacy cursor not set yet (display off?) - will appear on first pointer motion"); + } else if (kms_plane_set(cursor.plane, cursor.cursor, cursor.x, cursor.y)) + g_message("cursor plane not set yet (display off?) - will appear on first pointer motion"); cursor.enabled = TRUE; @@ -965,21 +1020,20 @@ input_handle_pointer_motion_event(struct libinput_event_pointer *pointer_event, cursor.x = libinput_event_pointer_get_absolute_x_transformed(pointer_event, cursor.screen_width); cursor.y = libinput_event_pointer_get_absolute_y_transformed(pointer_event, cursor.screen_height); } else { - cursor.x += libinput_event_pointer_get_dx(pointer_event); - cursor.y += libinput_event_pointer_get_dy(pointer_event); + /* cursor.{x,y} are unsigned: going past zero must be caught in + * floating point BEFORE the store, or the value wraps around and + * the upper clamp below pins the cursor to the opposite edge. */ + double x = cursor.x + libinput_event_pointer_get_dx(pointer_event); + double y = cursor.y + libinput_event_pointer_get_dy(pointer_event); + cursor.x = x < 0 ? 0 : (unsigned int) x; + cursor.y = y < 0 ? 0 : (unsigned int) y; } - if (cursor.x < 0) { - cursor.x = 0; - } else if (cursor.x > cursor.screen_width - 1) { + if (cursor.x > cursor.screen_width - 1) cursor.x = cursor.screen_width - 1; - } - if (cursor.y < 0) { - cursor.y = 0; - } else if (cursor.y > cursor.screen_height - 1) { + if (cursor.y > cursor.screen_height - 1) cursor.y = cursor.screen_height - 1; - } struct wpe_input_pointer_event event = { .type = wpe_input_pointer_event_type_motion, @@ -992,8 +1046,29 @@ input_handle_pointer_motion_event(struct libinput_event_pointer *pointer_event, }; wpe_view_backend_dispatch_pointer_event(wpe_view_data.backend, &event); - if (cursor.enabled) - kms_plane_set(cursor.plane, cursor.cursor, cursor.x, cursor.y); + if (cursor.enabled) { + if (cursor.software) { + if (sw_cursor_renderer) + cog_drm_modeset_renderer_sw_cursor_move(sw_cursor_renderer, (int) cursor.x, (int) cursor.y); + } else if (cursor.legacy) { + /* Re-upload on every motion: a modeset (the renderer's first + * frame commit, a mode change) silently disables the legacy + * hardware cursor, and there is no notification - MoveCursor + * alone would move an invisible cursor forever. */ + static gboolean logged = FALSE; + int sret = drmModeSetCursor(drm_data.fd, drm_data.crtc.obj_id, cursor.cursor->handle, + cursor.cursor->width, cursor.cursor->height); + int mret = drmModeMoveCursor(drm_data.fd, drm_data.crtc.obj_id, cursor.x, cursor.y); + if (!logged) { + g_message("legacy cursor: SetCursor=%d MoveCursor=%d (crtc %u, handle %u, %ux%u, pos %u,%u)", + sret, mret, drm_data.crtc.obj_id, cursor.cursor->handle, + cursor.cursor->width, cursor.cursor->height, cursor.x, cursor.y); + logged = TRUE; + } + cursor.armed = (sret == 0); + } else + kms_plane_set(cursor.plane, cursor.cursor, cursor.x, cursor.y); + } } static void @@ -1540,7 +1615,8 @@ cog_drm_platform_setup(CogPlatform *platform, CogShell *shell, const char *param return FALSE; } - if (self->draw_cursor) { + const char *cursor_env = g_getenv ("COG_PLATFORM_DRM_CURSOR"); + if (self->draw_cursor && !(cursor_env && strcmp (cursor_env, "sw") == 0)) { if (!init_cursor ()) { g_warning ("Failed to initialize cursor"); } @@ -1578,6 +1654,21 @@ cog_drm_platform_setup(CogPlatform *platform, CogShell *shell, const char *param drm_data.mode, drm_data.atomic_modesetting); } + if (cursor_env && strcmp (cursor_env, "sw") == 0) { + if (g_strcmp0 (self->renderer->name, "modeset") == 0) { + unsigned scr_w = 0, scr_h = 0; + if (cog_drm_modeset_renderer_sw_cursor_enable (self->renderer, cursor_scale(), &scr_w, &scr_h)) { + cursor.software = TRUE; + cursor.enabled = TRUE; + /* cursor position and screen bounds are established + * centrally by init_input() from the selected mode */ + sw_cursor_renderer = self->renderer; + g_message ("software cursor enabled (%ux%u)", scr_w, scr_h); + } + } else + g_warning ("software cursor requires the modeset renderer"); + } + if (cog_drm_renderer_supports_rotation(self->renderer, self->rotation)) { cog_drm_renderer_set_rotation(self->renderer, self->rotation); } else { diff --git a/platform/drm/cursor-drm.c b/platform/drm/cursor-drm.c index 7a4e480a..2d35e5fc 100644 --- a/platform/drm/cursor-drm.c +++ b/platform/drm/cursor-drm.c @@ -5,6 +5,7 @@ */ #include +#include #include "cursor-drm.h" #define CURSOR_WIDTH 16 @@ -58,8 +59,17 @@ static uint32_t convert_rgba_to_pixel_format(uint32_t rgba_pixel, uint32_t forma return rgba_pixel; case DRM_FORMAT_ARGB8888: { + /* KMS blending (hardware cursors included) expects + * PREMULTIPLIED alpha; the cursor image data is straight + * alpha. Without premultiplication every pixel whose color + * exceeds its alpha blends additively and washes out - on a + * light page only the black outline stays visible, reducing + * the arrow to a thin dotted line. */ uint8_t alpha = rgba_pixel & 0xff; - return (alpha << 24) + (rgba_pixel >> 8); + uint8_t r = ((rgba_pixel >> 24) & 0xff) * alpha / 255; + uint8_t g = ((rgba_pixel >> 16) & 0xff) * alpha / 255; + uint8_t b = ((rgba_pixel >> 8) & 0xff) * alpha / 255; + return ((uint32_t) alpha << 24) | ((uint32_t) r << 16) | ((uint32_t) g << 8) | b; } default: @@ -67,12 +77,48 @@ static uint32_t convert_rgba_to_pixel_format(uint32_t rgba_pixel, uint32_t forma } } -struct kms_framebuffer *create_cursor_framebuffer(struct kms_device *device, uint32_t format) +static unsigned clamp_cursor_scale(unsigned scale) +{ + if (scale < 1) + return 1; + if (scale > COG_DRM_CURSOR_IMAGE_MAX_SCALE) + return COG_DRM_CURSOR_IMAGE_MAX_SCALE; + return scale; +} + +void cog_drm_cursor_image_argb_premult(uint32_t *dst, unsigned scale) +{ + scale = clamp_cursor_scale(scale); + const unsigned size = CURSOR_WIDTH * scale; + + for (unsigned y = 0; y < size; y++) { + for (unsigned x = 0; x < size; x++) { + unsigned i = (y / scale) * CURSOR_WIDTH + (x / scale); + uint8_t r = cursorData[i * 4 + 0]; + uint8_t g = cursorData[i * 4 + 1]; + uint8_t b = cursorData[i * 4 + 2]; + uint8_t a = cursorData[i * 4 + 3]; + r = (uint16_t) r * a / 255; + g = (uint16_t) g * a / 255; + b = (uint16_t) b * a / 255; + dst[y * size + x] = ((uint32_t) a << 24) | ((uint32_t) r << 16) | ((uint32_t) g << 8) | b; + } + } +} + +struct kms_framebuffer *create_cursor_framebuffer(struct kms_device *device, uint32_t format, unsigned scale) { struct kms_framebuffer *fb; uint32_t *buf; - fb = kms_framebuffer_create(device, CURSOR_WIDTH, CURSOR_HEIGHT, format); + scale = clamp_cursor_scale(scale); + + /* Hardware cursors on several drivers (radeon in particular) only + * display buffers of exactly the size advertised by + * DRM_CAP_CURSOR_WIDTH/HEIGHT - typically 64x64. Smaller uploads are + * accepted by the ioctl but shown as nothing. Allocate 64x64 and + * blit the cursor image into the top-left corner, rest transparent. */ + fb = kms_framebuffer_create(device, 64, 64, format); if (!fb) return NULL; @@ -82,15 +128,27 @@ struct kms_framebuffer *create_cursor_framebuffer(struct kms_device *device, uin int index; uint32_t pixel; - for (int row = 0; row < fb->height; row++) { - for (int column = 0; column < fb->width; column++) { - index = (row * fb->width * 4) + (column * 4); - pixel = (cursorData[index] << 24) + - (cursorData[index + 1] << 16) + - (cursorData[index + 2] << 8) + - cursorData[index + 3]; + /* The legacy cursor engine ignores the BO's pitch and always reads + * tightly packed WIDTHx4-byte rows (radeon aligns dumb-buffer + * pitches far wider, e.g. 256 pixels - writing with that pitch + * smears the image into a dotted vertical line). Lay the pixels + * out with the hardware's fixed 64-pixel stride. */ + unsigned int stride_px = 64; - *buf++ = convert_rgba_to_pixel_format(pixel, format); + /* Nearest-neighbour upscale of the 16x16 artwork: a hardware cursor + * has a fixed physical size, so it must grow with the view's device + * scale factor or it dwarfs next to the scaled UI. */ + for (unsigned int row = 0; row < fb->height; row++) { + for (unsigned int column = 0; column < stride_px; column++) { + if (row < CURSOR_HEIGHT * scale && column < CURSOR_WIDTH * scale) { + index = ((row / scale) * CURSOR_WIDTH * 4) + ((column / scale) * 4); + pixel = (cursorData[index] << 24) + + (cursorData[index + 1] << 16) + + (cursorData[index + 2] << 8) + + cursorData[index + 3]; + buf[row * stride_px + column] = convert_rgba_to_pixel_format(pixel, format); + } else + buf[row * stride_px + column] = 0; } } diff --git a/platform/drm/cursor-drm.h b/platform/drm/cursor-drm.h index 371ea50a..8fcf7903 100644 --- a/platform/drm/cursor-drm.h +++ b/platform/drm/cursor-drm.h @@ -8,7 +8,19 @@ #define COG_CURSOR_DRM_H #include "kms.h" +#include -struct kms_framebuffer *create_cursor_framebuffer(struct kms_device *device, uint32_t format); +/* scale enlarges the 16x16 cursor artwork (nearest-neighbour) so it keeps + * pace with the view's device scale factor; the 64x64 hardware cursor + * buffer bounds it to 4. */ +struct kms_framebuffer *create_cursor_framebuffer(struct kms_device *device, uint32_t format, unsigned scale); + +/* The built-in cursor image as premultiplied ARGB8888, for renderers + * that composite a software cursor (broken hardware cursors exist, + * see RADEON_DCE41_CURSOR_NOTES). dst must hold (SIZE*scale)^2 pixels, + * scale in [1, MAX_SCALE]. */ +#define COG_DRM_CURSOR_IMAGE_SIZE 16 +#define COG_DRM_CURSOR_IMAGE_MAX_SCALE 4 +void cog_drm_cursor_image_argb_premult(uint32_t *dst, unsigned scale); #endif //COG_CURSOR_DRM_H