diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index 2199e275d..aba2baa58 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -226,7 +226,7 @@ static void on_client_created(struct wl_listener *l, void *data) { /* ------------------------------------------------------------------ surfaces */ -enum surface_role { ROLE_NONE, ROLE_TOPLEVEL, ROLE_SUBSURFACE, ROLE_DESKTOP }; +enum surface_role { ROLE_NONE, ROLE_TOPLEVEL, ROLE_SUBSURFACE, ROLE_DESKTOP, ROLE_CURSOR }; struct surface { struct wl_resource *resource; @@ -474,6 +474,69 @@ static struct seat_keyboard g_kbs[MAX_PTRS]; static int g_nkbs; static struct seat_touch g_touches[MAX_PTRS]; static int g_ntouches; + +/* ---- the client's cursor (wl_pointer.set_cursor) ------------------------------------------- + * Not composited: under zero-copy the program's buffer goes straight to an Android layer and there + * is nothing to composite into. The cursor surface is snapshotted here instead and the app draws it + * as its overlay, above every present path (Bannerlator's design). What it buys: the real SHAPE - + * labwc's resize arrows at a window's edges and corners, a text field's I-beam - and an + * authoritative HIDE (set_cursor with a NULL surface, what a mouse-look game does). + * wl_shm cursors (Wine, pixman) are copied directly; a GPU cursor (labwc on Vulkan/GLES, which + * wlroots hands over as a dma-buf, usually UBWC) is read back through vkp_image_readback. */ +#define CURSOR_MAX_PX (256 * 256) +static pthread_mutex_t g_cursor_lock = PTHREAD_MUTEX_INITIALIZER; +static struct surface *g_cursor_surface; /* compositor thread only */ +static int g_cursor_hx, g_cursor_hy; /* hotspot, surface-local */ +static uint32_t g_cursor_px[CURSOR_MAX_PX]; /* guarded by g_cursor_lock: ARGB8888 snapshot */ +static uint32_t g_cursor_rb[CURSOR_MAX_PX]; /* compositor thread: GPU readback staging */ +static int g_cursor_w, g_cursor_h; /* 0 = nothing to draw */ +static int g_cursor_hidden = 1; /* the client asked for no pointer */ +static int g_cursor_serial; /* bumped on every change; the app polls it */ + +static void cursor_publish_hidden(void) { + pthread_mutex_lock(&g_cursor_lock); + if (!g_cursor_hidden) { g_cursor_hidden = 1; g_cursor_serial++; } + pthread_mutex_unlock(&g_cursor_lock); +} + +static void cursor_publish_pixels(const uint8_t *src, int w, int h, size_t stride, int hx, int hy) { + if (w <= 0 || h <= 0 || w * h > CURSOR_MAX_PX) return; + pthread_mutex_lock(&g_cursor_lock); + for (int y = 0; y < h; y++) + memcpy(&g_cursor_px[y * w], src + (size_t)y * stride, (size_t)w * 4); + g_cursor_w = w; g_cursor_h = h; + g_cursor_hx = hx; g_cursor_hy = hy; + g_cursor_hidden = 0; + g_cursor_serial++; + pthread_mutex_unlock(&g_cursor_lock); +} + +static void cursor_publish_shm(struct wl_shm_buffer *shm, int hx, int hy) { + int32_t w = wl_shm_buffer_get_width(shm), h = wl_shm_buffer_get_height(shm); + if (w <= 0 || h <= 0 || w * h > CURSOR_MAX_PX) return; + wl_shm_buffer_begin_access(shm); + cursor_publish_pixels((const uint8_t *)wl_shm_buffer_get_data(shm), w, h, + (size_t)wl_shm_buffer_get_stride(shm), hx, hy); + wl_shm_buffer_end_access(shm); +} + +/* Read by the app (UI thread). out = [serial, hidden, w, h, hotspotX, hotspotY, pixels...]. + * Returns the number of ints written, or 0 if out is too small. */ +int banner_cursor_snapshot(int *out, int cap) { + int n = 0; + pthread_mutex_lock(&g_cursor_lock); + int need = 6 + (g_cursor_hidden ? 0 : g_cursor_w * g_cursor_h); + if (cap >= need) { + out[0] = g_cursor_serial; out[1] = g_cursor_hidden; + out[2] = g_cursor_w; out[3] = g_cursor_h; + out[4] = g_cursor_hx; out[5] = g_cursor_hy; + if (!g_cursor_hidden) + memcpy(out + 6, g_cursor_px, (size_t)g_cursor_w * g_cursor_h * 4); + n = need; + } + pthread_mutex_unlock(&g_cursor_lock); + return n; +} struct active_touch { struct surface *surface; int pointer_fallback; }; static struct active_touch g_touch_points[MAX_TOUCH_POINTS]; static void touch_cancel_surface(struct surface *surface); @@ -948,8 +1011,26 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { s->pending_buffer = NULL; s->pending_attach = 0; - if (s->role == ROLE_NONE) { - /* Cursor or role-less surface: never drawn (the app draws its own pointer). */ + if (s->role == ROLE_CURSOR) { + /* The client's pointer image, copied out for the app's overlay (see cursor_publish_*) + * rather than composited, so it survives the zero-copy and HDR layer paths. */ + if (!buffer) { + cursor_publish_hidden(); /* wlroots clears its cursor surface to hide the pointer */ + } else if (shm) { + cursor_publish_shm(shm, g_cursor_hx, g_cursor_hy); + } else if (db && db->n_planes >= 1 && db->width * db->height <= CURSOR_MAX_PX) { + if (!db->img && !db->import_failed) { + db->img = vkp_image_from_dmabuf(db->fd[0], db->format, db->modifier, db->width, db->height, + db->stride[0], db->offset[0]); + if (!db->img) db->import_failed = 1; + } + if (db->img && vkp_image_readback(db->img, g_cursor_rb, CURSOR_MAX_PX) == 0) + cursor_publish_pixels((const uint8_t *)g_cursor_rb, db->width, db->height, + (size_t)db->width * 4, g_cursor_hx, g_cursor_hy); + } + if (buffer) wl_buffer_send_release(buffer); + } else if (s->role == ROLE_NONE) { + /* Role-less surface: never drawn. */ if (buffer) wl_buffer_send_release(buffer); } else if (db) { take_dmabuf(s, db, buffer); @@ -1021,6 +1102,7 @@ static void surface_resource_destroy(struct wl_resource *r) { for (int i = 0; i < g_nptrs; i++) if (g_ptrs[i].focus == r) g_ptrs[i].focus = NULL; for (int i = 0; i < g_nkbs; i++) if (g_kbs[i].focus == r) g_kbs[i].focus = NULL; touch_cancel_surface(s); + if (g_cursor_surface == s) { g_cursor_surface = NULL; cursor_publish_hidden(); } if (g_grab == s) g_grab = NULL; if (g_key_target == s) g_key_target = NULL; if (g_ime_click == s) g_ime_click = NULL; @@ -2397,9 +2479,23 @@ static uint32_t now_ms(void) { return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); } +/* wl_pointer.set_cursor: the client tells us its pointer image, or asks for none. A NULL surface + * means "no cursor" and is authoritative. The surface itself is snapshotted on its next commit. */ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint32_t serial, struct wl_resource *surface, int32_t hx, int32_t hy) { - /* The app draws its own pointer; cursor surfaces stay role-less and aren't drawn. */ + if (!surface) { + if (g_cursor_surface) g_cursor_surface->role = ROLE_NONE; + g_cursor_surface = NULL; + cursor_publish_hidden(); + return; + } + struct surface *s = wl_resource_get_user_data(surface); + if (!s) return; + if (s->role != ROLE_NONE && s->role != ROLE_CURSOR) return; /* already has another role */ + s->role = ROLE_CURSOR; + g_cursor_surface = s; + g_cursor_hx = hx; + g_cursor_hy = hy; } static void pointer_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static const struct wl_pointer_interface pointer_impl = { diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.c b/app/src/main/cpp/waylandcomp/src/vk_present.c index 17846fb95..8c1d897fc 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -1910,6 +1910,104 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) { return 0; } +/* A client's GPU cursor read back to the CPU, for the app's pointer overlay (compositor.c, + * wl_pointer.set_cursor). wlroots hands a nested compositor its cursor as a dma-buf from the parent's + * formats - on Adreno usually UBWC, which the CPU cannot read - so it is blitted into a small linear, + * host-visible image first. Rare (a cursor changes shape, it is not redrawn per frame) and waited + * for on the CPU. out: w*h pixels, B8G8R8A8 in memory = 0xAARRGGBB ints. 0 = ok. */ +static struct vkp_image *g_rb; /* the readback image, kept for the next shape */ + +int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) { + if (!src || !out || src->w <= 0 || src->h <= 0 || src->w * src->h > max_px || + g_dev_state == -2 || dev_init() != 0) + return -1; + if (g_rb && (g_rb->w != src->w || g_rb->h != src->h)) { vkp_image_destroy(g_rb); g_rb = NULL; } + if (!g_rb) { + struct vkp_image *img = calloc(1, sizeof(*img)); + if (!img) return -1; + img->w = src->w; img->h = src->h; + VkImageCreateInfo ici = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, + .format = VK_FORMAT_B8G8R8A8_UNORM, .extent = {src->w, src->h, 1}, .mipLevels = 1, + .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_LINEAR, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED}; + if (g_vk.CreateImage(g_dev, &ici, NULL, &img->image) != VK_SUCCESS) { free(img); return -1; } + VkMemoryRequirements req; + g_vk.GetImageMemoryRequirements(g_dev, img->image, &req); + int idx = memory_type(req.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + VkMemoryAllocateInfo mai = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = req.size, .memoryTypeIndex = (uint32_t)idx}; + if (idx < 0 || g_vk.AllocateMemory(g_dev, &mai, NULL, &img->mem) != VK_SUCCESS) { + g_vk.DestroyImage(g_dev, img->image, NULL); free(img); return -1; + } + g_vk.BindImageMemory(g_dev, img->image, img->mem, 0); + VkImageSubresource subr = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0}; + VkSubresourceLayout lay; + g_vk.GetImageSubresourceLayout(g_dev, img->image, &subr, &lay); + img->offset = lay.offset; + img->row_pitch = lay.rowPitch; + if (g_vk.MapMemory(g_dev, img->mem, 0, req.size, 0, &img->map) != VK_SUCCESS) { + vkp_image_destroy(img); return -1; + } + g_rb = img; + } + VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkCommandBuffer cmd = g_cmds[0]; + g_vk.ResetCommandBuffer(cmd, 0); + VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; + g_vk.BeginCommandBuffer(cmd, &bi); + VkImageMemoryBarrier acq[2] = { + {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .srcQueueFamilyIndex = src->dmabuf ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED, + .image = src->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT}, + {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_rb->image, .subresourceRange = range, + .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}}; + g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, NULL, 0, NULL, 2, acq); + VkImageBlit blit = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .srcOffsets = {{0, 0, 0}, {src->w, src->h, 1}}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .dstOffsets = {{0, 0, 0}, {src->w, src->h, 1}}}; + g_vk.CmdBlitImage(cmd, src->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + g_rb->image, VK_IMAGE_LAYOUT_GENERAL, 1, &blit, VK_FILTER_NEAREST); + VkImageMemoryBarrier done[2] = { + {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_rb->image, .subresourceRange = range, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_HOST_READ_BIT}, + /* the client's buffer goes back to it */ + {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = src->dmabuf ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_IGNORED, + .image = src->image, .subresourceRange = range, .srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT}}; + g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, NULL, 0, NULL, 2, done); + g_vk.EndCommandBuffer(cmd); + VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &cmd}; + g_vk.ResetFences(g_dev, 1, &g_fence); + VkResult qr = g_vk.QueueSubmit(g_queue, 1, &si, g_fence); + if (qr != VK_SUCCESS) { + if (qr == VK_ERROR_DEVICE_LOST) device_lost("cursor readback"); + return -1; + } + VkResult fr = timed_wait(g_fence, 1000000000ULL); + if (fr == VK_ERROR_DEVICE_LOST) { device_lost("cursor readback"); return -1; } + if (fr != VK_SUCCESS) return -1; + for (int y = 0; y < src->h; y++) + memcpy(out + (size_t)y * src->w, (const uint8_t *)g_rb->map + g_rb->offset + (size_t)y * g_rb->row_pitch, + (size_t)src->w * 4); + return 0; +} + /* ---------------------------------------------------------------- the compositor pass, off-screen * * Layer mode with screen effects on: the game's frame still has to go through the compositor pass diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.h b/app/src/main/cpp/waylandcomp/src/vk_present.h index 4d77eda7c..899736520 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.h +++ b/app/src/main/cpp/waylandcomp/src/vk_present.h @@ -146,6 +146,9 @@ int vkp_pass_begin_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, i * wait_fd: a sync_file the copy must wait for before writing dst (the display's release fence of that * buffer), -1 = none. Always consumed (handed to the GPU, or waited for and closed). */ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd); +/* A client's GPU cursor (an imported dma-buf, often UBWC) read back as w*h 0xAARRGGBB pixels through a + * linear host-visible copy; waited for on the CPU. Compositor thread, between frames. 0 = ok. */ +int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px); /* Refresh the scene -> output mapping for this scene size without presenting (creates the * swapchain if needed, since the mapping is in output pixels). 0 = mapping valid. */ int vkp_update_map(int scene_w, int scene_h); diff --git a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c index 2125a5c08..10283f570 100644 --- a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c +++ b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c @@ -24,6 +24,7 @@ extern int banner_wayland_run(void); extern void banner_wayland_send_pointer(int action, int x, int y); extern void banner_wayland_send_touch(int action, int pointer_id, int x, int y); +extern int banner_cursor_snapshot(int *out, int cap); extern void banner_wayland_send_key(int evdev, int state); extern void banner_wayland_send_scene_input(int type, int a, int b); extern void banner_wayland_vsync(int64_t frame_time_ns); @@ -278,6 +279,21 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendTouch( banner_wayland_send_touch(action, pointer_id, x, y); } +/* The client's pointer image and whether it wants one at all (wl_pointer.set_cursor). + * out = [serial, hidden, w, h, hotspotX, hotspotY, ARGB pixels...]; returns ints written, 0 if the + * array is too small. The app polls the serial and only rebuilds its overlay when it changes. */ +JNIEXPORT jint JNICALL +Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot( + JNIEnv *env, jclass clazz, jintArray out) { + if (!out) return 0; + jint cap = (*env)->GetArrayLength(env, out); + jint *buf = (*env)->GetIntArrayElements(env, out, NULL); + if (!buf) return 0; + int n = banner_cursor_snapshot((int *)buf, (int)cap); + (*env)->ReleaseIntArrayElements(env, out, buf, 0); + return n; +} + /* Inject a key event. evdev = Linux input keycode (KEY_A=30…); state 1=down 0=up. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendKey( diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index 9a29329c5..a1a2be2a9 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -57,6 +57,7 @@ import com.droiddeck.launcher.wayland.HdrSupport import com.droiddeck.launcher.session.SessionService import com.droiddeck.launcher.session.SessionState import com.droiddeck.launcher.ui.CursorOverlay +import androidx.compose.ui.graphics.asImageBitmap import com.droiddeck.launcher.ui.DrawerActions import com.droiddeck.launcher.ui.HudText import com.droiddeck.launcher.ui.LoadingOverlay @@ -96,6 +97,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private val cursorHide = Runnable { cursorVisible = false } /** When the pad or the on-screen controls were last used (uptimeMillis); see [showCursor]. */ private var lastPadInputMs = 0L + // The program's own pointer, from wl_pointer.set_cursor (see syncClientCursor): its image in + // session pixels, hotspot, and whether it asked for no pointer at all. + private val cursorBuf = IntArray(WaylandCompositor.CURSOR_BUF_INTS) + private var cursorSerial = 0 + private var clientHidesCursor = false + private var cursorImage by mutableStateOf(null) + private var cursorHotX by mutableStateOf(0) + private var cursorHotY by mutableStateOf(0) + private var cursorImageScale by mutableStateOf(1f) + /** labwc changes the shape a moment after the move that reached an edge: look again then. */ + private val cursorResync = Runnable { if (syncClientCursor() && clientHidesCursor) cursorVisible = false } private val uiHandler = Handler(Looper.getMainLooper()) private var pendingBackAction: Runnable? = null private var drawerDirectionKey = KeyEvent.KEYCODE_UNKNOWN @@ -240,7 +252,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { isFocusableInTouchMode = true setContent { DroidDeckTheme { - CursorOverlay(cursorPos, cursorVisible, resources.displayMetrics.density) + CursorOverlay(cursorPos, cursorVisible, resources.displayMetrics.density, + cursorImage, cursorHotX, cursorHotY, cursorImageScale) if (hud.text.isNotEmpty()) HudText(hud.text) if (loading.visible) LoadingOverlay(loading.step, loading.percent, loading.elapsed, loading.hint, loading.ended) // Opening the drawer takes the controller away from the game: release its pad. @@ -1000,12 +1013,52 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { */ private fun showCursor(x: Float, y: Float) { cursorPos = androidx.compose.ui.geometry.Offset(x, y) + syncClientCursor() + uiHandler.removeCallbacks(cursorResync) + uiHandler.postDelayed(cursorResync, 60) + // A program that asked for no pointer (a mouse-look game) wins over everything below. + if (clientHidesCursor) { cursorVisible = false; return } if (android.os.SystemClock.uptimeMillis() - lastPadInputMs < CURSOR_PAD_HOLD_MS) return cursorVisible = true uiHandler.removeCallbacks(cursorHide) if (SessionState.mode == SessionService.MODE_STEAM && SessionState.steamUi != "desktop") uiHandler.postDelayed(cursorHide, 2500) } + /** + * Pulls the program's pointer (wl_pointer.set_cursor) if it changed: labwc's resize arrows at a + * window's edges and corners, a text field's I-beam, or a request for no pointer. Serial 0 = no + * program has set one yet, and the built-in arrow stays. Returns true when something changed. + */ + private fun syncClientCursor(): Boolean { + if (!CompositorHost.isStarted) return false + val n = WaylandCompositor.nativeCursorSnapshot(cursorBuf) + if (n < 6) return false + val serial = cursorBuf[0] + if (serial == 0 || serial == cursorSerial) return false + cursorSerial = serial + clientHidesCursor = cursorBuf[1] != 0 + if (clientHidesCursor) return true + val w = cursorBuf[2] + val h = cursorBuf[3] + if (w <= 0 || h <= 0 || n < 6 + w * h) return false + val bitmap = try { + android.graphics.Bitmap.createBitmap(cursorBuf, 6, w, w, h, android.graphics.Bitmap.Config.ARGB_8888) + } catch (e: Exception) { + Log.w(TAG, "cursor bitmap failed", e) + return false + } + cursorImage = bitmap.asImageBitmap() + cursorHotX = cursorBuf[4] + cursorHotY = cursorBuf[5] + // Session pixels -> screen pixels, as the session's picture is scaled (movePointer). + val width = surfaceView.width.takeIf { it > 0 } + val height = surfaceView.height.takeIf { it > 0 } + val out = SessionState.outputSize + if (width != null && height != null) + cursorImageScale = minOf(width / out.first.toFloat(), height / out.second.toFloat()) + return true + } + private fun refreshSecondScreenDisplays() { if (!::displayManager.isInitialized) return val primaryId = display?.displayId ?: windowManager.defaultDisplay.displayId diff --git a/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt index 3854526b3..6b527c97c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt @@ -6,13 +6,35 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke +/** + * The session's pointer. [image]: the program's own cursor (wl_pointer.set_cursor - labwc's resize + * arrows, a text field's I-beam), in session pixels, drawn [imageScale] times larger with its hotspot + * ([hotX], [hotY]) on [position]; null = the program has not set one, and the built-in arrow is drawn. + */ @Composable -fun CursorOverlay(position: Offset, visible: Boolean, scale: Float) { +fun CursorOverlay( + position: Offset, visible: Boolean, scale: Float, + image: ImageBitmap? = null, hotX: Int = 0, hotY: Int = 0, imageScale: Float = 1f, +) { if (!visible) return Canvas(modifier = Modifier.fillMaxSize()) { + if (image != null) { + val w = (image.width * imageScale).toInt().coerceAtLeast(1) + val h = (image.height * imageScale).toInt().coerceAtLeast(1) + drawImage( + image, + dstOffset = androidx.compose.ui.unit.IntOffset( + (position.x - hotX * imageScale).toInt(), (position.y - hotY * imageScale).toInt()), + dstSize = androidx.compose.ui.unit.IntSize(w, h), + filterQuality = FilterQuality.Low, + ) + return@Canvas + } val s = 11f * scale val path = Path().apply { moveTo(position.x, position.y) diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java b/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java index b9b2eb5d1..ffd1a28e5 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java +++ b/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java @@ -171,6 +171,17 @@ public final class WaylandCompositor { public static native void nativeSendTouch(int action, int pointerId, int x, int y); + /** + * The client's pointer image, straight from wl_pointer.set_cursor (labwc's resize arrows, a text + * field's I-beam). out = [serial, hidden, w, h, hotspotX, hotspotY, ARGB8888 pixels...]; returns + * the number of ints written, or 0 if {@code out} is too small. Serial 0 = the client has not + * set one yet; [1] = 1 means it asked for NO pointer. + */ + public static native int nativeCursorSnapshot(int[] out); + + /** Enough for the header plus a 256x256 cursor, the largest the compositor snapshots. */ + public static final int CURSOR_BUF_INTS = 6 + 256 * 256; + /** Inject a key event. evdev = Linux input keycode (KEY_A=30…); state: 1=down, 0=up. */ public static native void nativeSendKey(int evdev, int state);