detached c595af8

feat(dmabuf): offer a scanout tranche to fullscreen surfaces from the primary plane's IN_FORMATS

Each DRM output reports, after every modeset, the formats its primary plane scans out that the renderer also composes. Surface feedback puts them first in a scanout tranche while the surface's window is fullscreen on that output, and is sent again on fullscreen or output changes. Direct scanout accepts any format and modifier the plane lists (alpha formats as their opaque variant) and imports every plane of a buffer.
commit
c595af8155575709082f5e6d9fe9964c99a24782
author
brice · · 2026-09-26 11:45 UTC
parent
717ee76
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +466 -48
M internal/adapters/drm/card.go +18 -0
@@ -8,6 +8,7 @@ import (
88 "sync"
99 "sync/atomic"
1010
11+ "github.com/bnema/nefertty/internal/ports"
1112 "github.com/bnema/zerowrap"
1213 "golang.org/x/sys/unix"
1314 )
@@ -32,6 +33,9 @@ type Card struct {
3233 async bool
3334 // serials numbers the commits of every output (event userData).
3435 serials atomic.Uint64
36+ // formats receives each output's direct scanout formats (nil: none
37+ // sent); set before the first Scan.
38+ formats chan<- ports.OutputFormats
3539 }
3640
3741 // OpenCard turns on atomic modesetting and reads the card's CRTCs. fd
@@ -55,6 +59,20 @@ func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error
5559 // Path is the device path, e.g. /dev/dri/card1.
5660 func (c *Card) Path() string { return c.path }
5761
62+// Device is the card's dev_t, the KMS device clients allocate scanout
63+// buffers for (0 when unknown).
64+func (c *Card) Device() uint64 {
65+ var st unix.Stat_t
66+ if unix.Fstat(c.fd, &st) != nil {
67+ return 0
68+ }
69+ return st.Rdev
70+}
71+
72+// SetFormats sets where outputs report their direct scanout formats.
73+// Call before the first Scan.
74+func (c *Card) SetFormats(ch chan<- ports.OutputFormats) { c.formats = ch }
75+
5876 // SetWant replaces the connector config; the next Scan applies it.
5977 func (c *Card) SetWant(w Want) { c.want = w }
6078
@@ -8,6 +8,7 @@ import (
8 "sync"8 "sync"
9 "sync/atomic"9 "sync/atomic"
10 10
11+ "github.com/bnema/nefertty/internal/ports"
11 "github.com/bnema/zerowrap"12 "github.com/bnema/zerowrap"
12 "golang.org/x/sys/unix"13 "golang.org/x/sys/unix"
13 )14 )
@@ -32,6 +33,9 @@ type Card struct {
32 async bool33 async bool
33 // serials numbers the commits of every output (event userData).34 // serials numbers the commits of every output (event userData).
34 serials atomic.Uint6435 serials atomic.Uint64
36+ // formats receives each output's direct scanout formats (nil: none
37+ // sent); set before the first Scan.
38+ formats chan<- ports.OutputFormats
35 }39 }
36 40
37 // OpenCard turns on atomic modesetting and reads the card's CRTCs. fd41 // OpenCard turns on atomic modesetting and reads the card's CRTCs. fd
@@ -55,6 +59,20 @@ func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error
55 // Path is the device path, e.g. /dev/dri/card1.59 // Path is the device path, e.g. /dev/dri/card1.
56 func (c *Card) Path() string { return c.path }60 func (c *Card) Path() string { return c.path }
57 61
62+// Device is the card's dev_t, the KMS device clients allocate scanout
63+// buffers for (0 when unknown).
64+func (c *Card) Device() uint64 {
65+ var st unix.Stat_t
66+ if unix.Fstat(c.fd, &st) != nil {
67+ return 0
68+ }
69+ return st.Rdev
70+}
71+
72+// SetFormats sets where outputs report their direct scanout formats.
73+// Call before the first Scan.
74+func (c *Card) SetFormats(ch chan<- ports.OutputFormats) { c.formats = ch }
75+
58 // SetWant replaces the connector config; the next Scan applies it.76 // SetWant replaces the connector config; the next Scan applies it.
59 func (c *Card) SetWant(w Want) { c.want = w }77 func (c *Card) SetWant(w Want) { c.want = w }
60 78
M internal/adapters/drm/kms.go +6 -0
@@ -6,6 +6,7 @@ import (
66 "math"
77 "unsafe"
88
9+ "github.com/bnema/nefertty/internal/ports"
910 "golang.org/x/sys/unix"
1011 )
1112
@@ -99,6 +100,11 @@ type Want struct {
99100 NoTearing bool
100101 // NoVRR keeps variable refresh off (render.vrr = off).
101102 NoVRR bool
103+ // Sampled are the dmabuf formats the renderer composes: direct
104+ // scanout is offered only for those, so a refused buffer still draws.
105+ Sampled []ports.DMABufFormat
106+ // Device is the KMS device (dev_t) offered in scanout tranches.
107+ Device uint64
102108 }
103109
104110 // usable reports whether a connector should be driven.
@@ -6,6 +6,7 @@ import (
6 "math"6 "math"
7 "unsafe"7 "unsafe"
8 8
9+ "github.com/bnema/nefertty/internal/ports"
9 "golang.org/x/sys/unix"10 "golang.org/x/sys/unix"
10 )11 )
11 12
@@ -99,6 +100,11 @@ type Want struct {
99 NoTearing bool100 NoTearing bool
100 // NoVRR keeps variable refresh off (render.vrr = off).101 // NoVRR keeps variable refresh off (render.vrr = off).
101 NoVRR bool102 NoVRR bool
103+ // Sampled are the dmabuf formats the renderer composes: direct
104+ // scanout is offered only for those, so a refused buffer still draws.
105+ Sampled []ports.DMABufFormat
106+ // Device is the KMS device (dev_t) offered in scanout tranches.
107+ Device uint64
102 }108 }
103 109
104 // usable reports whether a connector should be driven.110 // usable reports whether a connector should be driven.
M internal/adapters/drm/output.go +26 -1
@@ -76,6 +76,11 @@ type Output struct {
7676 composedSince time.Time
7777 // kind is how the images were made.
7878 kind imageKind
79+ // formats receives the direct scanout formats after each modeset;
80+ // sampled and device are what they are built from (Want).
81+ formats chan<- ports.OutputFormats
82+ sampled []ports.DMABufFormat
83+ device uint64
7984 }
8085
8186 // pendingFrame is what the pending frame commit shows.
@@ -115,7 +120,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu
115120 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
116121 log := card.log
117122 pipe := slices.Index(card.crtcs, crtc)
118- o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
123+ o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
119124 var err error
120125 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
121126 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
@@ -267,9 +272,29 @@ func (o *Output) modeset() error {
267272 }
268273 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
269274 o.testCursor()
275+ o.sendFormats()
270276 return nil
271277 }
272278
279+// sendFormats reports the formats clients may allocate for direct
280+// scanout on this output (none when scanout is off). It never blocks:
281+// wayland keeps the latest per output, a dropped report is resent at the
282+// next modeset.
283+func (o *Output) sendFormats() {
284+ if o.formats == nil {
285+ return
286+ }
287+ f := ports.OutputFormats{Output: o.conn.name, Device: o.device}
288+ if o.scanout {
289+ f.Formats = o.scanoutFormats(o.sampled)
290+ }
291+ select {
292+ case o.formats <- f:
293+ default:
294+ o.log.Warn().Str("connector", o.conn.name).Msg("scanout formats not sent: wayland busy")
295+ }
296+}
297+
273298 // testModeset asks KMS whether it would take the modeset of the output
274299 // images without applying it.
275300 func (o *Output) testModeset() error {
@@ -76,6 +76,11 @@ type Output struct {
76 composedSince time.Time76 composedSince time.Time
77 // kind is how the images were made.77 // kind is how the images were made.
78 kind imageKind78 kind imageKind
79+ // formats receives the direct scanout formats after each modeset;
80+ // sampled and device are what they are built from (Want).
81+ formats chan<- ports.OutputFormats
82+ sampled []ports.DMABufFormat
83+ device uint64
79 }84 }
80 85
81 // pendingFrame is what the pending frame commit shows.86 // pendingFrame is what the pending frame commit shows.
@@ -115,7 +120,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu
115 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {120 func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
116 log := card.log121 log := card.log
117 pipe := slices.Index(card.crtcs, crtc)122 pipe := slices.Index(card.crtcs, crtc)
118- o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}123+ o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
119 var err error124 var err error
120 if o.saved, err = getCrtc(card.fd, crtc); err != nil {125 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
121 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")126 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
@@ -267,9 +272,29 @@ func (o *Output) modeset() error {
267 }272 }
268 o.log.Info().Str("connector", o.conn.name).Msg("modeset")273 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
269 o.testCursor()274 o.testCursor()
275+ o.sendFormats()
270 return nil276 return nil
271 }277 }
272 278
279+// sendFormats reports the formats clients may allocate for direct
280+// scanout on this output (none when scanout is off). It never blocks:
281+// wayland keeps the latest per output, a dropped report is resent at the
282+// next modeset.
283+func (o *Output) sendFormats() {
284+ if o.formats == nil {
285+ return
286+ }
287+ f := ports.OutputFormats{Output: o.conn.name, Device: o.device}
288+ if o.scanout {
289+ f.Formats = o.scanoutFormats(o.sampled)
290+ }
291+ select {
292+ case o.formats <- f:
293+ default:
294+ o.log.Warn().Str("connector", o.conn.name).Msg("scanout formats not sent: wayland busy")
295+ }
296+}
297+
273 // testModeset asks KMS whether it would take the modeset of the output298 // testModeset asks KMS whether it would take the modeset of the output
274 // images without applying it.299 // images without applying it.
275 func (o *Output) testModeset() error {300 func (o *Output) testModeset() error {
M internal/adapters/drm/scanout.go +97 -18
@@ -2,6 +2,8 @@ package drm
22
33 import (
44 "errors"
5+ "os"
6+ "slices"
57 "time"
68 "unsafe"
79
@@ -42,6 +44,7 @@ type fbCmd2 struct {
4244
4345 // scanoutCandidate returns the window content the output can scan out
4446 // directly, or a reason why it cannot. w, h are the output's physical size.
47+// The plane's formats are checked by scanoutFB.
4548 func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) {
4649 var full *ports.SceneWindow
4750 for i := range s.Windows {
@@ -69,14 +72,10 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
6972 return c, "not_dmabuf"
7073 case len(c.Children) > 0:
7174 return c, "subsurfaces"
72- case len(c.DMABuf.Planes) != 1:
73- return c, "multi_plane"
7475 case c.Width != w || c.Height != h:
7576 return c, "size_mismatch"
7677 case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H:
7778 return c, "logical_mismatch"
78- case !scanoutable(c.DMABuf.Format):
79- return c, "format"
8079 case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH):
8180 return c, "geometry_crop"
8281 }
@@ -102,7 +101,7 @@ func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
102101 }
103102 fb := &clientFB{last: now}
104103 o.clientFBs[b.ID] = fb
105- format, ok := scanoutFormat(b.Format)
104+ format, ok := o.scanoutFormat(b)
106105 if !ok {
107106 fb.failed = "format"
108107 return 0, fb.failed
@@ -117,20 +116,57 @@ func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
117116 return id, ""
118117 }
119118
120-// scanoutFormat is the framebuffer format for a client format. The primary
121-// plane shows no alpha: alpha formats are scanned out as their X variant,
122-// others are refused.
123-func scanoutFormat(f uint32) (uint32, bool) {
124- switch f {
125- case fourccXRGB, fourccARGB:
126- return fourccXRGB, true
119+// scanoutFormat is the framebuffer format a client buffer is scanned out
120+// as, when the primary plane lists it with the buffer's modifier. The
121+// primary plane shows no alpha: an alpha format goes as its X variant
122+// when the plane has that one.
123+func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) {
124+ has := func(f uint32) bool {
125+ return slices.Contains(o.primary.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier})
127126 }
128- return 0, false
127+ if x, ok := opaqueVariant[b.Format]; ok && has(x) {
128+ return x, true
129+ }
130+ return b.Format, has(b.Format)
131+}
132+
133+// opaqueVariant maps alpha formats to the same layout without alpha.
134+var opaqueVariant = map[uint32]uint32{
135+ fourccARGB: fourccXRGB,
136+ fourccABGR: fourccXBGR,
137+ fourccAR30: fourccXR30,
138+ fourccAB30: fourccXB30,
139+ fourccAR4H: fourccXR4H,
140+ fourccAB4H: fourccXB4H,
141+}
142+
143+// scanoutFormats are the formats a buffer can be scanned out with: in the
144+// primary plane's IN_FORMATS (directly or as its opaque variant) and
145+// composable by the renderer.
146+func (o *Output) scanoutFormats(sampled []ports.DMABufFormat) []ports.DMABufFormat {
147+ var out []ports.DMABufFormat
148+ for _, f := range sampled {
149+ b := ports.DMABuf{Format: f.Format, Modifier: f.Modifier}
150+ if _, ok := o.scanoutFormat(&b); ok {
151+ out = append(out, f)
152+ }
153+ }
154+ return out
129155 }
130156
131157 const (
132158 fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
133159 fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
160+ fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
161+ fourccABGR = 'A' | 'B'<<8 | '2'<<16 | '4'<<24
162+ fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
163+ fourccAR30 = 'A' | 'R'<<8 | '3'<<16 | '0'<<24
164+ fourccXB30 = 'X' | 'B'<<8 | '3'<<16 | '0'<<24
165+ fourccAB30 = 'A' | 'B'<<8 | '3'<<16 | '0'<<24
166+ fourccXR4H = 'X' | 'R'<<8 | '4'<<16 | 'H'<<24
167+ fourccAR4H = 'A' | 'R'<<8 | '4'<<16 | 'H'<<24
168+ fourccXB4H = 'X' | 'B'<<8 | '4'<<16 | 'H'<<24
169+ fourccAB4H = 'A' | 'B'<<8 | '4'<<16 | 'H'<<24
134170 )
135171
136172 // addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's
@@ -161,9 +197,28 @@ func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
161197 _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
162198 }()
163199 cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format}
164- cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = p.handle, b.Planes[0].Stride, b.Planes[0].Offset
165- if b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers) {
166- cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier
200+ useMod := b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers)
201+ if useMod {
202+ cmd.flags = fbModifiers
203+ }
204+ // Every plane of a buffer lives in the same dmabuf or its own: each
205+ // gets its GEM handle (one import per distinct file).
206+ for i, pl := range b.Planes {
207+ if i >= len(cmd.handles) {
208+ break
209+ }
210+ h := p.handle
211+ if i > 0 {
212+ var err error
213+ if h, err = k.primeImport(pl.File); err != nil {
214+ return 0, err
215+ }
216+ defer k.gemClose(h)
217+ }
218+ cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset
219+ if useMod {
220+ cmd.modifiers[i] = b.Modifier
221+ }
167222 }
168223 if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
169224 return 0, errors.Join(errors.New("add fb2"), err)
@@ -171,6 +226,32 @@ func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
171226 return cmd.fbID, nil
172227 }
173228
229+// primeImport returns the GEM handle of a dmabuf file. The caller holds
230+// gemMu and closes the handle.
231+func (k kmsDevice) primeImport(f *os.File) (uint32, error) {
232+ raw, err := f.SyscallConn()
233+ if err != nil {
234+ return 0, err
235+ }
236+ var p primeHandle
237+ var ioErr error
238+ if err := raw.Control(func(fd uintptr) {
239+ p.fd = int32(fd)
240+ ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
241+ }); err != nil {
242+ return 0, err
243+ }
244+ if ioErr != nil {
245+ return 0, errors.Join(errors.New("prime import"), ioErr)
246+ }
247+ return p.handle, nil
248+}
249+
250+func (k kmsDevice) gemClose(h uint32) {
251+ g := gemClose{handle: h}
252+ _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
253+}
254+
174255 // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all
175256 // of them, except those on screen or queued.
176257 func (o *Output) dropClientFBs(now time.Time, all bool) {
@@ -187,5 +268,3 @@ func (o *Output) dropClientFBs(now time.Time, all bool) {
187268 delete(o.clientFBs, id)
188269 }
189270 }
190-
191-func scanoutable(f uint32) bool { _, ok := scanoutFormat(f); return ok }
@@ -2,6 +2,8 @@ package drm
2 2
3 import (3 import (
4 "errors"4 "errors"
5+ "os"
6+ "slices"
5 "time"7 "time"
6 "unsafe"8 "unsafe"
7 9
@@ -42,6 +44,7 @@ type fbCmd2 struct {
42 44
43 // scanoutCandidate returns the window content the output can scan out45 // scanoutCandidate returns the window content the output can scan out
44 // directly, or a reason why it cannot. w, h are the output's physical size.46 // directly, or a reason why it cannot. w, h are the output's physical size.
47+// The plane's formats are checked by scanoutFB.
45 func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) {48 func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) {
46 var full *ports.SceneWindow49 var full *ports.SceneWindow
47 for i := range s.Windows {50 for i := range s.Windows {
@@ -69,14 +72,10 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
69 return c, "not_dmabuf"72 return c, "not_dmabuf"
70 case len(c.Children) > 0:73 case len(c.Children) > 0:
71 return c, "subsurfaces"74 return c, "subsurfaces"
72- case len(c.DMABuf.Planes) != 1:
73- return c, "multi_plane"
74 case c.Width != w || c.Height != h:75 case c.Width != w || c.Height != h:
75 return c, "size_mismatch"76 return c, "size_mismatch"
76 case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H:77 case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H:
77 return c, "logical_mismatch"78 return c, "logical_mismatch"
78- case !scanoutable(c.DMABuf.Format):
79- return c, "format"
80 case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH):79 case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH):
81 return c, "geometry_crop"80 return c, "geometry_crop"
82 }81 }
@@ -102,7 +101,7 @@ func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
102 }101 }
103 fb := &clientFB{last: now}102 fb := &clientFB{last: now}
104 o.clientFBs[b.ID] = fb103 o.clientFBs[b.ID] = fb
105- format, ok := scanoutFormat(b.Format)104+ format, ok := o.scanoutFormat(b)
106 if !ok {105 if !ok {
107 fb.failed = "format"106 fb.failed = "format"
108 return 0, fb.failed107 return 0, fb.failed
@@ -117,20 +116,57 @@ func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
117 return id, ""116 return id, ""
118 }117 }
119 118
120-// scanoutFormat is the framebuffer format for a client format. The primary119+// scanoutFormat is the framebuffer format a client buffer is scanned out
121-// plane shows no alpha: alpha formats are scanned out as their X variant,120+// as, when the primary plane lists it with the buffer's modifier. The
122-// others are refused.121+// primary plane shows no alpha: an alpha format goes as its X variant
123-func scanoutFormat(f uint32) (uint32, bool) {122+// when the plane has that one.
124- switch f {123+func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) {
125- case fourccXRGB, fourccARGB:124+ has := func(f uint32) bool {
126- return fourccXRGB, true125+ return slices.Contains(o.primary.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier})
127 }126 }
128- return 0, false127+ if x, ok := opaqueVariant[b.Format]; ok && has(x) {
128+ return x, true
129+ }
130+ return b.Format, has(b.Format)
131+}
132+
133+// opaqueVariant maps alpha formats to the same layout without alpha.
134+var opaqueVariant = map[uint32]uint32{
135+ fourccARGB: fourccXRGB,
136+ fourccABGR: fourccXBGR,
137+ fourccAR30: fourccXR30,
138+ fourccAB30: fourccXB30,
139+ fourccAR4H: fourccXR4H,
140+ fourccAB4H: fourccXB4H,
141+}
142+
143+// scanoutFormats are the formats a buffer can be scanned out with: in the
144+// primary plane's IN_FORMATS (directly or as its opaque variant) and
145+// composable by the renderer.
146+func (o *Output) scanoutFormats(sampled []ports.DMABufFormat) []ports.DMABufFormat {
147+ var out []ports.DMABufFormat
148+ for _, f := range sampled {
149+ b := ports.DMABuf{Format: f.Format, Modifier: f.Modifier}
150+ if _, ok := o.scanoutFormat(&b); ok {
151+ out = append(out, f)
152+ }
153+ }
154+ return out
129 }155 }
130 156
131 const (157 const (
132 fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24158 fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
133 fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24159 fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
160+ fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
161+ fourccABGR = 'A' | 'B'<<8 | '2'<<16 | '4'<<24
162+ fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
163+ fourccAR30 = 'A' | 'R'<<8 | '3'<<16 | '0'<<24
164+ fourccXB30 = 'X' | 'B'<<8 | '3'<<16 | '0'<<24
165+ fourccAB30 = 'A' | 'B'<<8 | '3'<<16 | '0'<<24
166+ fourccXR4H = 'X' | 'R'<<8 | '4'<<16 | 'H'<<24
167+ fourccAR4H = 'A' | 'R'<<8 | '4'<<16 | 'H'<<24
168+ fourccXB4H = 'X' | 'B'<<8 | '4'<<16 | 'H'<<24
169+ fourccAB4H = 'A' | 'B'<<8 | '4'<<16 | 'H'<<24
134 )170 )
135 171
136 // addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's172 // addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's
@@ -161,9 +197,28 @@ func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
161 _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))197 _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
162 }()198 }()
163 cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format}199 cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format}
164- cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = p.handle, b.Planes[0].Stride, b.Planes[0].Offset200+ useMod := b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers)
165- if b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers) {201+ if useMod {
166- cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier202+ cmd.flags = fbModifiers
203+ }
204+ // Every plane of a buffer lives in the same dmabuf or its own: each
205+ // gets its GEM handle (one import per distinct file).
206+ for i, pl := range b.Planes {
207+ if i >= len(cmd.handles) {
208+ break
209+ }
210+ h := p.handle
211+ if i > 0 {
212+ var err error
213+ if h, err = k.primeImport(pl.File); err != nil {
214+ return 0, err
215+ }
216+ defer k.gemClose(h)
217+ }
218+ cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset
219+ if useMod {
220+ cmd.modifiers[i] = b.Modifier
221+ }
167 }222 }
168 if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {223 if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
169 return 0, errors.Join(errors.New("add fb2"), err)224 return 0, errors.Join(errors.New("add fb2"), err)
@@ -171,6 +226,32 @@ func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
171 return cmd.fbID, nil226 return cmd.fbID, nil
172 }227 }
173 228
229+// primeImport returns the GEM handle of a dmabuf file. The caller holds
230+// gemMu and closes the handle.
231+func (k kmsDevice) primeImport(f *os.File) (uint32, error) {
232+ raw, err := f.SyscallConn()
233+ if err != nil {
234+ return 0, err
235+ }
236+ var p primeHandle
237+ var ioErr error
238+ if err := raw.Control(func(fd uintptr) {
239+ p.fd = int32(fd)
240+ ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
241+ }); err != nil {
242+ return 0, err
243+ }
244+ if ioErr != nil {
245+ return 0, errors.Join(errors.New("prime import"), ioErr)
246+ }
247+ return p.handle, nil
248+}
249+
250+func (k kmsDevice) gemClose(h uint32) {
251+ g := gemClose{handle: h}
252+ _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
253+}
254+
174 // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all255 // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all
175 // of them, except those on screen or queued.256 // of them, except those on screen or queued.
176 func (o *Output) dropClientFBs(now time.Time, all bool) {257 func (o *Output) dropClientFBs(now time.Time, all bool) {
@@ -187,5 +268,3 @@ func (o *Output) dropClientFBs(now time.Time, all bool) {
187 delete(o.clientFBs, id)268 delete(o.clientFBs, id)
188 }269 }
189 }270 }
190-
191-func scanoutable(f uint32) bool { _, ok := scanoutFormat(f); return ok }
M internal/adapters/drm/scanout_test.go +66 -10
@@ -2,8 +2,10 @@ package drm
22
33 import (
44 "testing"
5+ "time"
56
67 "github.com/bnema/nefertty/internal/ports"
8+ "github.com/stretchr/testify/mock"
79 )
810
911 func TestScanoutCandidate(t *testing.T) {
@@ -38,11 +40,6 @@ func TestScanoutCandidate(t *testing.T) {
3840 {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"},
3941 {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"},
4042 {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "logical_mismatch"},
41- {"unsupported format", nil, func(c *ports.SurfaceContent) {
42- d := *c.DMABuf
43- d.Format = 'N' | 'V'<<8 | '1'<<16 | '2'<<24
44- c.DMABuf = &d
45- }, "format"},
4643 {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""},
4744 } {
4845 s := scene
@@ -60,11 +57,70 @@ func TestScanoutCandidate(t *testing.T) {
6057 }
6158 }
6259
63-func TestScanoutFormat(t *testing.T) {
64- for f, want := range map[uint32]bool{fourccXRGB: true, fourccARGB: true, 'A' | 'B'<<8 | '2'<<16 | '4'<<24: false} {
65- got, ok := scanoutFormat(f)
66- if ok != want || ok && got != fourccXRGB {
67- t.Errorf("%x: %x %v", f, got, ok)
60+// A buffer scans out when the primary plane lists its format (or the
61+// opaque variant) with its modifier; the tranche offers only formats the
62+// renderer also composes.
63+func TestScanoutFormatFollowsInFormats(t *testing.T) {
64+ const tiled = 0x0200000000000001
65+ o, k, _ := testOutput(t)
66+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0}, {Format: fourccXR30, Modifier: tiled}}
67+ nv12 := uint32('N' | 'V'<<8 | '1'<<16 | '2'<<24)
68+ for _, tc := range []struct {
69+ name string
70+ format uint32
71+ mod uint64
72+ as uint32
73+ ok bool
74+ }{
75+ {"argb as xrgb", fourccARGB, tiled, fourccXRGB, true},
76+ {"xrgb linear", fourccXRGB, 0, fourccXRGB, true},
77+ {"10 bit alpha as opaque", fourccAR30, tiled, fourccXR30, true},
78+ {"modifier not listed", fourccXRGB, 0x0200000000000002, 0, false},
79+ {"format not listed", nv12, tiled, 0, false},
80+ } {
81+ got, ok := o.scanoutFormat(&ports.DMABuf{Format: tc.format, Modifier: tc.mod})
82+ if ok != tc.ok || ok && got != tc.as {
83+ t.Errorf("%s: %#x %v", tc.name, got, ok)
84+ }
85+ }
86+ sampled := []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0x0200000000000002}, {Format: nv12, Modifier: tiled}}
87+ if got := o.scanoutFormats(sampled); len(got) != 1 || got[0] != sampled[0] {
88+ t.Fatalf("tranche %v", got)
89+ }
90+ // A buffer not in IN_FORMATS is refused before any import.
91+ o.clientFBs = map[uint64]*clientFB{}
92+ if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 4, Format: nv12, Modifier: tiled}, time.Now()); fb != 0 || reason != "format" {
93+ t.Fatalf("fb %d reason %q", fb, reason)
94+ }
95+ // A listed one is imported with every plane (addFB builds ADDFB2).
96+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
97+ if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 5, Format: fourccARGB, Modifier: tiled, Planes: []ports.DMABufPlane{{}, {}}}, time.Now()); fb != 88 || reason != "" {
98+ t.Fatalf("fb %d reason %q", fb, reason)
99+ }
100+}
101+
102+// Each modeset reports the formats clients may allocate for scanout;
103+// with direct scanout off the list is empty.
104+func TestModesetSendsScanoutFormats(t *testing.T) {
105+ const tiled = 0x0200000000000001
106+ for _, on := range []bool{true, false} {
107+ o, k, _ := testOutput(t)
108+ o.cursor = nil
109+ o.scanout, o.device = on, 9
110+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}}
111+ o.sampled = []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0}}
112+ ch := make(chan ports.OutputFormats, 1)
113+ o.formats = ch
114+ k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
115+ if err := o.modeset(); err != nil {
116+ t.Fatal(err)
117+ }
118+ f := <-ch
119+ if f.Output != "DP-1" || f.Device != 9 {
120+ t.Fatalf("report %+v", f)
121+ }
122+ if on != (len(f.Formats) == 1 && f.Formats[0] == o.sampled[0]) || !on && len(f.Formats) != 0 {
123+ t.Fatalf("scanout=%v formats %v", on, f.Formats)
68124 }
69125 }
70126 }
@@ -2,8 +2,10 @@ package drm
2 2
3 import (3 import (
4 "testing"4 "testing"
5+ "time"
5 6
6 "github.com/bnema/nefertty/internal/ports"7 "github.com/bnema/nefertty/internal/ports"
8+ "github.com/stretchr/testify/mock"
7 )9 )
8 10
9 func TestScanoutCandidate(t *testing.T) {11 func TestScanoutCandidate(t *testing.T) {
@@ -38,11 +40,6 @@ func TestScanoutCandidate(t *testing.T) {
38 {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"},40 {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"},
39 {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"},41 {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"},
40 {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "logical_mismatch"},42 {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "logical_mismatch"},
41- {"unsupported format", nil, func(c *ports.SurfaceContent) {
42- d := *c.DMABuf
43- d.Format = 'N' | 'V'<<8 | '1'<<16 | '2'<<24
44- c.DMABuf = &d
45- }, "format"},
46 {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""},43 {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""},
47 } {44 } {
48 s := scene45 s := scene
@@ -60,11 +57,70 @@ func TestScanoutCandidate(t *testing.T) {
60 }57 }
61 }58 }
62 59
63-func TestScanoutFormat(t *testing.T) {60+// A buffer scans out when the primary plane lists its format (or the
64- for f, want := range map[uint32]bool{fourccXRGB: true, fourccARGB: true, 'A' | 'B'<<8 | '2'<<16 | '4'<<24: false} {61+// opaque variant) with its modifier; the tranche offers only formats the
65- got, ok := scanoutFormat(f)62+// renderer also composes.
66- if ok != want || ok && got != fourccXRGB {63+func TestScanoutFormatFollowsInFormats(t *testing.T) {
67- t.Errorf("%x: %x %v", f, got, ok)64+ const tiled = 0x0200000000000001
65+ o, k, _ := testOutput(t)
66+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0}, {Format: fourccXR30, Modifier: tiled}}
67+ nv12 := uint32('N' | 'V'<<8 | '1'<<16 | '2'<<24)
68+ for _, tc := range []struct {
69+ name string
70+ format uint32
71+ mod uint64
72+ as uint32
73+ ok bool
74+ }{
75+ {"argb as xrgb", fourccARGB, tiled, fourccXRGB, true},
76+ {"xrgb linear", fourccXRGB, 0, fourccXRGB, true},
77+ {"10 bit alpha as opaque", fourccAR30, tiled, fourccXR30, true},
78+ {"modifier not listed", fourccXRGB, 0x0200000000000002, 0, false},
79+ {"format not listed", nv12, tiled, 0, false},
80+ } {
81+ got, ok := o.scanoutFormat(&ports.DMABuf{Format: tc.format, Modifier: tc.mod})
82+ if ok != tc.ok || ok && got != tc.as {
83+ t.Errorf("%s: %#x %v", tc.name, got, ok)
84+ }
85+ }
86+ sampled := []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0x0200000000000002}, {Format: nv12, Modifier: tiled}}
87+ if got := o.scanoutFormats(sampled); len(got) != 1 || got[0] != sampled[0] {
88+ t.Fatalf("tranche %v", got)
89+ }
90+ // A buffer not in IN_FORMATS is refused before any import.
91+ o.clientFBs = map[uint64]*clientFB{}
92+ if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 4, Format: nv12, Modifier: tiled}, time.Now()); fb != 0 || reason != "format" {
93+ t.Fatalf("fb %d reason %q", fb, reason)
94+ }
95+ // A listed one is imported with every plane (addFB builds ADDFB2).
96+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
97+ if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 5, Format: fourccARGB, Modifier: tiled, Planes: []ports.DMABufPlane{{}, {}}}, time.Now()); fb != 88 || reason != "" {
98+ t.Fatalf("fb %d reason %q", fb, reason)
99+ }
100+}
101+
102+// Each modeset reports the formats clients may allocate for scanout;
103+// with direct scanout off the list is empty.
104+func TestModesetSendsScanoutFormats(t *testing.T) {
105+ const tiled = 0x0200000000000001
106+ for _, on := range []bool{true, false} {
107+ o, k, _ := testOutput(t)
108+ o.cursor = nil
109+ o.scanout, o.device = on, 9
110+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}}
111+ o.sampled = []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0}}
112+ ch := make(chan ports.OutputFormats, 1)
113+ o.formats = ch
114+ k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
115+ if err := o.modeset(); err != nil {
116+ t.Fatal(err)
117+ }
118+ f := <-ch
119+ if f.Output != "DP-1" || f.Device != 9 {
120+ t.Fatalf("report %+v", f)
121+ }
122+ if on != (len(f.Formats) == 1 && f.Formats[0] == o.sampled[0]) || !on && len(f.Formats) != 0 {
123+ t.Fatalf("scanout=%v formats %v", on, f.Formats)
68 }124 }
69 }125 }
70 }126 }
M internal/adapters/wayland/dmabuf.go +99 -13
@@ -15,7 +15,11 @@ import (
1515
1616 // linux-dmabuf v4: clients hand GPU buffers to the renderer without copies.
1717 // The formats are those the renderer imports (ports.DMABufSupport); without
18-// any, the global is not advertised and clients use wl_shm.
18+// any, the global is not advertised and clients use wl_shm. Surface
19+// feedback also offers, first, a scanout tranche while the surface's
20+// window is fullscreen on an output that reported scanout formats
21+// (ports.OutputFormats): a buffer allocated from it can be flipped to the
22+// display with no composition.
1923
2024 const dmabufVersion = 4
2125
@@ -30,7 +34,7 @@ func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error
3034 if err != nil {
3135 return err
3236 }
33- s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table}
37+ s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table, scanout: map[string]ports.OutputFormats{}, feedbacks: map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface{}}
3438 return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {
3539 r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)
3640 if err != nil || v >= 4 {
@@ -77,6 +81,11 @@ type dmabufGlobal struct {
7781 support ports.DMABufSupport
7882 table *os.File
7983 nextID uint64
84+ // scanout are the outputs' direct scanout formats by output name.
85+ scanout map[string]ports.OutputFormats
86+ // feedbacks are the live feedback objects and their surface (nil:
87+ // default feedback).
88+ feedbacks map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface
8089 }
8190
8291 func (g *dmabufGlobal) close() {
@@ -98,35 +107,112 @@ func (g *dmabufGlobal) CreateParams(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32)
98107 }
99108
100109 func (g *dmabufGlobal) GetDefaultFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
101- g.feedback(r, id)
110+ g.feedback(r, id, nil)
102111 }
103112
104-// GetSurfaceFeedback sends the default feedback: one renderer, one tranche.
105-// Scanout tranches come with direct scanout.
106-func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, _ *wayland.Surface) {
107- g.feedback(r, id)
113+// GetSurfaceFeedback follows the surface: a scanout tranche comes first
114+// while it can be scanned out (see scanoutFor).
115+func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *wayland.Surface) {
116+ g.feedback(r, id, g.server.surfaceOf(surf))
108117 }
109118
110-func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
119+func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *surface) {
111120 fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{})
112121 if err != nil {
113122 return
114123 }
115- dev := make([]byte, 8)
116- binary.NativeEndian.PutUint64(dev, g.support.Device)
124+ g.feedbacks[fb] = surf
125+ fb.OnDestroy = func() { delete(g.feedbacks, fb) }
126+ g.send(fb, surf)
127+}
128+
129+// devBytes is a dev_t as the protocol's array.
130+func devBytes(dev uint64) []byte {
131+ b := make([]byte, 8)
132+ binary.NativeEndian.PutUint64(b, dev)
133+ return b
134+}
135+
136+// send sends the whole feedback: table, main device, the scanout tranche
137+// when the surface has one, then the renderer tranche.
138+func (g *dmabufGlobal) send(fb *linuxdmabuf.ZwpLinuxDmabufFeedbackV1, surf *surface) {
139+ fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))
140+ main := devBytes(g.support.Device)
141+ fb.SendMainDevice(main)
142+ if out, ok := g.scanoutFor(surf); ok {
143+ var indices []byte
144+ for _, f := range out.Formats {
145+ if i := slices.Index(g.support.Formats, f); i >= 0 {
146+ indices = binary.NativeEndian.AppendUint16(indices, uint16(i))
147+ }
148+ }
149+ if len(indices) > 0 {
150+ fb.SendTrancheTargetDevice(devBytes(out.Device))
151+ fb.SendTrancheFlags(uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout))
152+ fb.SendTrancheFormats(indices)
153+ fb.SendTrancheDone()
154+ }
155+ }
117156 indices := make([]byte, 2*len(g.support.Formats))
118157 for i := range g.support.Formats {
119158 binary.NativeEndian.PutUint16(indices[i*2:], uint16(i))
120159 }
121- fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))
122- fb.SendMainDevice(dev)
123- fb.SendTrancheTargetDevice(dev)
160+ fb.SendTrancheTargetDevice(main)
124161 fb.SendTrancheFlags(0)
125162 fb.SendTrancheFormats(indices)
126163 fb.SendTrancheDone()
127164 fb.SendDone()
128165 }
129166
167+// scanoutFor is the scanout offer for a surface: its window is fullscreen
168+// on an output that reported scanout formats.
169+func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) {
170+ if surf == nil || surf.destroyed {
171+ return ports.OutputFormats{}, false
172+ }
173+ root := surf.root()
174+ if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen {
175+ return ports.OutputFormats{}, false
176+ }
177+ out, ok := g.scanout[root.xdg.window.last.Output]
178+ return out, ok && len(out.Formats) > 0
179+}
180+
181+// setOutputFormats records an output's scanout formats and sends the
182+// feedback of the surfaces fullscreen on it again. Outputs offer only
183+// formats the renderer samples (a refused buffer is composed), so they
184+// are all in the table.
185+func (g *dmabufGlobal) setOutputFormats(f ports.OutputFormats) {
186+ if old, ok := g.scanout[f.Output]; ok && old.Device == f.Device && slices.Equal(old.Formats, f.Formats) {
187+ return
188+ }
189+ if len(f.Formats) == 0 {
190+ delete(g.scanout, f.Output)
191+ } else {
192+ g.scanout[f.Output] = f
193+ }
194+ for fb, surf := range g.feedbacks {
195+ if surf != nil && !surf.destroyed {
196+ if root := surf.root(); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Output == f.Output {
197+ g.send(fb, surf)
198+ }
199+ }
200+ }
201+}
202+
203+// resendSurface sends the feedback of a window's surfaces again, when its
204+// fullscreen state or output changed.
205+func (g *dmabufGlobal) resendSurface(root *surface) {
206+ if g == nil {
207+ return
208+ }
209+ for fb, surf := range g.feedbacks {
210+ if surf != nil && surf.root() == root {
211+ g.send(fb, surf)
212+ }
213+ }
214+}
215+
130216 type feedback struct{}
131217
132218 func (feedback) Destroy(*linuxdmabuf.ZwpLinuxDmabufFeedbackV1) {}
@@ -15,7 +15,11 @@ import (
15 15
16 // linux-dmabuf v4: clients hand GPU buffers to the renderer without copies.16 // linux-dmabuf v4: clients hand GPU buffers to the renderer without copies.
17 // The formats are those the renderer imports (ports.DMABufSupport); without17 // The formats are those the renderer imports (ports.DMABufSupport); without
18-// any, the global is not advertised and clients use wl_shm.18+// any, the global is not advertised and clients use wl_shm. Surface
19+// feedback also offers, first, a scanout tranche while the surface's
20+// window is fullscreen on an output that reported scanout formats
21+// (ports.OutputFormats): a buffer allocated from it can be flipped to the
22+// display with no composition.
19 23
20 const dmabufVersion = 424 const dmabufVersion = 4
21 25
@@ -30,7 +34,7 @@ func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error
30 if err != nil {34 if err != nil {
31 return err35 return err
32 }36 }
33- s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table}37+ s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table, scanout: map[string]ports.OutputFormats{}, feedbacks: map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface{}}
34 return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {38 return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {
35 r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)39 r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)
36 if err != nil || v >= 4 {40 if err != nil || v >= 4 {
@@ -77,6 +81,11 @@ type dmabufGlobal struct {
77 support ports.DMABufSupport81 support ports.DMABufSupport
78 table *os.File82 table *os.File
79 nextID uint6483 nextID uint64
84+ // scanout are the outputs' direct scanout formats by output name.
85+ scanout map[string]ports.OutputFormats
86+ // feedbacks are the live feedback objects and their surface (nil:
87+ // default feedback).
88+ feedbacks map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface
80 }89 }
81 90
82 func (g *dmabufGlobal) close() {91 func (g *dmabufGlobal) close() {
@@ -98,35 +107,112 @@ func (g *dmabufGlobal) CreateParams(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32)
98 }107 }
99 108
100 func (g *dmabufGlobal) GetDefaultFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {109 func (g *dmabufGlobal) GetDefaultFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
101- g.feedback(r, id)110+ g.feedback(r, id, nil)
102 }111 }
103 112
104-// GetSurfaceFeedback sends the default feedback: one renderer, one tranche.113+// GetSurfaceFeedback follows the surface: a scanout tranche comes first
105-// Scanout tranches come with direct scanout.114+// while it can be scanned out (see scanoutFor).
106-func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, _ *wayland.Surface) {115+func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *wayland.Surface) {
107- g.feedback(r, id)116+ g.feedback(r, id, g.server.surfaceOf(surf))
108 }117 }
109 118
110-func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {119+func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *surface) {
111 fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{})120 fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{})
112 if err != nil {121 if err != nil {
113 return122 return
114 }123 }
115- dev := make([]byte, 8)124+ g.feedbacks[fb] = surf
116- binary.NativeEndian.PutUint64(dev, g.support.Device)125+ fb.OnDestroy = func() { delete(g.feedbacks, fb) }
126+ g.send(fb, surf)
127+}
128+
129+// devBytes is a dev_t as the protocol's array.
130+func devBytes(dev uint64) []byte {
131+ b := make([]byte, 8)
132+ binary.NativeEndian.PutUint64(b, dev)
133+ return b
134+}
135+
136+// send sends the whole feedback: table, main device, the scanout tranche
137+// when the surface has one, then the renderer tranche.
138+func (g *dmabufGlobal) send(fb *linuxdmabuf.ZwpLinuxDmabufFeedbackV1, surf *surface) {
139+ fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))
140+ main := devBytes(g.support.Device)
141+ fb.SendMainDevice(main)
142+ if out, ok := g.scanoutFor(surf); ok {
143+ var indices []byte
144+ for _, f := range out.Formats {
145+ if i := slices.Index(g.support.Formats, f); i >= 0 {
146+ indices = binary.NativeEndian.AppendUint16(indices, uint16(i))
147+ }
148+ }
149+ if len(indices) > 0 {
150+ fb.SendTrancheTargetDevice(devBytes(out.Device))
151+ fb.SendTrancheFlags(uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout))
152+ fb.SendTrancheFormats(indices)
153+ fb.SendTrancheDone()
154+ }
155+ }
117 indices := make([]byte, 2*len(g.support.Formats))156 indices := make([]byte, 2*len(g.support.Formats))
118 for i := range g.support.Formats {157 for i := range g.support.Formats {
119 binary.NativeEndian.PutUint16(indices[i*2:], uint16(i))158 binary.NativeEndian.PutUint16(indices[i*2:], uint16(i))
120 }159 }
121- fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))160+ fb.SendTrancheTargetDevice(main)
122- fb.SendMainDevice(dev)
123- fb.SendTrancheTargetDevice(dev)
124 fb.SendTrancheFlags(0)161 fb.SendTrancheFlags(0)
125 fb.SendTrancheFormats(indices)162 fb.SendTrancheFormats(indices)
126 fb.SendTrancheDone()163 fb.SendTrancheDone()
127 fb.SendDone()164 fb.SendDone()
128 }165 }
129 166
167+// scanoutFor is the scanout offer for a surface: its window is fullscreen
168+// on an output that reported scanout formats.
169+func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) {
170+ if surf == nil || surf.destroyed {
171+ return ports.OutputFormats{}, false
172+ }
173+ root := surf.root()
174+ if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen {
175+ return ports.OutputFormats{}, false
176+ }
177+ out, ok := g.scanout[root.xdg.window.last.Output]
178+ return out, ok && len(out.Formats) > 0
179+}
180+
181+// setOutputFormats records an output's scanout formats and sends the
182+// feedback of the surfaces fullscreen on it again. Outputs offer only
183+// formats the renderer samples (a refused buffer is composed), so they
184+// are all in the table.
185+func (g *dmabufGlobal) setOutputFormats(f ports.OutputFormats) {
186+ if old, ok := g.scanout[f.Output]; ok && old.Device == f.Device && slices.Equal(old.Formats, f.Formats) {
187+ return
188+ }
189+ if len(f.Formats) == 0 {
190+ delete(g.scanout, f.Output)
191+ } else {
192+ g.scanout[f.Output] = f
193+ }
194+ for fb, surf := range g.feedbacks {
195+ if surf != nil && !surf.destroyed {
196+ if root := surf.root(); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Output == f.Output {
197+ g.send(fb, surf)
198+ }
199+ }
200+ }
201+}
202+
203+// resendSurface sends the feedback of a window's surfaces again, when its
204+// fullscreen state or output changed.
205+func (g *dmabufGlobal) resendSurface(root *surface) {
206+ if g == nil {
207+ return
208+ }
209+ for fb, surf := range g.feedbacks {
210+ if surf != nil && surf.root() == root {
211+ g.send(fb, surf)
212+ }
213+ }
214+}
215+
130 type feedback struct{}216 type feedback struct{}
131 217
132 func (feedback) Destroy(*linuxdmabuf.ZwpLinuxDmabufFeedbackV1) {}218 func (feedback) Destroy(*linuxdmabuf.ZwpLinuxDmabufFeedbackV1) {}
M internal/adapters/wayland/dmabuf_test.go +97 -0
@@ -172,3 +172,100 @@ func TestWestonDMABuf(t *testing.T) {
172172 }
173173 }
174174 }
175+
176+// feedbackProxy records the tranches of each feedback round (done).
177+type feedbackProxy struct {
178+ wlturbo.BaseProxy
179+ rounds chan []tranche
180+ cur []tranche
181+ open tranche
182+}
183+
184+type tranche struct {
185+ flags uint32
186+ indices []uint16
187+}
188+
189+func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {
190+ switch uint32(e.Opcode) {
191+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable:
192+ if fd := e.Fd(); fd != 0 {
193+ unix.Close(int(fd))
194+ }
195+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags:
196+ p.open.flags = e.Uint32()
197+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats:
198+ a := e.Array()
199+ for i := 0; i+1 < len(a); i += 2 {
200+ p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8)
201+ }
202+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheDone:
203+ p.cur, p.open = append(p.cur, p.open), tranche{}
204+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone:
205+ p.rounds <- p.cur
206+ p.cur = nil
207+ }
208+}
209+
210+// A fullscreen surface on an output with scanout formats gets a scanout
211+// tranche first; leaving fullscreen sends the renderer tranche alone.
212+func TestDMABufScanoutTranche(t *testing.T) {
213+ tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
214+ sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
215+ dir := t.TempDir()
216+ events := make(chan ports.ClientEvent, 16)
217+ commands := make(chan ports.ClientCommand, 16)
218+ formats := make(chan ports.OutputFormats, 1)
219+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: commands, OutputFormats: formats}, logging.For(context.Background(), "wayland"))
220+ if err != nil {
221+ t.Fatal(err)
222+ }
223+ ctx, cancel := context.WithCancel(context.Background())
224+ done := make(chan error, 1)
225+ go func() { done <- s.Run(ctx) }()
226+ t.Cleanup(func() { cancel(); <-done })
227+ c := protocolClient(t, s, dir)
228+ w, surf, xdg := surfaceMapper(t, c, events)()
229+ // Our configures must not block the mapper's one-slot proxy.
230+ registerProtocol(t, c, xdg)
231+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
232+ fb := c.AllocateID()
233+ proxy := &feedbackProxy{rounds: make(chan []tranche, 8)}
234+ proxy.SetID(fb)
235+ c.Context().Register(proxy)
236+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, fb, surf)
237+ round := func(what string) []tranche {
238+ t.Helper()
239+ deadline := time.After(2 * time.Second)
240+ for {
241+ if err := c.Roundtrip(); err != nil {
242+ t.Fatal(err)
243+ }
244+ select {
245+ case r := <-proxy.rounds:
246+ return r
247+ case <-deadline:
248+ t.Fatalf("no feedback: %s", what)
249+ default:
250+ time.Sleep(5 * time.Millisecond)
251+ }
252+ }
253+ }
254+ scanout := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout)
255+ if r := round("initial"); len(r) != 1 || r[0].flags != 0 || len(r[0].indices) != 2 {
256+ t.Fatalf("tiled window: %+v", r)
257+ }
258+ formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
259+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"}
260+ r := round("fullscreen")
261+ for len(r) == 1 { // the formats may land after the configure
262+ r = round("fullscreen with formats")
263+ }
264+ if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 {
265+ t.Fatalf("fullscreen: %+v", r)
266+ }
267+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1"}
268+ if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 {
269+ t.Fatalf("left fullscreen: %+v", r)
270+ }
271+}
@@ -172,3 +172,100 @@ func TestWestonDMABuf(t *testing.T) {
172 }172 }
173 }173 }
174 }174 }
175+
176+// feedbackProxy records the tranches of each feedback round (done).
177+type feedbackProxy struct {
178+ wlturbo.BaseProxy
179+ rounds chan []tranche
180+ cur []tranche
181+ open tranche
182+}
183+
184+type tranche struct {
185+ flags uint32
186+ indices []uint16
187+}
188+
189+func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {
190+ switch uint32(e.Opcode) {
191+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable:
192+ if fd := e.Fd(); fd != 0 {
193+ unix.Close(int(fd))
194+ }
195+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags:
196+ p.open.flags = e.Uint32()
197+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats:
198+ a := e.Array()
199+ for i := 0; i+1 < len(a); i += 2 {
200+ p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8)
201+ }
202+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheDone:
203+ p.cur, p.open = append(p.cur, p.open), tranche{}
204+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone:
205+ p.rounds <- p.cur
206+ p.cur = nil
207+ }
208+}
209+
210+// A fullscreen surface on an output with scanout formats gets a scanout
211+// tranche first; leaving fullscreen sends the renderer tranche alone.
212+func TestDMABufScanoutTranche(t *testing.T) {
213+ tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
214+ sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
215+ dir := t.TempDir()
216+ events := make(chan ports.ClientEvent, 16)
217+ commands := make(chan ports.ClientCommand, 16)
218+ formats := make(chan ports.OutputFormats, 1)
219+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: commands, OutputFormats: formats}, logging.For(context.Background(), "wayland"))
220+ if err != nil {
221+ t.Fatal(err)
222+ }
223+ ctx, cancel := context.WithCancel(context.Background())
224+ done := make(chan error, 1)
225+ go func() { done <- s.Run(ctx) }()
226+ t.Cleanup(func() { cancel(); <-done })
227+ c := protocolClient(t, s, dir)
228+ w, surf, xdg := surfaceMapper(t, c, events)()
229+ // Our configures must not block the mapper's one-slot proxy.
230+ registerProtocol(t, c, xdg)
231+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
232+ fb := c.AllocateID()
233+ proxy := &feedbackProxy{rounds: make(chan []tranche, 8)}
234+ proxy.SetID(fb)
235+ c.Context().Register(proxy)
236+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, fb, surf)
237+ round := func(what string) []tranche {
238+ t.Helper()
239+ deadline := time.After(2 * time.Second)
240+ for {
241+ if err := c.Roundtrip(); err != nil {
242+ t.Fatal(err)
243+ }
244+ select {
245+ case r := <-proxy.rounds:
246+ return r
247+ case <-deadline:
248+ t.Fatalf("no feedback: %s", what)
249+ default:
250+ time.Sleep(5 * time.Millisecond)
251+ }
252+ }
253+ }
254+ scanout := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout)
255+ if r := round("initial"); len(r) != 1 || r[0].flags != 0 || len(r[0].indices) != 2 {
256+ t.Fatalf("tiled window: %+v", r)
257+ }
258+ formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
259+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"}
260+ r := round("fullscreen")
261+ for len(r) == 1 { // the formats may land after the configure
262+ r = round("fullscreen with formats")
263+ }
264+ if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 {
265+ t.Fatalf("fullscreen: %+v", r)
266+ }
267+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1"}
268+ if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 {
269+ t.Fatalf("left fullscreen: %+v", r)
270+ }
271+}
M internal/adapters/wayland/server.go +33 -1
@@ -38,6 +38,9 @@ type Channels struct {
3838 // Presented paces frame callbacks on the outputs' page flips; outputs
3939 // that do not flip (idle, headless) are paced at their refresh rate.
4040 Presented <-chan ports.OutputPresented
41+ // OutputFormats are the outputs' direct scanout formats, offered in
42+ // dmabuf feedback to fullscreen surfaces.
43+ OutputFormats <-chan ports.OutputFormats
4144 }
4245 type Server struct {
4346 display *server.Display
@@ -196,7 +199,7 @@ func (s *Server) Run(ctx context.Context) error {
196199 s.started = time.Now()
197200 s.ctx = ctx
198201 var wg sync.WaitGroup
199- wg.Add(5)
202+ wg.Add(6)
200203 go func() { defer wg.Done(); s.forward(ctx) }()
201204 go func() { defer wg.Done(); s.forwardCursors(ctx) }()
202205 go func() { defer wg.Done(); s.forwardContents(ctx) }()
@@ -233,11 +236,36 @@ func (s *Server) Run(ctx context.Context) error {
233236 }
234237 }()
235238 go func() { defer wg.Done(); s.pace(ctx) }()
239+ go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
236240 err := s.display.Run(ctx)
237241 wg.Wait()
238242 return err
239243 }
240244
245+// forwardOutputFormats applies the outputs' scanout formats on the
246+// display goroutine.
247+func (s *Server) forwardOutputFormats(ctx context.Context) {
248+ for {
249+ select {
250+ case <-ctx.Done():
251+ return
252+ case <-s.display.Stopped():
253+ return
254+ case f, ok := <-s.channels.OutputFormats:
255+ if !ok {
256+ return
257+ }
258+ if !s.display.Do(func() {
259+ if s.dmabuf != nil {
260+ s.dmabuf.setOutputFormats(f)
261+ }
262+ }) {
263+ return
264+ }
265+ }
266+ }
267+}
268+
241269 func (s *Server) emit(ev ports.ClientEvent) {
242270 if s.channels.Events == nil {
243271 return
@@ -473,8 +501,12 @@ func (s *Server) apply(cmd ports.ClientCommand) {
473501 return
474502 }
475503 s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure")
504+ scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output
476505 w.last, w.hasLast = c, true
477506 w.sendConfigure()
507+ if scanoutChanged && w.xdg.surface != nil {
508+ s.dmabuf.resendSurface(w.xdg.surface)
509+ }
478510 if surf := w.xdg.surface; surf != nil && c.Output != "" {
479511 surf.sendTreeScale()
480512 }
@@ -38,6 +38,9 @@ type Channels struct {
38 // Presented paces frame callbacks on the outputs' page flips; outputs38 // Presented paces frame callbacks on the outputs' page flips; outputs
39 // that do not flip (idle, headless) are paced at their refresh rate.39 // that do not flip (idle, headless) are paced at their refresh rate.
40 Presented <-chan ports.OutputPresented40 Presented <-chan ports.OutputPresented
41+ // OutputFormats are the outputs' direct scanout formats, offered in
42+ // dmabuf feedback to fullscreen surfaces.
43+ OutputFormats <-chan ports.OutputFormats
41 }44 }
42 type Server struct {45 type Server struct {
43 display *server.Display46 display *server.Display
@@ -196,7 +199,7 @@ func (s *Server) Run(ctx context.Context) error {
196 s.started = time.Now()199 s.started = time.Now()
197 s.ctx = ctx200 s.ctx = ctx
198 var wg sync.WaitGroup201 var wg sync.WaitGroup
199- wg.Add(5)202+ wg.Add(6)
200 go func() { defer wg.Done(); s.forward(ctx) }()203 go func() { defer wg.Done(); s.forward(ctx) }()
201 go func() { defer wg.Done(); s.forwardCursors(ctx) }()204 go func() { defer wg.Done(); s.forwardCursors(ctx) }()
202 go func() { defer wg.Done(); s.forwardContents(ctx) }()205 go func() { defer wg.Done(); s.forwardContents(ctx) }()
@@ -233,11 +236,36 @@ func (s *Server) Run(ctx context.Context) error {
233 }236 }
234 }()237 }()
235 go func() { defer wg.Done(); s.pace(ctx) }()238 go func() { defer wg.Done(); s.pace(ctx) }()
239+ go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
236 err := s.display.Run(ctx)240 err := s.display.Run(ctx)
237 wg.Wait()241 wg.Wait()
238 return err242 return err
239 }243 }
240 244
245+// forwardOutputFormats applies the outputs' scanout formats on the
246+// display goroutine.
247+func (s *Server) forwardOutputFormats(ctx context.Context) {
248+ for {
249+ select {
250+ case <-ctx.Done():
251+ return
252+ case <-s.display.Stopped():
253+ return
254+ case f, ok := <-s.channels.OutputFormats:
255+ if !ok {
256+ return
257+ }
258+ if !s.display.Do(func() {
259+ if s.dmabuf != nil {
260+ s.dmabuf.setOutputFormats(f)
261+ }
262+ }) {
263+ return
264+ }
265+ }
266+ }
267+}
268+
241 func (s *Server) emit(ev ports.ClientEvent) {269 func (s *Server) emit(ev ports.ClientEvent) {
242 if s.channels.Events == nil {270 if s.channels.Events == nil {
243 return271 return
@@ -473,8 +501,12 @@ func (s *Server) apply(cmd ports.ClientCommand) {
473 return501 return
474 }502 }
475 s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure")503 s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure")
504+ scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output
476 w.last, w.hasLast = c, true505 w.last, w.hasLast = c, true
477 w.sendConfigure()506 w.sendConfigure()
507+ if scanoutChanged && w.xdg.surface != nil {
508+ s.dmabuf.resendSurface(w.xdg.surface)
509+ }
478 if surf := w.xdg.surface; surf != nil && c.Output != "" {510 if surf := w.xdg.surface; surf != nil && c.Output != "" {
479 surf.sendTreeScale()511 surf.sendTreeScale()
480 }512 }
M internal/app/app.go +8 -3
@@ -87,6 +87,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
8787 contents := make(chan ports.SurfaceContent, 64)
8888 cursorChanges := make(chan ports.CursorChange, 1)
8989 presented := make(chan ports.OutputPresented, 64)
90+ outputFormats := make(chan ports.OutputFormats, 8)
9091 ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal}
9192 c, err := core.New(opts.Config, ch)
9293 if err != nil {
@@ -101,7 +102,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
101102 // Every output renders on the same GPU: its formats are the clients'.
102103 dmabuf := vulkan.Probe()
103104 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
104- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented}, logging.For(ctx, "wayland"))
105+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented, OutputFormats: outputFormats}, logging.For(ctx, "wayland"))
105106 if err != nil {
106107 km.Close()
107108 return err
@@ -215,8 +216,12 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
215216 }
216217 if hw != nil {
217218 done <- safe("output", func() error {
218- want := func() drm.Want { return wantFromConfig(opts.Config) }
219- return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm"))
219+ want := func() drm.Want {
220+ w := wantFromConfig(opts.Config)
221+ w.Sampled = dmabuf.Formats
222+ return w
223+ }
224+ return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, outputFormats, curs, newRenderer, logging.For(ctx, "drm"))
220225 })
221226 return
222227 }
@@ -87,6 +87,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
87 contents := make(chan ports.SurfaceContent, 64)87 contents := make(chan ports.SurfaceContent, 64)
88 cursorChanges := make(chan ports.CursorChange, 1)88 cursorChanges := make(chan ports.CursorChange, 1)
89 presented := make(chan ports.OutputPresented, 64)89 presented := make(chan ports.OutputPresented, 64)
90+ outputFormats := make(chan ports.OutputFormats, 8)
90 ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal}91 ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal}
91 c, err := core.New(opts.Config, ch)92 c, err := core.New(opts.Config, ch)
92 if err != nil {93 if err != nil {
@@ -101,7 +102,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
101 // Every output renders on the same GPU: its formats are the clients'.102 // Every output renders on the same GPU: its formats are the clients'.
102 dmabuf := vulkan.Probe()103 dmabuf := vulkan.Probe()
103 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")104 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
104- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented}, logging.For(ctx, "wayland"))105+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented, OutputFormats: outputFormats}, logging.For(ctx, "wayland"))
105 if err != nil {106 if err != nil {
106 km.Close()107 km.Close()
107 return err108 return err
@@ -215,8 +216,12 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
215 }216 }
216 if hw != nil {217 if hw != nil {
217 done <- safe("output", func() error {218 done <- safe("output", func() error {
218- want := func() drm.Want { return wantFromConfig(opts.Config) }219+ want := func() drm.Want {
219- return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm"))220+ w := wantFromConfig(opts.Config)
221+ w.Sampled = dmabuf.Formats
222+ return w
223+ }
224+ return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, outputFormats, curs, newRenderer, logging.For(ctx, "drm"))
220 })225 })
221 return226 return
222 }227 }
M internal/app/drm.go +5 -2
@@ -109,12 +109,13 @@ func (b *drmBackend) close() {
109109 // one flip reader per card, and a udev watcher that rescans connectors on
110110 // hotplug. Core learns about outputs through events. It returns when ctx
111111 // ends, after every output is closed.
112-func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
112+func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, formats chan<- ports.OutputFormats, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
113113 ctx, cancel := context.WithCancel(ctx)
114114 defer cancel()
115115 var readers sync.WaitGroup
116116 readerErr := make(chan error, len(b.cards))
117117 for _, c := range b.cards {
118+ c.SetFormats(formats)
118119 readers.Go(func() {
119120 if err := safe("drm events", func() error { return c.ReadEvents(ctx) }); err != nil {
120121 readerErr <- fmt.Errorf("%s: %w", c.Path(), err)
@@ -141,7 +142,9 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, event
141142 stopping := map[string]bool{}
142143 scan := func() {
143144 for _, c := range b.cards {
144- c.SetWant(want())
145+ w := want()
146+ w.Device = c.Device()
147+ c.SetWant(w)
145148 added, removed, replaced, err := c.Scan()
146149 if err != nil {
147150 log.Warn().Err(err).Msg("scan connectors")
@@ -109,12 +109,13 @@ func (b *drmBackend) close() {
109 // one flip reader per card, and a udev watcher that rescans connectors on109 // one flip reader per card, and a udev watcher that rescans connectors on
110 // hotplug. Core learns about outputs through events. It returns when ctx110 // hotplug. Core learns about outputs through events. It returns when ctx
111 // ends, after every output is closed.111 // ends, after every output is closed.
112-func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {112+func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, formats chan<- ports.OutputFormats, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
113 ctx, cancel := context.WithCancel(ctx)113 ctx, cancel := context.WithCancel(ctx)
114 defer cancel()114 defer cancel()
115 var readers sync.WaitGroup115 var readers sync.WaitGroup
116 readerErr := make(chan error, len(b.cards))116 readerErr := make(chan error, len(b.cards))
117 for _, c := range b.cards {117 for _, c := range b.cards {
118+ c.SetFormats(formats)
118 readers.Go(func() {119 readers.Go(func() {
119 if err := safe("drm events", func() error { return c.ReadEvents(ctx) }); err != nil {120 if err := safe("drm events", func() error { return c.ReadEvents(ctx) }); err != nil {
120 readerErr <- fmt.Errorf("%s: %w", c.Path(), err)121 readerErr <- fmt.Errorf("%s: %w", c.Path(), err)
@@ -141,7 +142,9 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, event
141 stopping := map[string]bool{}142 stopping := map[string]bool{}
142 scan := func() {143 scan := func() {
143 for _, c := range b.cards {144 for _, c := range b.cards {
144- c.SetWant(want())145+ w := want()
146+ w.Device = c.Device()
147+ c.SetWant(w)
145 added, removed, replaced, err := c.Scan()148 added, removed, replaced, err := c.Scan()
146 if err != nil {149 if err != nil {
147 log.Warn().Err(err).Msg("scan connectors")150 log.Warn().Err(err).Msg("scan connectors")
M internal/ports/messages.go +11 -0
@@ -329,6 +329,17 @@ type OutputPresented struct {
329329 Seen map[WindowID]uint64
330330 }
331331
332+// OutputFormats carries output → wayland the dmabuf formats an output can
333+// scan out directly (its primary plane's, that the renderer also samples,
334+// so a refused buffer can still be composed). Device is the KMS device
335+// (dev_t) clients allocate scanout buffers for. Sent at start and after
336+// every modeset; empty Formats means no direct scanout.
337+type OutputFormats struct {
338+ Output string
339+ Device uint64
340+ Formats []DMABufFormat
341+}
342+
332343 // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time
333344 // (hardware clock on DRM), Seq the output's vblank counter, Refresh the
334345 // refresh period (0 while variable refresh is on). ZeroCopy is set when a
@@ -329,6 +329,17 @@ type OutputPresented struct {
329 Seen map[WindowID]uint64329 Seen map[WindowID]uint64
330 }330 }
331 331
332+// OutputFormats carries output → wayland the dmabuf formats an output can
333+// scan out directly (its primary plane's, that the renderer also samples,
334+// so a refused buffer can still be composed). Device is the KMS device
335+// (dev_t) clients allocate scanout buffers for. Sent at start and after
336+// every modeset; empty Formats means no direct scanout.
337+type OutputFormats struct {
338+ Output string
339+ Device uint64
340+ Formats []DMABufFormat
341+}
342+
332 // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time343 // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time
333 // (hardware clock on DRM), Seq the output's vblank counter, Refresh the344 // (hardware clock on DRM), Seq the output's vblank counter, Refresh the
334 // refresh period (0 while variable refresh is on). ZeroCopy is set when a345 // refresh period (0 while variable refresh is on). ZeroCopy is set when a

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close