Skip to content

Commit 8cae22c

Browse files
MagicalTuxclaude
andcommitted
forma-platform(x11): DRI3 PixmapFromBuffers + Present encoders + negotiation
The wire building blocks for the zero-copy on-GPU present (compositor "Next"): wrap a rendered dma-buf as an X Pixmap via DRI3 PixmapFromBuffers, then flip it to the window with Present PresentPixmap — no readback. - pixmap_from_buffers_request: DRI3 minor 7 encoder over a DmabufImage (geometry + format + up to 4 planes' stride/offset + the DRM format modifier the import must echo). The plane fds ride alongside as SCM_RIGHTS ancillary data (scm::send_with_fds). - present_pixmap_request: Present minor 1 encoder (regions/crtc/fences None, a plain immediate present; serial + options pass through). - present_query_version + present_probe: negotiate the Present extension. Present needs no GPU, so this is CI-verifiable under Xvfb — the half of the present path that doesn't require DRM. dri3probe now runs it and a dri3-present CI job asserts "Present X.Y available" under Xvfb. Both encoders are unit-tested for exact wire layout (16-word PixmapFromBuffers, 18-word PresentPixmap). The runtime composition (render -> export dma-buf descriptor -> PixmapFromBuffers -> PresentPixmap on the window) lands once forma-gpu exports a scene's dma-buf descriptor; both that import and the flip need real GPU hardware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e1df6f0 commit 8cae22c

3 files changed

Lines changed: 223 additions & 0 deletions

File tree

.github/workflows/visual.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,34 @@ jobs:
671671
exit 1
672672
fi
673673
674+
# Present extension negotiation over the raw X11 socket. The Present extension
675+
# (which flips the DRI3 pixmap to the window) needs no GPU, so dri3probe can
676+
# negotiate it under Xvfb — the CI-verifiable half of the zero-copy GPU-present
677+
# path (the dma-buf Pixmap it flips still needs real GPU hardware). DRI3 itself
678+
# is absent under Xvfb (no DRM), so the probe exits non-zero; we assert Present.
679+
dri3-present:
680+
name: DRI3 Present (X11 negotiation / Xvfb)
681+
runs-on: ubuntu-latest
682+
steps:
683+
- uses: actions/checkout@v6
684+
- uses: dtolnay/rust-toolchain@stable
685+
- uses: Swatinem/rust-cache@v2
686+
- name: Install Xvfb
687+
run: sudo apt-get update && sudo apt-get install -y xvfb
688+
- name: Build dri3probe
689+
run: cargo build --release -p dri3probe
690+
- name: Negotiate the Present extension under Xvfb
691+
run: |
692+
Xvfb :99 -screen 0 1024x768x24 &
693+
XVFB_PID=$!
694+
sleep 2
695+
set +e
696+
DISPLAY=:99 ./target/release/dri3probe | tee dri3.log
697+
set -e
698+
kill "$XVFB_PID" 2>/dev/null || true
699+
# The Present extension must negotiate (Xvfb supports Present, no GPU).
700+
grep -qE "Present probe: Present [0-9]+\.[0-9]+ available" dri3.log
701+
674702
# Render the window example under a headless wlroots compositor (sway) and
675703
# screenshot it with grim, proving the hand-authored Wayland backend connects,
676704
# binds the globals, creates an xdg-shell window, and presents via wl_shm.

crates/forma-platform/src/backend/x11.rs

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,138 @@ unsafe extern "C" {
450450
fn libc_close_fd(fd: RawFd) -> i32;
451451
}
452452

453+
// ---- DRI3 PixmapFromBuffers + Present: flip a GPU frame with no readback -----
454+
//
455+
// The last hop of the zero-copy path: wrap a rendered dma-buf as an X Pixmap via
456+
// DRI3 `PixmapFromBuffers` (the plane fds sent over this socket with SCM_RIGHTS,
457+
// echoing the format modifier so tiled buffers import correctly), then flip that
458+
// Pixmap to the window with the Present extension's `PresentPixmap`. The
459+
// `PixmapFromBuffers` step needs a real GPU (the server imports the dma-buf), so
460+
// it's hardware-gated; the Present extension itself needs no GPU (it works under
461+
// Xvfb), so its negotiation is CI-verifiable. The wire encoders below are
462+
// unit-tested regardless.
463+
464+
const DRI3_PIXMAP_FROM_BUFFERS: u8 = 7;
465+
const PRESENT_QUERY_VERSION: u8 = 0;
466+
const PRESENT_PIXMAP: u8 = 1;
467+
468+
/// A rendered dma-buf to wrap as an X Pixmap via DRI3 `PixmapFromBuffers`. Holds
469+
/// the geometry + format the server needs to interpret the (1..=4) planes; the
470+
/// plane fds are passed separately as SCM_RIGHTS ancillary data.
471+
#[derive(Clone, Debug, PartialEq, Eq)]
472+
pub struct DmabufImage {
473+
pub width: u16,
474+
pub height: u16,
475+
/// Pixmap depth (e.g. 24 for XRGB8888, 32 for ARGB8888).
476+
pub depth: u8,
477+
/// Bits per pixel (e.g. 32).
478+
pub bpp: u8,
479+
/// DRM format modifier — must echo the export's modifier (e.g. from
480+
/// `EGL_DMA_BUF_PLANE0_MODIFIER_*`) or a tiled buffer imports as garbage.
481+
pub modifier: u64,
482+
/// Per-plane `(stride, offset)` in bytes; 1..=4 planes, parallel to the fds.
483+
pub planes: Vec<(u32, u32)>,
484+
}
485+
486+
/// Build a DRI3 `PixmapFromBuffers` request (minor 7): wrap the dma-buf planes
487+
/// of `img` (whose fds are sent alongside as ancillary data via
488+
/// [`crate::scm::send_with_fds`]) as the X resource `pixmap`, in `window`'s
489+
/// screen. Absent planes (of the 4 wire slots) are zero-filled.
490+
pub fn pixmap_from_buffers_request(
491+
major: u8,
492+
pixmap: u32,
493+
window: u32,
494+
img: &DmabufImage,
495+
) -> Vec<u8> {
496+
let mut req = vec![major, DRI3_PIXMAP_FROM_BUFFERS, 0, 0];
497+
req.extend_from_slice(&pixmap.to_le_bytes());
498+
req.extend_from_slice(&window.to_le_bytes());
499+
req.push(img.planes.len() as u8);
500+
req.extend_from_slice(&[0u8; 3]); // pad
501+
req.extend_from_slice(&img.width.to_le_bytes());
502+
req.extend_from_slice(&img.height.to_le_bytes());
503+
// stride0,offset0 .. stride3,offset3 (zero-filled for absent planes).
504+
for i in 0..4 {
505+
let (stride, offset) = img.planes.get(i).copied().unwrap_or((0, 0));
506+
req.extend_from_slice(&stride.to_le_bytes());
507+
req.extend_from_slice(&offset.to_le_bytes());
508+
}
509+
req.push(img.depth);
510+
req.push(img.bpp);
511+
req.extend_from_slice(&[0u8; 2]); // pad
512+
req.extend_from_slice(&img.modifier.to_le_bytes());
513+
finish(req)
514+
}
515+
516+
/// Build a Present `PresentPixmap` request (minor 1): present `pixmap` to
517+
/// `window`. Regions, target CRTC, and sync fences are left as None (0) and the
518+
/// target MSC/divisor/remainder as 0 — a plain immediate present; `serial` and
519+
/// `options` pass through. No notify list.
520+
pub fn present_pixmap_request(
521+
major: u8,
522+
window: u32,
523+
pixmap: u32,
524+
serial: u32,
525+
options: u32,
526+
) -> Vec<u8> {
527+
let mut req = vec![major, PRESENT_PIXMAP, 0, 0];
528+
req.extend_from_slice(&window.to_le_bytes());
529+
req.extend_from_slice(&pixmap.to_le_bytes());
530+
req.extend_from_slice(&serial.to_le_bytes());
531+
req.extend_from_slice(&0u32.to_le_bytes()); // valid region (None)
532+
req.extend_from_slice(&0u32.to_le_bytes()); // update region (None)
533+
req.extend_from_slice(&0i16.to_le_bytes()); // x-off
534+
req.extend_from_slice(&0i16.to_le_bytes()); // y-off
535+
req.extend_from_slice(&0u32.to_le_bytes()); // target CRTC (None)
536+
req.extend_from_slice(&0u32.to_le_bytes()); // wait fence (None)
537+
req.extend_from_slice(&0u32.to_le_bytes()); // idle fence (None)
538+
req.extend_from_slice(&options.to_le_bytes());
539+
req.extend_from_slice(&0u32.to_le_bytes()); // unused
540+
req.extend_from_slice(&0u64.to_le_bytes()); // target MSC
541+
req.extend_from_slice(&0u64.to_le_bytes()); // divisor
542+
req.extend_from_slice(&0u64.to_le_bytes()); // remainder
543+
finish(req)
544+
}
545+
546+
/// Negotiate the Present extension, returning the server's `(major, minor)`
547+
/// version. `None` if the server lacks Present. Works without a GPU.
548+
fn present_query_version(conn: &mut Conn) -> io::Result<Option<(u32, u32)>> {
549+
let Some(major) = query_extension(conn, b"Present")? else {
550+
return Ok(None);
551+
};
552+
let mut req = vec![major, PRESENT_QUERY_VERSION, 0, 0];
553+
req.extend_from_slice(&1u32.to_le_bytes()); // client major
554+
req.extend_from_slice(&2u32.to_le_bytes()); // client minor
555+
conn.send(&finish(req))?;
556+
let mut reply = [0u8; 32];
557+
conn.stream.read_exact(&mut reply)?;
558+
if reply[0] != 1 {
559+
return Ok(None);
560+
}
561+
let smajor = u32::from_le_bytes([reply[8], reply[9], reply[10], reply[11]]);
562+
let sminor = u32::from_le_bytes([reply[12], reply[13], reply[14], reply[15]]);
563+
Ok(Some((smajor, sminor)))
564+
}
565+
566+
// The runtime composition that flips a GPU frame to a window with no readback —
567+
// query DRI3 + Present, `conn.new_id()` for the pixmap,
568+
// `scm::send_with_fds(pixmap_from_buffers_request(..), plane_fds)`, then
569+
// `present_pixmap_request(..)` — lives in the windowed compositor once
570+
// `forma-gpu` exports a scene's dma-buf descriptor (fds + per-plane
571+
// stride/offset + modifier). The wire encoders above are that path's tested
572+
// building blocks; both DRI3 import and Present flip need real GPU hardware.
573+
574+
/// Connect and negotiate the Present extension, returning a one-line status.
575+
/// Present needs no GPU (Xvfb supports it), so this is CI-verifiable — it proves
576+
/// the half of the zero-copy present path that doesn't require DRM hardware.
577+
pub fn present_probe() -> Result<String, PlatformError> {
578+
let mut conn = Conn::connect()?;
579+
match present_query_version(&mut conn).map_err(os)? {
580+
Some((maj, min)) => Ok(format!("Present {maj}.{min} available")),
581+
None => Ok("Present unavailable on this server".to_string()),
582+
}
583+
}
584+
453585
/// Set up MIT-SHM: create a shared segment of `capacity` bytes, attach it to the
454586
/// server, and confirm the attach succeeded. Returns `None` (with everything
455587
/// cleaned up) if the extension is absent or any step fails — the caller then
@@ -1383,6 +1515,61 @@ mod tests {
13831515
assert_eq!(req.len(), 12);
13841516
}
13851517

1518+
#[test]
1519+
fn pixmap_from_buffers_request_is_well_formed() {
1520+
// A single-plane ARGB8888 4096×2160 buffer with a tiled modifier.
1521+
let img = DmabufImage {
1522+
width: 4096,
1523+
height: 2160,
1524+
depth: 24,
1525+
bpp: 32,
1526+
modifier: 0x0100_0000_0000_0001, // an arbitrary 64-bit DRM modifier
1527+
planes: vec![(16384, 0)],
1528+
};
1529+
let req = pixmap_from_buffers_request(0x90, 0x0040_0000, 0x0020_0001, &img);
1530+
assert_eq!(req[0], 0x90);
1531+
assert_eq!(req[1], DRI3_PIXMAP_FROM_BUFFERS);
1532+
// Fixed wire size: header(4) + pixmap(4) + window(4) + num/pad/w/h(8)
1533+
// + 8 plane words(32) + depth/bpp/pad(4) + modifier(8) = 64 bytes / 16 words.
1534+
assert_eq!(u16::from_le_bytes([req[2], req[3]]), 16);
1535+
assert_eq!(req.len(), 64);
1536+
assert_eq!(rd_u32(&req, 4).unwrap(), 0x0040_0000); // pixmap
1537+
assert_eq!(rd_u32(&req, 8).unwrap(), 0x0020_0001); // window
1538+
assert_eq!(req[12], 1); // num_buffers
1539+
assert_eq!(u16::from_le_bytes([req[16], req[17]]), 4096); // width
1540+
assert_eq!(u16::from_le_bytes([req[18], req[19]]), 2160); // height
1541+
assert_eq!(rd_u32(&req, 20).unwrap(), 16384); // stride0
1542+
assert_eq!(rd_u32(&req, 24).unwrap(), 0); // offset0
1543+
// Planes 1..3 are zero-filled (stride1@28 .. offset3@48).
1544+
assert_eq!(rd_u32(&req, 28).unwrap(), 0);
1545+
assert_eq!(rd_u32(&req, 48).unwrap(), 0);
1546+
assert_eq!(req[52], 24); // depth
1547+
assert_eq!(req[53], 32); // bpp
1548+
// Modifier is the last 8 bytes (little-endian u64).
1549+
let modifier = u64::from_le_bytes(req[56..64].try_into().unwrap());
1550+
assert_eq!(modifier, 0x0100_0000_0000_0001);
1551+
}
1552+
1553+
#[test]
1554+
fn present_pixmap_request_is_well_formed() {
1555+
let req = present_pixmap_request(0x91, 0x0020_0001, 0x0040_0000, 7, 0);
1556+
assert_eq!(req[0], 0x91);
1557+
assert_eq!(req[1], PRESENT_PIXMAP);
1558+
// 4-byte header + window+pixmap+serial+valid+update (5 words) + off word
1559+
// + crtc+wait+idle+options+unused (5 words) + msc/divisor/remainder
1560+
// (6 words) = 18 words.
1561+
assert_eq!(u16::from_le_bytes([req[2], req[3]]), 18);
1562+
assert_eq!(req.len(), 72);
1563+
assert_eq!(rd_u32(&req, 4).unwrap(), 0x0020_0001); // window
1564+
assert_eq!(rd_u32(&req, 8).unwrap(), 0x0040_0000); // pixmap
1565+
assert_eq!(rd_u32(&req, 12).unwrap(), 7); // serial
1566+
assert_eq!(rd_u32(&req, 16).unwrap(), 0); // valid region (None)
1567+
assert_eq!(rd_u32(&req, 20).unwrap(), 0); // update region (None)
1568+
// target-msc/divisor/remainder are the final three zero u64s.
1569+
assert_eq!(u64::from_le_bytes(req[48..56].try_into().unwrap()), 0);
1570+
assert_eq!(u64::from_le_bytes(req[64..72].try_into().unwrap()), 0);
1571+
}
1572+
13861573
#[test]
13871574
fn selection_notify_send_event_is_well_formed() {
13881575
// A SendEvent carrying a SelectionNotify is 44 bytes (11 words): 12-byte

examples/dri3probe/src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ use std::process::ExitCode;
1515
fn main() -> ExitCode {
1616
#[cfg(target_os = "linux")]
1717
{
18+
// The Present extension (which flips the DRI3 pixmap to the window) needs
19+
// no GPU, so this negotiation succeeds even under Xvfb — the CI-verifiable
20+
// half of the zero-copy present path.
21+
match forma_platform::backend::x11::present_probe() {
22+
Ok(s) => println!("Present probe: {s}"),
23+
Err(e) => println!("Present probe error: {e}"),
24+
}
25+
1826
let fd = match forma_platform::backend::x11::dri3_open_drm_fd() {
1927
Ok(Some(fd)) => {
2028
println!("DRI3Open ok: DRM device fd = {fd}");

0 commit comments

Comments
 (0)