daemon: present the cursor on a layer of its own (GNCL cursor plane)
daemon: present the cursor on a layer of its own (GNCL)
Base: master (now at 53df34a, with #37). No longer stacked on #36: this PR stands alone and carries no latch-grid code (no GNLG cap, no latch.rs, no frozen LSFG lead, no gn_panel_refresh). Commits: 260d894 (GNCL cursor layer), 36e5ee1 (switchable presentation, experimental), c69d8f7 (staged presentation as the default, rotation derived from the window, experimental modes removed).
Problem
On Android gamescope cannot use a cursor plane: the daemon's cursor plane was capped at 64x64 and never presented, and drm_cursor_plane stays off because liftoff could otherwise park the game on it (black screen). So every cursor move is a full composite plus a scan-out flip. Measured on the Retroid Pocket 6 (120 Hz, Dave the Diver, cap 60, cursor moving): 70-76 presents/s on the scan-out layer (cursor-only composites), gamescope +2.8 % CPU, +500 wakeups/s; with LSFG x2 the cursor composites wreck pacing (719 doubled gaps per 1500 frames vs 160 hidden).
Change
- New private client cap
GNDRM_CLIENT_CAP_CURSOR_LAYER=0x474E434C("GNCL"), card node only. With it set the cursor plane reports 256x256 and is presented; without it (old gamescope) the 64x64 rule and today's behaviour are unchanged. An old daemon answers EINVAL and gamescope keeps the cursor in the composite. - With GNCL the cursor plane rejects (TEST_ONLY and real commits, EINVAL) any rect or framebuffer larger than 256, so liftoff can never place the game there.
- Presenter (
asc.rs): a siblinggndrm-cursorSurfaceFlinger layer above the scan-out layer, premultiplied alpha, geometry-only transactions for moves, FB 0 hides, rebuilt on retarget. The cursor image is copied once through the foreign-buffer path and follows the session layout rule (RGBA on sdk>=36 GLES). - HWC finding on the RP6 (portrait panel at ROTATION_90): a cursor layer with transform 90 plus 1.5x scaling falls back to GPU composition (the inline rotator takes UBWC only and the single IWE path is used by the scan-out layer). The presenter therefore copies the cursor pre-rotated by the display rotation and pre-scaled to the output scale and presents it with the inverse buffer transform, so HWC sees transform 0 and no scaling: DEVICE composition, correct size and orientation (verified on the RP6 as mode 7: buffer 24x18, dst 18x24, transform 7). This staged presentation is now the default (
asc cursor staged rot=.. (derived) transform=..in logcat). The geometry lives ingndrm_daemon::cursor(host-tested). - Rotation: no NDK call reports the display rotation, so it is derived from the window (
derive_rotation): 90 when the window's orientation differs from the panel's assumed natural one (landscape whenro.build.characteristicscontainstablet, portrait otherwise: phones and handhelds), else 0. Limitation: 90 and 270 cannot be told apart, and a panel of the other natural orientation gets the wrong value. The picture is right either way (pre-rotation and the inverse buffer transform cancel in window space); only the composer then sees a residual rotation (180, or 90/270) and may compose the cursor on the GPU, as before. - A 10-bit cursor or a failed staging allocation falls back to the padded buffer (composer-scaled, transform 0).
- Debug overrides, read live:
debug.gndrm.cursor_mode=0shows the padded buffer as KMS describes it;debug.gndrm.cursor_rot=0|90|180|270replaces the derived rotation. The other experimental modes are removed. - A cursor-only commit never republishes the scan-out layer, so LSFG gets no extra source frames.
Results (RP6, cap 60, same scene)
| cursor hidden | cursor moving | |
|---|---|---|
| before (fix 1 stack): scan-out presents/s | 46.9 (pre shim fix) | 70.78, full composite per move |
| this PR, GPU-composited cursor layer (mode 0) | 60.00 | 54.95, cursor CLIENT |
| this PR, pre-rotated/pre-scaled (default) | 59.98 | 54.85, both layers DEVICE, picture intact |
The game's own rate still drops ~5 fps with the cursor moving on this device; that is the per-motion wineserver work on the game's main thread (10 round trips per motion event under Xwayland, made expensive by libredirect's per-read readlink), not composition. Separate follow-up.
Tests
Golden: cap raises the limit and a render node is refused; anything larger than a cursor is rejected; cursor motion updates only the cursor layer; no presentation without the cap. Unit tests for the clip/crop geometry, the derived rotation, the staged geometry (the measured RP6 case), identity in window space for all four rotations, hotspot placement after scaling and edge clipping. cargo test -p gndrm-daemon -p gndrm-android (rust:1-bookworm, 129 tests) and clippy (aarch64-linux-android) clean after the rebase onto master.
Risks
HWC handling of a small translucent layer on other SoCs (it falls back to GPU, never to a wrong picture); one 256 KB copy per cursor image change; HDR sessions tag the cursor sRGB; the rotation is derived from the window, not read from the display (limits above), and an external display is not considered.