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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +466 -48
M internal/adapters/drm/card.go +18 -0
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | "sync" | |
| 9 | 9 | "sync/atomic" | |
| 10 | 10 | ||
| 11 | + | "github.com/bnema/nefertty/internal/ports" | |
| 11 | 12 | "github.com/bnema/zerowrap" | |
| 12 | 13 | "golang.org/x/sys/unix" | |
| 13 | 14 | ) | |
| @@ -32,6 +33,9 @@ type Card struct { | |||
| 32 | 33 | async bool | |
| 33 | 34 | // serials numbers the commits of every output (event userData). | |
| 34 | 35 | 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 | 41 | // 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 | 59 | // Path is the device path, e.g. /dev/dri/card1. | |
| 56 | 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 | 76 | // SetWant replaces the connector config; the next Scan applies it. | |
| 59 | 77 | func (c *Card) SetWant(w Want) { c.want = w } | |
| 60 | 78 | ||
| @@ -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 bool | 33 | 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.Uint64 | 35 | 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. fd | 41 | // 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 ( | |||
| 6 | 6 | "math" | |
| 7 | 7 | "unsafe" | |
| 8 | 8 | ||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |
| 9 | 10 | "golang.org/x/sys/unix" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| @@ -99,6 +100,11 @@ type Want struct { | |||
| 99 | 100 | NoTearing bool | |
| 100 | 101 | // NoVRR keeps variable refresh off (render.vrr = off). | |
| 101 | 102 | 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 | 110 | // 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 bool | 100 | NoTearing bool | ||
| 100 | // NoVRR keeps variable refresh off (render.vrr = off). | 101 | // NoVRR keeps variable refresh off (render.vrr = off). | ||
| 101 | NoVRR bool | 102 | 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 { | |||
| 76 | 76 | composedSince time.Time | |
| 77 | 77 | // kind is how the images were made. | |
| 78 | 78 | 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 | 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 | 120 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | |
| 116 | 121 | log := card.log | |
| 117 | 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 | 124 | var err error | |
| 120 | 125 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 121 | 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 | 273 | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | |
| 269 | 274 | o.testCursor() | |
| 275 | + | o.sendFormats() | |
| 270 | 276 | 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 | 298 | // testModeset asks KMS whether it would take the modeset of the output | |
| 274 | 299 | // images without applying it. | |
| 275 | 300 | func (o *Output) testModeset() error { | |
| @@ -76,6 +76,11 @@ type Output struct { | |||||
| 76 | composedSince time.Time | 76 | composedSince time.Time | ||
| 77 | // kind is how the images were made. | 77 | // kind is how the images were made. | ||
| 78 | kind imageKind | 78 | 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.log | 121 | 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 error | 124 | 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 nil | 276 | 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 output | 298 | // 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "os" | |
| 6 | + | "slices" | |
| 5 | 7 | "time" | |
| 6 | 8 | "unsafe" | |
| 7 | 9 | ||
| @@ -42,6 +44,7 @@ type fbCmd2 struct { | |||
| 42 | 44 | ||
| 43 | 45 | // scanoutCandidate returns the window content the output can scan out | |
| 44 | 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 | 48 | func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) { | |
| 46 | 49 | var full *ports.SceneWindow | |
| 47 | 50 | for i := range s.Windows { | |
| @@ -69,14 +72,10 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 69 | 72 | return c, "not_dmabuf" | |
| 70 | 73 | case len(c.Children) > 0: | |
| 71 | 74 | return c, "subsurfaces" | |
| 72 | - | case len(c.DMABuf.Planes) != 1: | |
| 73 | - | return c, "multi_plane" | |
| 74 | 75 | case c.Width != w || c.Height != h: | |
| 75 | 76 | return c, "size_mismatch" | |
| 76 | 77 | case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H: | |
| 77 | 78 | return c, "logical_mismatch" | |
| 78 | - | case !scanoutable(c.DMABuf.Format): | |
| 79 | - | return c, "format" | |
| 80 | 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 | 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 | 102 | fb := &clientFB{last: now} | |
| 104 | 103 | o.clientFBs[b.ID] = fb | |
| 105 | - | format, ok := scanoutFormat(b.Format) | |
| 104 | + | format, ok := o.scanoutFormat(b) | |
| 106 | 105 | if !ok { | |
| 107 | 106 | fb.failed = "format" | |
| 108 | 107 | return 0, fb.failed | |
| @@ -117,20 +116,57 @@ func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | |||
| 117 | 116 | return id, "" | |
| 118 | 117 | } | |
| 119 | 118 | ||
| 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}) | |
| 127 | 126 | } | |
| 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 | |
| 129 | 155 | } | |
| 130 | 156 | ||
| 131 | 157 | const ( | |
| 132 | 158 | fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 133 | 159 | 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 | 172 | // 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 | 197 | _ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g)) | |
| 162 | 198 | }() | |
| 163 | 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].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 | + | } | |
| 167 | 222 | } | |
| 168 | 223 | if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | |
| 169 | 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 | 226 | 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 | 255 | // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all | |
| 175 | 256 | // of them, except those on screen or queued. | |
| 176 | 257 | func (o *Output) dropClientFBs(now time.Time, all bool) { | |
| @@ -187,5 +268,3 @@ func (o *Output) dropClientFBs(now time.Time, all bool) { | |||
| 187 | 268 | delete(o.clientFBs, id) | |
| 188 | 269 | } | |
| 189 | 270 | } | |
| 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 out | 45 | // 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.SceneWindow | 49 | 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] = fb | 103 | 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.failed | 107 | 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 primary | 119 | + | // 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, true | 125 | + | return slices.Contains(o.primary.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier}) |
| 127 | } | 126 | } | ||
| 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 | |||
| 129 | } | 155 | } | ||
| 130 | 156 | ||||
| 131 | const ( | 157 | const ( | ||
| 132 | fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | 158 | fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | ||
| 133 | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | 159 | 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's | 172 | // 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].Offset | 200 | + | 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.Modifier | 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 | + | } | |||
| 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, nil | 226 | 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 all | 255 | // 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "testing" | |
| 5 | + | "time" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/nefertty/internal/ports" | |
| 8 | + | "github.com/stretchr/testify/mock" | |
| 7 | 9 | ) | |
| 8 | 10 | ||
| 9 | 11 | func TestScanoutCandidate(t *testing.T) { | |
| @@ -38,11 +40,6 @@ func TestScanoutCandidate(t *testing.T) { | |||
| 38 | 40 | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"}, | |
| 39 | 41 | {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"}, | |
| 40 | 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 | 43 | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | |
| 47 | 44 | } { | |
| 48 | 45 | s := scene | |
| @@ -60,11 +57,70 @@ func TestScanoutCandidate(t *testing.T) { | |||
| 60 | 57 | } | |
| 61 | 58 | } | |
| 62 | 59 | ||
| 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) | |
| 68 | 124 | } | |
| 69 | 125 | } | |
| 70 | 126 | } | |
| @@ -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 := scene | 45 | 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 ( | |||
| 15 | 15 | ||
| 16 | 16 | // linux-dmabuf v4: clients hand GPU buffers to the renderer without copies. | |
| 17 | 17 | // 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 | 24 | const dmabufVersion = 4 | |
| 21 | 25 | ||
| @@ -30,7 +34,7 @@ func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error | |||
| 30 | 34 | if err != nil { | |
| 31 | 35 | 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 | 38 | return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) { | |
| 35 | 39 | r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf) | |
| 36 | 40 | if err != nil || v >= 4 { | |
| @@ -77,6 +81,11 @@ type dmabufGlobal struct { | |||
| 77 | 81 | support ports.DMABufSupport | |
| 78 | 82 | table *os.File | |
| 79 | 83 | 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 | 91 | func (g *dmabufGlobal) close() { | |
| @@ -98,35 +107,112 @@ func (g *dmabufGlobal) CreateParams(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) | |||
| 98 | 107 | } | |
| 99 | 108 | ||
| 100 | 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. | |
| 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)) | |
| 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 | 120 | fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{}) | |
| 112 | 121 | if err != nil { | |
| 113 | 122 | return | |
| 114 | 123 | } | |
| 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 | + | } | |
| 117 | 156 | indices := make([]byte, 2*len(g.support.Formats)) | |
| 118 | 157 | for i := range g.support.Formats { | |
| 119 | 158 | binary.NativeEndian.PutUint16(indices[i*2:], uint16(i)) | |
| 120 | 159 | } | |
| 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) | |
| 124 | 161 | fb.SendTrancheFlags(0) | |
| 125 | 162 | fb.SendTrancheFormats(indices) | |
| 126 | 163 | fb.SendTrancheDone() | |
| 127 | 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 | 216 | type feedback struct{} | |
| 131 | 217 | ||
| 132 | 218 | 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); without | 17 | // 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 = 4 | 24 | 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 err | 35 | 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.DMABufSupport | 81 | support ports.DMABufSupport | ||
| 78 | table *os.File | 82 | table *os.File | ||
| 79 | nextID uint64 | 83 | 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 | return | 122 | 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) { | |||
| 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 | + | } | |
| @@ -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 { | |||
| 38 | 38 | // Presented paces frame callbacks on the outputs' page flips; outputs | |
| 39 | 39 | // that do not flip (idle, headless) are paced at their refresh rate. | |
| 40 | 40 | 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 | 45 | type Server struct { | |
| 43 | 46 | display *server.Display | |
| @@ -196,7 +199,7 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 196 | 199 | s.started = time.Now() | |
| 197 | 200 | s.ctx = ctx | |
| 198 | 201 | var wg sync.WaitGroup | |
| 199 | - | wg.Add(5) | |
| 202 | + | wg.Add(6) | |
| 200 | 203 | go func() { defer wg.Done(); s.forward(ctx) }() | |
| 201 | 204 | go func() { defer wg.Done(); s.forwardCursors(ctx) }() | |
| 202 | 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 | 238 | go func() { defer wg.Done(); s.pace(ctx) }() | |
| 239 | + | go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }() | |
| 236 | 240 | err := s.display.Run(ctx) | |
| 237 | 241 | wg.Wait() | |
| 238 | 242 | 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 | 269 | func (s *Server) emit(ev ports.ClientEvent) { | |
| 242 | 270 | if s.channels.Events == nil { | |
| 243 | 271 | return | |
| @@ -473,8 +501,12 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 473 | 501 | return | |
| 474 | 502 | } | |
| 475 | 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 | 505 | w.last, w.hasLast = c, true | |
| 477 | 506 | w.sendConfigure() | |
| 507 | + | if scanoutChanged && w.xdg.surface != nil { | |
| 508 | + | s.dmabuf.resendSurface(w.xdg.surface) | |
| 509 | + | } | |
| 478 | 510 | if surf := w.xdg.surface; surf != nil && c.Output != "" { | |
| 479 | 511 | surf.sendTreeScale() | |
| 480 | 512 | } | |
| @@ -38,6 +38,9 @@ type Channels struct { | |||||
| 38 | // Presented paces frame callbacks on the outputs' page flips; outputs | 38 | // 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.OutputPresented | 40 | 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.Display | 46 | 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 = ctx | 200 | s.ctx = ctx | ||
| 198 | var wg sync.WaitGroup | 201 | 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 err | 242 | 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 | return | 271 | return | ||
| @@ -473,8 +501,12 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 473 | return | 501 | 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, true | 505 | 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) | |||
| 87 | 87 | contents := make(chan ports.SurfaceContent, 64) | |
| 88 | 88 | cursorChanges := make(chan ports.CursorChange, 1) | |
| 89 | 89 | presented := make(chan ports.OutputPresented, 64) | |
| 90 | + | outputFormats := make(chan ports.OutputFormats, 8) | |
| 90 | 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 | 92 | c, err := core.New(opts.Config, ch) | |
| 92 | 93 | if err != nil { | |
| @@ -101,7 +102,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 101 | 102 | // Every output renders on the same GPU: its formats are the clients'. | |
| 102 | 103 | dmabuf := vulkan.Probe() | |
| 103 | 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 | 106 | if err != nil { | |
| 106 | 107 | km.Close() | |
| 107 | 108 | return err | |
| @@ -215,8 +216,12 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 215 | 216 | } | |
| 216 | 217 | if hw != nil { | |
| 217 | 218 | 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")) | |
| 220 | 225 | }) | |
| 221 | 226 | return | |
| 222 | 227 | } | |
| @@ -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 err | 108 | 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 | return | 226 | return | ||
| 222 | } | 227 | } | ||
M internal/app/drm.go +5 -2
| @@ -109,12 +109,13 @@ func (b *drmBackend) close() { | |||
| 109 | 109 | // one flip reader per card, and a udev watcher that rescans connectors on | |
| 110 | 110 | // hotplug. Core learns about outputs through events. It returns when ctx | |
| 111 | 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 | 113 | ctx, cancel := context.WithCancel(ctx) | |
| 114 | 114 | defer cancel() | |
| 115 | 115 | var readers sync.WaitGroup | |
| 116 | 116 | readerErr := make(chan error, len(b.cards)) | |
| 117 | 117 | for _, c := range b.cards { | |
| 118 | + | c.SetFormats(formats) | |
| 118 | 119 | readers.Go(func() { | |
| 119 | 120 | if err := safe("drm events", func() error { return c.ReadEvents(ctx) }); err != nil { | |
| 120 | 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 | 142 | stopping := map[string]bool{} | |
| 142 | 143 | scan := func() { | |
| 143 | 144 | for _, c := range b.cards { | |
| 144 | - | c.SetWant(want()) | |
| 145 | + | w := want() | |
| 146 | + | w.Device = c.Device() | |
| 147 | + | c.SetWant(w) | |
| 145 | 148 | added, removed, replaced, err := c.Scan() | |
| 146 | 149 | if err != nil { | |
| 147 | 150 | 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 on | 109 | // one flip reader per card, and a udev watcher that rescans connectors on | ||
| 110 | // hotplug. Core learns about outputs through events. It returns when ctx | 110 | // 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.WaitGroup | 115 | 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 { | |||
| 329 | 329 | 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 | 343 | // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time | |
| 333 | 344 | // (hardware clock on DRM), Seq the output's vblank counter, Refresh the | |
| 334 | 345 | // refresh period (0 while variable refresh is on). ZeroCopy is set when a | |
| @@ -329,6 +329,17 @@ type OutputPresented struct { | |||||
| 329 | Seen map[WindowID]uint64 | 329 | 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 time | 343 | // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time | ||
| 333 | // (hardware clock on DRM), Seq the output's vblank counter, Refresh the | 344 | // (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 a | 345 | // refresh period (0 while variable refresh is on). ZeroCopy is set when a | ||