gndrm-daemon: copy foreign dma-bufs into the pool instead of dropping the frame
Companion to GameNative/gamenative-drm#26, independent of it.
The gap. The daemon can only scan out buffers from its own AHardwareBuffer pool. gamescope and Xwayland end up with pool-backed memory only because the preload shim hijacks their large allocations (dma-heap today, ION with #26). Any buffer that reaches a commit without having gone through a hooked allocator is dropped at asc.rs (DROPPED frame: no AHardwareBuffer for dma-buf ino), and that line prints only with GNDRM_LOG_DAEMON. The user sees a black screen with audio and no error anywhere; that is what Adreno 650 owners got for OpenGL games, and it is what any future allocator or driver the shim does not know will produce.
Change. When a committed buffer is not pool-backed, copy it into the pool instead of dropping it.
commit.rshands the presenter aF_DUPFD_CLOEXECcopy of the GEM's dma-buf with the commit (publish_with_source; the default implementation is the oldpublish, so existing presenters and tests are untouched). The worker closes it after the publish, so a client closing the GEM while the commit is queued is safe.gndrm-daemon/src/foreign.rsholds the host-testable rules: LINEAR or no modifier, single plane, the four 8888 and four 2101010 formats, rows must fit the dma-buf; row copy honouring both pitches.gndrm-android/src/foreign.rs: once per inode and layout, map the dma-buf read-only and allocate a pool AHB in the matching format withCPU_WRITE_OFTEN; per frame wait the release fence, wait up to 100 ms on the acquire fence,DMA_BUF_IOCTL_SYNCstart, lock, copy, unlock, sync end, publish through the normal path. The AHB has its own completion mailbox, so release fences, present ids and the last-frame fallback behave as for pool buffers. Targets are freed when the inode leaves the live set and at teardown.- The pool's zero-copy path never enters this code; the per-commit cost on it is one dup and one close.
- Logging, always on, capped at 32 lines per session, none per frame:
gndrm-daemon: foreign dma-buf ino=N WxH fourcc=... pitch=... -> copied into pool (reason: not pool-backed)on the first copy per inode, acopy FAILED (<reason>); frames droppedline on failure, and aforeign dma-buf summary: copied_frames=N buffers=M failures=Kline at teardown, so a user's debug report shows which path carried the frames.
Verified alone, on BrowserStack Galaxy Tab S7 (SM-T875, Android 11, dma_heap_system=false ion=true), Legacy flavor, the SHIPPED 20261027.1 payload (no ION redirect in the shim), wrapper test gl/ under gamescope with the window held 90 s: the GL window is visible and colour-cycling where the shipped daemon showed black. 29 foreign buffers, all copied: the game's 1280x800 AR24 pitch 5120 window buffers and two 2560x1600 screen-size buffers (XR24 then AR24, pitch 10240), summary: copied_frames=2466 buffers=28 failures=0, no DROPPED, no copy FAILED. Pixels clean, so these ION buffers are linear. Runner reported first frames at 23 fps on a cold first boot; the shim heartbeat ran at ~54 commits/s with 0 failures.
Known limits: tiled or multi-plane buffers are not copied (they log copy FAILED and stay dropped, as today); the copy is CPU-side, so it costs memory bandwidth whenever it engages, which is why #26 keeps the zero-copy path for the known ION case.
Build: cargo clippy clean for aarch64-linux-android and on the host; cargo test -p gndrm-daemon -p gndrm-android all pass (5 new unit tests; the golden suite gains a test that the presenter receives the committed buffer's dma-buf with the right inode, including after GEM_CLOSE). Daemon sha256 574829b4bbbd35bae78f82410c77c23f5d8161358d9ab9684373d4bfdd282f1a (NDK 27.3). The daemon ships in the app's jniLibs, so landing this needs an app PR as well.