merge: scanout (direct scanout of fullscreen client buffers)
17 files · +627 -39
M internal/adapters/drm/card.go +9 -1
| @@ -5,6 +5,8 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "slices" | |
| 8 | + | "sync" | |
| 9 | + | "unsafe" | |
| 8 | 10 | ||
| 9 | 11 | "github.com/bnema/zerowrap" | |
| 10 | 12 | "golang.org/x/sys/unix" | |
| @@ -23,6 +25,10 @@ type Card struct { | |||
| 23 | 25 | // outputs by connector name; owned by the goroutine that calls Scan. | |
| 24 | 26 | outputs map[string]*Output | |
| 25 | 27 | flips map[uint32]chan int // by CRTC; set before the reader starts | |
| 28 | + | // gemMu serialises GEM handle import and close on fd (outputs run on | |
| 29 | + | // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS. | |
| 30 | + | gemMu sync.Mutex | |
| 31 | + | modifiers bool | |
| 26 | 32 | } | |
| 27 | 33 | ||
| 28 | 34 | // OpenCard reads the card's CRTCs. fd stays owned by the caller. | |
| @@ -35,7 +41,9 @@ func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error | |||
| 35 | 41 | for _, c := range crtcs { | |
| 36 | 42 | flips[c] = make(chan int, 4) | |
| 37 | 43 | } | |
| 38 | - | return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips}, nil | |
| 44 | + | cp := getCap{capability: capAddFB2Modifiers} | |
| 45 | + | mods := ioctl(fd, ioctlGetCap, unsafe.Pointer(&cp)) == nil && cp.value == 1 | |
| 46 | + | return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, modifiers: mods}, nil | |
| 39 | 47 | } | |
| 40 | 48 | ||
| 41 | 49 | // Path is the device path, e.g. /dev/dri/card1. | |
| @@ -5,6 +5,8 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "slices" | 7 | "slices" | ||
| 8 | + | "sync" | |||
| 9 | + | "unsafe" | |||
| 8 | 10 | ||||
| 9 | "github.com/bnema/zerowrap" | 11 | "github.com/bnema/zerowrap" | ||
| 10 | "golang.org/x/sys/unix" | 12 | "golang.org/x/sys/unix" | ||
| @@ -23,6 +25,10 @@ type Card struct { | |||||
| 23 | // outputs by connector name; owned by the goroutine that calls Scan. | 25 | // outputs by connector name; owned by the goroutine that calls Scan. | ||
| 24 | outputs map[string]*Output | 26 | outputs map[string]*Output | ||
| 25 | flips map[uint32]chan int // by CRTC; set before the reader starts | 27 | flips map[uint32]chan int // by CRTC; set before the reader starts | ||
| 28 | + | // gemMu serialises GEM handle import and close on fd (outputs run on | |||
| 29 | + | // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS. | |||
| 30 | + | gemMu sync.Mutex | |||
| 31 | + | modifiers bool | |||
| 26 | } | 32 | } | ||
| 27 | 33 | ||||
| 28 | // OpenCard reads the card's CRTCs. fd stays owned by the caller. | 34 | // OpenCard reads the card's CRTCs. fd stays owned by the caller. | ||
| @@ -35,7 +41,9 @@ func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error | |||||
| 35 | for _, c := range crtcs { | 41 | for _, c := range crtcs { | ||
| 36 | flips[c] = make(chan int, 4) | 42 | flips[c] = make(chan int, 4) | ||
| 37 | } | 43 | } | ||
| 38 | - | return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips}, nil | 44 | + | cp := getCap{capability: capAddFB2Modifiers} |
| 45 | + | mods := ioctl(fd, ioctlGetCap, unsafe.Pointer(&cp)) == nil && cp.value == 1 | |||
| 46 | + | return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, modifiers: mods}, nil | |||
| 39 | } | 47 | } | ||
| 40 | 48 | ||||
| 41 | // Path is the device path, e.g. /dev/dri/card1. | 49 | // Path is the device path, e.g. /dev/dri/card1. | ||
M internal/adapters/drm/kms.go +2 -0
| @@ -119,6 +119,8 @@ type Want struct { | |||
| 119 | 119 | Disabled map[string]bool | |
| 120 | 120 | // Modes maps a connector to W, H and Hz (0: highest refresh at W x H). | |
| 121 | 121 | Modes map[string][3]float64 | |
| 122 | + | // NoScanout disables direct scanout (render.direct-scanout = off). | |
| 123 | + | NoScanout bool | |
| 122 | 124 | } | |
| 123 | 125 | ||
| 124 | 126 | // usable reports whether a connector should be driven. | |
| @@ -119,6 +119,8 @@ type Want struct { | |||||
| 119 | Disabled map[string]bool | 119 | Disabled map[string]bool | ||
| 120 | // Modes maps a connector to W, H and Hz (0: highest refresh at W x H). | 120 | // Modes maps a connector to W, H and Hz (0: highest refresh at W x H). | ||
| 121 | Modes map[string][3]float64 | 121 | Modes map[string][3]float64 | ||
| 122 | + | // NoScanout disables direct scanout (render.direct-scanout = off). | |||
| 123 | + | NoScanout bool | |||
| 122 | } | 124 | } | ||
| 123 | 125 | ||||
| 124 | // usable reports whether a connector should be driven. | 126 | // usable reports whether a connector should be driven. | ||
M internal/adapters/drm/kms_test.go +3 -0
| @@ -20,6 +20,9 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||
| 20 | 20 | {"page_flip", unsafe.Sizeof(pageFlip{}), ioctlPageFlip}, | |
| 21 | 21 | {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb}, | |
| 22 | 22 | {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb}, | |
| 23 | + | {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2}, | |
| 24 | + | {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle}, | |
| 25 | + | {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose}, | |
| 23 | 26 | } { | |
| 24 | 27 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | |
| 25 | 28 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | |
| @@ -20,6 +20,9 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||||
| 20 | {"page_flip", unsafe.Sizeof(pageFlip{}), ioctlPageFlip}, | 20 | {"page_flip", unsafe.Sizeof(pageFlip{}), ioctlPageFlip}, | ||
| 21 | {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb}, | 21 | {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb}, | ||
| 22 | {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb}, | 22 | {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb}, | ||
| 23 | + | {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2}, | |||
| 24 | + | {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle}, | |||
| 25 | + | {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose}, | |||
| 23 | } { | 26 | } { | ||
| 24 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | 27 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | ||
| 25 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | 28 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | ||
M internal/adapters/drm/output.go +110 -8
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | + | "maps" | |
| 7 | 8 | "slices" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| @@ -29,6 +30,14 @@ type Output struct { | |||
| 29 | 30 | monitor Monitor | |
| 30 | 31 | cursor *Cursor | |
| 31 | 32 | log zerowrap.Logger | |
| 33 | + | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | |
| 34 | + | // and the buffer on screen and queued (0: the composed image). | |
| 35 | + | card *Card | |
| 36 | + | scanout bool | |
| 37 | + | clientFBs map[uint64]*clientFB | |
| 38 | + | shown, queued uint64 | |
| 39 | + | reason string // why the last frame was composed ("" = scanout) | |
| 40 | + | unsent ports.OutputPresented | |
| 32 | 41 | } | |
| 33 | 42 | ||
| 34 | 43 | // CursorLoader returns the image of a cursor at an output scale, at most | |
| @@ -38,7 +47,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu | |||
| 38 | 47 | // newOutput allocates buffers and the cursor for a connector on crtc. | |
| 39 | 48 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | |
| 40 | 49 | log := card.log | |
| 41 | - | o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name)} | |
| 50 | + | o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"} | |
| 42 | 51 | var err error | |
| 43 | 52 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 44 | 53 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | |
| @@ -76,6 +85,7 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) } | |||
| 76 | 85 | // modeset shows the front buffer; needed at start and after every VT resume. | |
| 77 | 86 | func (o *Output) modeset() error { | |
| 78 | 87 | o.pending = false | |
| 88 | + | o.shown, o.queued = 0, 0 | |
| 79 | 89 | front := o.bufs[1-o.back] | |
| 80 | 90 | if err := setCrtc(o.fd, o.crtc, o.conn.id, front.fbID, &o.mode); err != nil { | |
| 81 | 91 | return fmt.Errorf("set crtc: %w", err) | |
| @@ -92,10 +102,50 @@ func (o *Output) present(r ports.Renderer) error { | |||
| 92 | 102 | return err | |
| 93 | 103 | } | |
| 94 | 104 | o.pending, o.flipStart = true, time.Now() | |
| 105 | + | o.queued = 0 | |
| 95 | 106 | o.back = 1 - o.back | |
| 96 | 107 | return nil | |
| 97 | 108 | } | |
| 98 | 109 | ||
| 110 | + | // tryScanout flips a fullscreen client buffer directly. It reports false, | |
| 111 | + | // with the reason logged on change, when the frame must be composed. | |
| 112 | + | func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (bool, error) { | |
| 113 | + | reason := "disabled" | |
| 114 | + | var c ports.SurfaceContent | |
| 115 | + | if o.scanout { | |
| 116 | + | c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height()) | |
| 117 | + | } | |
| 118 | + | var fb uint32 | |
| 119 | + | if reason == "" { | |
| 120 | + | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | |
| 121 | + | } | |
| 122 | + | if reason != o.reason { | |
| 123 | + | o.log.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout") | |
| 124 | + | o.reason = reason | |
| 125 | + | } | |
| 126 | + | if reason != "" { | |
| 127 | + | return false, nil | |
| 128 | + | } | |
| 129 | + | if o.shown == c.DMABuf.ID && o.queued == 0 { | |
| 130 | + | // Already on screen: nothing to flip. | |
| 131 | + | return true, nil | |
| 132 | + | } | |
| 133 | + | if err := flip(o.fd, o.crtc, fb); err != nil { | |
| 134 | + | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | |
| 135 | + | // KMS refuses this buffer on the plane: compose it instead. | |
| 136 | + | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | |
| 137 | + | o.clientFBs[c.DMABuf.ID].failed = "flip_refused" | |
| 138 | + | o.reason = "flip_refused" | |
| 139 | + | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | |
| 140 | + | return false, nil | |
| 141 | + | } | |
| 142 | + | return true, err | |
| 143 | + | } | |
| 144 | + | o.pending, o.flipStart = true, time.Now() | |
| 145 | + | o.queued = c.DMABuf.ID | |
| 146 | + | return true, nil | |
| 147 | + | } | |
| 148 | + | ||
| 99 | 149 | // Close restores the CRTC state found at startup, when it had a mode, and | |
| 100 | 150 | // frees buffers. | |
| 101 | 151 | func (o *Output) Close() { | |
| @@ -114,6 +164,8 @@ func (o *Output) Close() { | |||
| 114 | 164 | b.destroy(o.fd) | |
| 115 | 165 | } | |
| 116 | 166 | } | |
| 167 | + | o.shown, o.queued = 0, 0 | |
| 168 | + | o.dropClientFBs(time.Now(), true) | |
| 117 | 169 | } | |
| 118 | 170 | ||
| 119 | 171 | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | |
| @@ -144,8 +196,26 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 144 | 196 | frame := 0 | |
| 145 | 197 | stats := time.NewTicker(10 * time.Second) | |
| 146 | 198 | defer stats.Stop() | |
| 199 | + | // seen is the latest content Seq per window, reported to wayland with | |
| 200 | + | // what the output scans out, after each frame decision. | |
| 201 | + | seen := map[ports.WindowID]uint64{} | |
| 202 | + | reportDirty := false | |
| 203 | + | flipped := false | |
| 147 | 204 | for { | |
| 205 | + | if flipped || reportDirty { | |
| 206 | + | o.report(presented, flipped, seen) | |
| 207 | + | flipped, reportDirty = false, false | |
| 208 | + | } else { | |
| 209 | + | o.flushReport(presented) | |
| 210 | + | } | |
| 211 | + | // An unsent report is retried soon, not only on the next event. | |
| 212 | + | var retry <-chan time.Time | |
| 213 | + | if o.unsent.Output != "" { | |
| 214 | + | retry = time.After(time.Millisecond) | |
| 215 | + | } | |
| 148 | 216 | select { | |
| 217 | + | case <-retry: | |
| 218 | + | continue | |
| 149 | 219 | case <-ctx.Done(): | |
| 150 | 220 | return nil | |
| 151 | 221 | case on := <-active: | |
| @@ -170,10 +240,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 170 | 240 | } | |
| 171 | 241 | o.pending = false | |
| 172 | 242 | o.flips += n | |
| 173 | - | select { | |
| 174 | - | case presented <- ports.OutputPresented{Output: o.conn.name}: | |
| 175 | - | default: | |
| 176 | - | } | |
| 243 | + | o.shown = o.queued | |
| 244 | + | o.queued = 0 | |
| 245 | + | flipped = true | |
| 177 | 246 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | |
| 178 | 247 | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | |
| 179 | 248 | } | |
| @@ -196,9 +265,14 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 196 | 265 | } else { | |
| 197 | 266 | surfaces[c.ID] = c | |
| 198 | 267 | } | |
| 268 | + | if c.Seq > seen[c.ID] { | |
| 269 | + | seen[c.ID] = c.Seq | |
| 270 | + | reportDirty = true | |
| 271 | + | } | |
| 199 | 272 | // Windows on other outputs or workspaces do not need a frame. | |
| 200 | 273 | dirty = dirty || scene.Shows(c.ID) | |
| 201 | 274 | case <-stats.C: | |
| 275 | + | o.dropClientFBs(time.Now(), false) | |
| 202 | 276 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | |
| 203 | 277 | if o.cursor != nil { | |
| 204 | 278 | cs := o.cursor.TakeStats() | |
| @@ -210,11 +284,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 210 | 284 | continue | |
| 211 | 285 | } | |
| 212 | 286 | start := time.Now() | |
| 213 | - | if err := r.Render(scene, surfaces); err != nil { | |
| 214 | - | return fmt.Errorf("render frame: %w", err) | |
| 287 | + | direct, err := o.tryScanout(scene, surfaces) | |
| 288 | + | if !direct { | |
| 289 | + | if err := r.Render(scene, surfaces); err != nil { | |
| 290 | + | return fmt.Errorf("render frame: %w", err) | |
| 291 | + | } | |
| 292 | + | err = o.present(r) | |
| 215 | 293 | } | |
| 216 | 294 | copyStart := time.Now() | |
| 217 | - | if err := o.present(r); err != nil { | |
| 295 | + | if err != nil { | |
| 218 | 296 | if errors.Is(err, unix.EBUSY) { | |
| 219 | 297 | // A flip is still in flight; retry after its event. | |
| 220 | 298 | o.pending, o.flipStart = true, time.Now() | |
| @@ -230,6 +308,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 230 | 308 | return fmt.Errorf("page flip: %w", err) | |
| 231 | 309 | } | |
| 232 | 310 | dirty = false | |
| 311 | + | reportDirty = true | |
| 233 | 312 | frame++ | |
| 234 | 313 | o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("render", time.Since(start)).Dur("copy", time.Since(copyStart)).Msg("frame") | |
| 235 | 314 | } | |
| @@ -247,3 +326,26 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6 | |||
| 247 | 326 | } | |
| 248 | 327 | o.log.Debug().Float64("scale", scale).Str("shape", c.Shape).Bool("client", c.Image != nil).Int("w", img.W).Int("h", img.H).Msg("cursor image") | |
| 249 | 328 | } | |
| 329 | + | ||
| 330 | + | // report tells wayland what the output shows and has read, so replaced | |
| 331 | + | // client buffers can be released. It must follow the frame decision: a | |
| 332 | + | // buffer is safe once a later content is seen and it is neither shown nor | |
| 333 | + | // queued. A report the channel cannot take waits in unsent, replaced by | |
| 334 | + | // newer ones (Flip kept), and is retried on the next loop. | |
| 335 | + | func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) { | |
| 336 | + | r := ports.OutputPresented{Output: o.conn.name, Flip: flip || o.unsent.Flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | |
| 337 | + | o.unsent = r | |
| 338 | + | o.flushReport(presented) | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | // flushReport sends the unsent report, if any, without blocking. | |
| 342 | + | func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | |
| 343 | + | if o.unsent.Output == "" { | |
| 344 | + | return | |
| 345 | + | } | |
| 346 | + | select { | |
| 347 | + | case presented <- o.unsent: | |
| 348 | + | o.unsent = ports.OutputPresented{} | |
| 349 | + | default: | |
| 350 | + | } | |
| 351 | + | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | + | "maps" | |||
| 7 | "slices" | 8 | "slices" | ||
| 8 | "time" | 9 | "time" | ||
| 9 | 10 | ||||
| @@ -29,6 +30,14 @@ type Output struct { | |||||
| 29 | monitor Monitor | 30 | monitor Monitor | ||
| 30 | cursor *Cursor | 31 | cursor *Cursor | ||
| 31 | log zerowrap.Logger | 32 | log zerowrap.Logger | ||
| 33 | + | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | |||
| 34 | + | // and the buffer on screen and queued (0: the composed image). | |||
| 35 | + | card *Card | |||
| 36 | + | scanout bool | |||
| 37 | + | clientFBs map[uint64]*clientFB | |||
| 38 | + | shown, queued uint64 | |||
| 39 | + | reason string // why the last frame was composed ("" = scanout) | |||
| 40 | + | unsent ports.OutputPresented | |||
| 32 | } | 41 | } | ||
| 33 | 42 | ||||
| 34 | // CursorLoader returns the image of a cursor at an output scale, at most | 43 | // CursorLoader returns the image of a cursor at an output scale, at most | ||
| @@ -38,7 +47,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu | |||||
| 38 | // newOutput allocates buffers and the cursor for a connector on crtc. | 47 | // newOutput allocates buffers and the cursor for a connector on crtc. | ||
| 39 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | 48 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | ||
| 40 | log := card.log | 49 | log := card.log | ||
| 41 | - | o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name)} | 50 | + | o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), card: card, scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"} |
| 42 | var err error | 51 | var err error | ||
| 43 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | 52 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | ||
| 44 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | 53 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | ||
| @@ -76,6 +85,7 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) } | |||||
| 76 | // modeset shows the front buffer; needed at start and after every VT resume. | 85 | // modeset shows the front buffer; needed at start and after every VT resume. | ||
| 77 | func (o *Output) modeset() error { | 86 | func (o *Output) modeset() error { | ||
| 78 | o.pending = false | 87 | o.pending = false | ||
| 88 | + | o.shown, o.queued = 0, 0 | |||
| 79 | front := o.bufs[1-o.back] | 89 | front := o.bufs[1-o.back] | ||
| 80 | if err := setCrtc(o.fd, o.crtc, o.conn.id, front.fbID, &o.mode); err != nil { | 90 | if err := setCrtc(o.fd, o.crtc, o.conn.id, front.fbID, &o.mode); err != nil { | ||
| 81 | return fmt.Errorf("set crtc: %w", err) | 91 | return fmt.Errorf("set crtc: %w", err) | ||
| @@ -92,10 +102,50 @@ func (o *Output) present(r ports.Renderer) error { | |||||
| 92 | return err | 102 | return err | ||
| 93 | } | 103 | } | ||
| 94 | o.pending, o.flipStart = true, time.Now() | 104 | o.pending, o.flipStart = true, time.Now() | ||
| 105 | + | o.queued = 0 | |||
| 95 | o.back = 1 - o.back | 106 | o.back = 1 - o.back | ||
| 96 | return nil | 107 | return nil | ||
| 97 | } | 108 | } | ||
| 98 | 109 | ||||
| 110 | + | // tryScanout flips a fullscreen client buffer directly. It reports false, | |||
| 111 | + | // with the reason logged on change, when the frame must be composed. | |||
| 112 | + | func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (bool, error) { | |||
| 113 | + | reason := "disabled" | |||
| 114 | + | var c ports.SurfaceContent | |||
| 115 | + | if o.scanout { | |||
| 116 | + | c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height()) | |||
| 117 | + | } | |||
| 118 | + | var fb uint32 | |||
| 119 | + | if reason == "" { | |||
| 120 | + | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | |||
| 121 | + | } | |||
| 122 | + | if reason != o.reason { | |||
| 123 | + | o.log.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout") | |||
| 124 | + | o.reason = reason | |||
| 125 | + | } | |||
| 126 | + | if reason != "" { | |||
| 127 | + | return false, nil | |||
| 128 | + | } | |||
| 129 | + | if o.shown == c.DMABuf.ID && o.queued == 0 { | |||
| 130 | + | // Already on screen: nothing to flip. | |||
| 131 | + | return true, nil | |||
| 132 | + | } | |||
| 133 | + | if err := flip(o.fd, o.crtc, fb); err != nil { | |||
| 134 | + | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | |||
| 135 | + | // KMS refuses this buffer on the plane: compose it instead. | |||
| 136 | + | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | |||
| 137 | + | o.clientFBs[c.DMABuf.ID].failed = "flip_refused" | |||
| 138 | + | o.reason = "flip_refused" | |||
| 139 | + | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | |||
| 140 | + | return false, nil | |||
| 141 | + | } | |||
| 142 | + | return true, err | |||
| 143 | + | } | |||
| 144 | + | o.pending, o.flipStart = true, time.Now() | |||
| 145 | + | o.queued = c.DMABuf.ID | |||
| 146 | + | return true, nil | |||
| 147 | + | } | |||
| 148 | + | ||||
| 99 | // Close restores the CRTC state found at startup, when it had a mode, and | 149 | // Close restores the CRTC state found at startup, when it had a mode, and | ||
| 100 | // frees buffers. | 150 | // frees buffers. | ||
| 101 | func (o *Output) Close() { | 151 | func (o *Output) Close() { | ||
| @@ -114,6 +164,8 @@ func (o *Output) Close() { | |||||
| 114 | b.destroy(o.fd) | 164 | b.destroy(o.fd) | ||
| 115 | } | 165 | } | ||
| 116 | } | 166 | } | ||
| 167 | + | o.shown, o.queued = 0, 0 | |||
| 168 | + | o.dropClientFBs(time.Now(), true) | |||
| 117 | } | 169 | } | ||
| 118 | 170 | ||||
| 119 | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | 171 | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | ||
| @@ -144,8 +196,26 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 144 | frame := 0 | 196 | frame := 0 | ||
| 145 | stats := time.NewTicker(10 * time.Second) | 197 | stats := time.NewTicker(10 * time.Second) | ||
| 146 | defer stats.Stop() | 198 | defer stats.Stop() | ||
| 199 | + | // seen is the latest content Seq per window, reported to wayland with | |||
| 200 | + | // what the output scans out, after each frame decision. | |||
| 201 | + | seen := map[ports.WindowID]uint64{} | |||
| 202 | + | reportDirty := false | |||
| 203 | + | flipped := false | |||
| 147 | for { | 204 | for { | ||
| 205 | + | if flipped || reportDirty { | |||
| 206 | + | o.report(presented, flipped, seen) | |||
| 207 | + | flipped, reportDirty = false, false | |||
| 208 | + | } else { | |||
| 209 | + | o.flushReport(presented) | |||
| 210 | + | } | |||
| 211 | + | // An unsent report is retried soon, not only on the next event. | |||
| 212 | + | var retry <-chan time.Time | |||
| 213 | + | if o.unsent.Output != "" { | |||
| 214 | + | retry = time.After(time.Millisecond) | |||
| 215 | + | } | |||
| 148 | select { | 216 | select { | ||
| 217 | + | case <-retry: | |||
| 218 | + | continue | |||
| 149 | case <-ctx.Done(): | 219 | case <-ctx.Done(): | ||
| 150 | return nil | 220 | return nil | ||
| 151 | case on := <-active: | 221 | case on := <-active: | ||
| @@ -170,10 +240,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 170 | } | 240 | } | ||
| 171 | o.pending = false | 241 | o.pending = false | ||
| 172 | o.flips += n | 242 | o.flips += n | ||
| 173 | - | select { | 243 | + | o.shown = o.queued |
| 174 | - | case presented <- ports.OutputPresented{Output: o.conn.name}: | 244 | + | o.queued = 0 |
| 175 | - | default: | 245 | + | flipped = true |
| 176 | - | } | |||
| 177 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | 246 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | ||
| 178 | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | 247 | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | ||
| 179 | } | 248 | } | ||
| @@ -196,9 +265,14 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 196 | } else { | 265 | } else { | ||
| 197 | surfaces[c.ID] = c | 266 | surfaces[c.ID] = c | ||
| 198 | } | 267 | } | ||
| 268 | + | if c.Seq > seen[c.ID] { | |||
| 269 | + | seen[c.ID] = c.Seq | |||
| 270 | + | reportDirty = true | |||
| 271 | + | } | |||
| 199 | // Windows on other outputs or workspaces do not need a frame. | 272 | // Windows on other outputs or workspaces do not need a frame. | ||
| 200 | dirty = dirty || scene.Shows(c.ID) | 273 | dirty = dirty || scene.Shows(c.ID) | ||
| 201 | case <-stats.C: | 274 | case <-stats.C: | ||
| 275 | + | o.dropClientFBs(time.Now(), false) | |||
| 202 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | 276 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | ||
| 203 | if o.cursor != nil { | 277 | if o.cursor != nil { | ||
| 204 | cs := o.cursor.TakeStats() | 278 | cs := o.cursor.TakeStats() | ||
| @@ -210,11 +284,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 210 | continue | 284 | continue | ||
| 211 | } | 285 | } | ||
| 212 | start := time.Now() | 286 | start := time.Now() | ||
| 213 | - | if err := r.Render(scene, surfaces); err != nil { | 287 | + | direct, err := o.tryScanout(scene, surfaces) |
| 214 | - | return fmt.Errorf("render frame: %w", err) | 288 | + | if !direct { |
| 289 | + | if err := r.Render(scene, surfaces); err != nil { | |||
| 290 | + | return fmt.Errorf("render frame: %w", err) | |||
| 291 | + | } | |||
| 292 | + | err = o.present(r) | |||
| 215 | } | 293 | } | ||
| 216 | copyStart := time.Now() | 294 | copyStart := time.Now() | ||
| 217 | - | if err := o.present(r); err != nil { | 295 | + | if err != nil { |
| 218 | if errors.Is(err, unix.EBUSY) { | 296 | if errors.Is(err, unix.EBUSY) { | ||
| 219 | // A flip is still in flight; retry after its event. | 297 | // A flip is still in flight; retry after its event. | ||
| 220 | o.pending, o.flipStart = true, time.Now() | 298 | o.pending, o.flipStart = true, time.Now() | ||
| @@ -230,6 +308,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 230 | return fmt.Errorf("page flip: %w", err) | 308 | return fmt.Errorf("page flip: %w", err) | ||
| 231 | } | 309 | } | ||
| 232 | dirty = false | 310 | dirty = false | ||
| 311 | + | reportDirty = true | |||
| 233 | frame++ | 312 | frame++ | ||
| 234 | o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("render", time.Since(start)).Dur("copy", time.Since(copyStart)).Msg("frame") | 313 | o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Dur("render", time.Since(start)).Dur("copy", time.Since(copyStart)).Msg("frame") | ||
| 235 | } | 314 | } | ||
| @@ -247,3 +326,26 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6 | |||||
| 247 | } | 326 | } | ||
| 248 | o.log.Debug().Float64("scale", scale).Str("shape", c.Shape).Bool("client", c.Image != nil).Int("w", img.W).Int("h", img.H).Msg("cursor image") | 327 | o.log.Debug().Float64("scale", scale).Str("shape", c.Shape).Bool("client", c.Image != nil).Int("w", img.W).Int("h", img.H).Msg("cursor image") | ||
| 249 | } | 328 | } | ||
| 329 | + | ||||
| 330 | + | // report tells wayland what the output shows and has read, so replaced | |||
| 331 | + | // client buffers can be released. It must follow the frame decision: a | |||
| 332 | + | // buffer is safe once a later content is seen and it is neither shown nor | |||
| 333 | + | // queued. A report the channel cannot take waits in unsent, replaced by | |||
| 334 | + | // newer ones (Flip kept), and is retried on the next loop. | |||
| 335 | + | func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) { | |||
| 336 | + | r := ports.OutputPresented{Output: o.conn.name, Flip: flip || o.unsent.Flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | |||
| 337 | + | o.unsent = r | |||
| 338 | + | o.flushReport(presented) | |||
| 339 | + | } | |||
| 340 | + | ||||
| 341 | + | // flushReport sends the unsent report, if any, without blocking. | |||
| 342 | + | func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | |||
| 343 | + | if o.unsent.Output == "" { | |||
| 344 | + | return | |||
| 345 | + | } | |||
| 346 | + | select { | |||
| 347 | + | case presented <- o.unsent: | |||
| 348 | + | o.unsent = ports.OutputPresented{} | |||
| 349 | + | default: | |||
| 350 | + | } | |||
| 351 | + | } | |||
A internal/adapters/drm/scanout.go +189 -0
| @@ -0,0 +1,189 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "time" | |
| 6 | + | "unsafe" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Direct scanout (ADR 006): when one fullscreen window covers the output | |
| 12 | + | // with a GPU buffer of the output's size, the output flips to that buffer | |
| 13 | + | // instead of compositing it. The buffer becomes a KMS framebuffer once | |
| 14 | + | // (PRIME import + ADDFB2) and is cached by DMABuf ID while the client | |
| 15 | + | // keeps using it. KMS waits on the buffer's implicit fences before showing | |
| 16 | + | // it. Anything else on screen, a scale, or a buffer KMS refuses falls back | |
| 17 | + | // to composition. | |
| 18 | + | ||
| 19 | + | const ( | |
| 20 | + | ioctlGemClose = 0x40086409 | |
| 21 | + | ioctlPrimeFDToHandle = 0xC00C642E | |
| 22 | + | ioctlAddFB2 = 0xC06864B8 | |
| 23 | + | ||
| 24 | + | fbModifiers = 1 << 1 | |
| 25 | + | capAddFB2Modifiers = 0x10 | |
| 26 | + | modInvalid = 0x00ffffffffffffff | |
| 27 | + | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | type primeHandle struct { | |
| 31 | + | handle, flags uint32 | |
| 32 | + | fd int32 | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | type gemClose struct{ handle, pad uint32 } | |
| 36 | + | ||
| 37 | + | type fbCmd2 struct { | |
| 38 | + | fbID, width, height, format, flags uint32 | |
| 39 | + | handles, pitches, offsets [4]uint32 | |
| 40 | + | modifiers [4]uint64 | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // 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. | |
| 45 | + | func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) { | |
| 46 | + | var full *ports.SceneWindow | |
| 47 | + | for i := range s.Windows { | |
| 48 | + | win := &s.Windows[i] | |
| 49 | + | if win.Hidden || win.Rect.W <= 0 || win.Rect.H <= 0 { | |
| 50 | + | continue | |
| 51 | + | } | |
| 52 | + | covers := win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight | |
| 53 | + | if !win.Fullscreen || !covers || full != nil { | |
| 54 | + | return ports.SurfaceContent{}, "other_windows" | |
| 55 | + | } | |
| 56 | + | full = win | |
| 57 | + | } | |
| 58 | + | if full == nil { | |
| 59 | + | return ports.SurfaceContent{}, "no_fullscreen" | |
| 60 | + | } | |
| 61 | + | for _, l := range s.Layers { | |
| 62 | + | if l.Layer == ports.LayerOverlay && l.Rect.W > 0 && l.Rect.H > 0 { | |
| 63 | + | return ports.SurfaceContent{}, "overlay_surface" | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | c := surfaces[full.ID] | |
| 67 | + | switch { | |
| 68 | + | case c.DMABuf == nil: | |
| 69 | + | return c, "not_dmabuf" | |
| 70 | + | case len(c.Children) > 0: | |
| 71 | + | return c, "subsurfaces" | |
| 72 | + | case len(c.DMABuf.Planes) != 1: | |
| 73 | + | return c, "multi_plane" | |
| 74 | + | case c.Width != w || c.Height != h: | |
| 75 | + | return c, "size_mismatch" | |
| 76 | + | case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H: | |
| 77 | + | 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): | |
| 81 | + | return c, "geometry_crop" | |
| 82 | + | } | |
| 83 | + | return c, "" | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // clientFB is a client buffer imported as a KMS framebuffer. | |
| 87 | + | type clientFB struct { | |
| 88 | + | fbID uint32 | |
| 89 | + | last time.Time // when last wanted on screen | |
| 90 | + | // failed is the reason KMS refused the buffer; it is not retried. | |
| 91 | + | failed string | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // scanoutFB returns the framebuffer of a client buffer, importing it on | |
| 95 | + | // first use. A buffer KMS refuses is remembered with its reason. | |
| 96 | + | func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | |
| 97 | + | if fb := o.clientFBs[b.ID]; fb != nil { | |
| 98 | + | fb.last = now | |
| 99 | + | return fb.fbID, fb.failed | |
| 100 | + | } | |
| 101 | + | fb := &clientFB{last: now} | |
| 102 | + | o.clientFBs[b.ID] = fb | |
| 103 | + | id, err := o.card.addClientFB(b) | |
| 104 | + | if err != nil { | |
| 105 | + | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | |
| 106 | + | fb.failed = "import_failed" | |
| 107 | + | return 0, fb.failed | |
| 108 | + | } | |
| 109 | + | fb.fbID = id | |
| 110 | + | return id, "" | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // scanoutFormat is the framebuffer format for a client format. The primary | |
| 114 | + | // plane shows no alpha, and a legacy page flip keeps the format of the | |
| 115 | + | // composed image (XRGB8888): alpha formats are scanned out as their X | |
| 116 | + | // variant, others are refused. | |
| 117 | + | func scanoutFormat(f uint32) (uint32, bool) { | |
| 118 | + | switch f { | |
| 119 | + | case fourccXRGB, fourccARGB: | |
| 120 | + | return fourccXRGB, true | |
| 121 | + | } | |
| 122 | + | return 0, false | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | const ( | |
| 126 | + | fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 127 | + | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 128 | + | ) | |
| 129 | + | ||
| 130 | + | // addClientFB imports a dmabuf as a framebuffer. The GEM handle belongs to | |
| 131 | + | // the card fd and is shared by every import of the same buffer, so import, | |
| 132 | + | // ADDFB2 and close run under the card lock and the handle is closed right | |
| 133 | + | // away: the framebuffer keeps its own reference to the buffer. | |
| 134 | + | func (c *Card) addClientFB(b *ports.DMABuf) (uint32, error) { | |
| 135 | + | format, ok := scanoutFormat(b.Format) | |
| 136 | + | if !ok { | |
| 137 | + | return 0, errors.New("format not scanned out") | |
| 138 | + | } | |
| 139 | + | raw, err := b.Planes[0].File.SyscallConn() | |
| 140 | + | if err != nil { | |
| 141 | + | return 0, err | |
| 142 | + | } | |
| 143 | + | c.gemMu.Lock() | |
| 144 | + | defer c.gemMu.Unlock() | |
| 145 | + | var p primeHandle | |
| 146 | + | var ioErr error | |
| 147 | + | if err := raw.Control(func(fd uintptr) { | |
| 148 | + | p.fd = int32(fd) | |
| 149 | + | ioErr = ioctl(c.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | |
| 150 | + | }); err != nil { | |
| 151 | + | return 0, err | |
| 152 | + | } | |
| 153 | + | if ioErr != nil { | |
| 154 | + | return 0, errors.Join(errors.New("prime import"), ioErr) | |
| 155 | + | } | |
| 156 | + | defer func() { | |
| 157 | + | g := gemClose{handle: p.handle} | |
| 158 | + | _ = ioctl(c.fd, ioctlGemClose, unsafe.Pointer(&g)) | |
| 159 | + | }() | |
| 160 | + | cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format} | |
| 161 | + | cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = p.handle, b.Planes[0].Stride, b.Planes[0].Offset | |
| 162 | + | if b.Modifier != modInvalid && (b.Modifier != 0 || c.modifiers) { | |
| 163 | + | cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier | |
| 164 | + | } | |
| 165 | + | if err := ioctl(c.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | |
| 166 | + | return 0, errors.Join(errors.New("add fb2"), err) | |
| 167 | + | } | |
| 168 | + | return cmd.fbID, nil | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all | |
| 172 | + | // of them, except those on screen or queued. | |
| 173 | + | func (o *Output) dropClientFBs(now time.Time, all bool) { | |
| 174 | + | for id, fb := range o.clientFBs { | |
| 175 | + | if id == o.shown || id == o.queued { | |
| 176 | + | continue | |
| 177 | + | } | |
| 178 | + | if !all && now.Sub(fb.last) < scanoutIdleTTL { | |
| 179 | + | continue | |
| 180 | + | } | |
| 181 | + | if fb.fbID != 0 { | |
| 182 | + | v := fb.fbID | |
| 183 | + | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v)) | |
| 184 | + | } | |
| 185 | + | delete(o.clientFBs, id) | |
| 186 | + | } | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | func scanoutable(f uint32) bool { _, ok := scanoutFormat(f); return ok } | |
| @@ -0,0 +1,189 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "time" | |||
| 6 | + | "unsafe" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Direct scanout (ADR 006): when one fullscreen window covers the output | |||
| 12 | + | // with a GPU buffer of the output's size, the output flips to that buffer | |||
| 13 | + | // instead of compositing it. The buffer becomes a KMS framebuffer once | |||
| 14 | + | // (PRIME import + ADDFB2) and is cached by DMABuf ID while the client | |||
| 15 | + | // keeps using it. KMS waits on the buffer's implicit fences before showing | |||
| 16 | + | // it. Anything else on screen, a scale, or a buffer KMS refuses falls back | |||
| 17 | + | // to composition. | |||
| 18 | + | ||||
| 19 | + | const ( | |||
| 20 | + | ioctlGemClose = 0x40086409 | |||
| 21 | + | ioctlPrimeFDToHandle = 0xC00C642E | |||
| 22 | + | ioctlAddFB2 = 0xC06864B8 | |||
| 23 | + | ||||
| 24 | + | fbModifiers = 1 << 1 | |||
| 25 | + | capAddFB2Modifiers = 0x10 | |||
| 26 | + | modInvalid = 0x00ffffffffffffff | |||
| 27 | + | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | |||
| 28 | + | ) | |||
| 29 | + | ||||
| 30 | + | type primeHandle struct { | |||
| 31 | + | handle, flags uint32 | |||
| 32 | + | fd int32 | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | type gemClose struct{ handle, pad uint32 } | |||
| 36 | + | ||||
| 37 | + | type fbCmd2 struct { | |||
| 38 | + | fbID, width, height, format, flags uint32 | |||
| 39 | + | handles, pitches, offsets [4]uint32 | |||
| 40 | + | modifiers [4]uint64 | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // 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. | |||
| 45 | + | func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) { | |||
| 46 | + | var full *ports.SceneWindow | |||
| 47 | + | for i := range s.Windows { | |||
| 48 | + | win := &s.Windows[i] | |||
| 49 | + | if win.Hidden || win.Rect.W <= 0 || win.Rect.H <= 0 { | |||
| 50 | + | continue | |||
| 51 | + | } | |||
| 52 | + | covers := win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight | |||
| 53 | + | if !win.Fullscreen || !covers || full != nil { | |||
| 54 | + | return ports.SurfaceContent{}, "other_windows" | |||
| 55 | + | } | |||
| 56 | + | full = win | |||
| 57 | + | } | |||
| 58 | + | if full == nil { | |||
| 59 | + | return ports.SurfaceContent{}, "no_fullscreen" | |||
| 60 | + | } | |||
| 61 | + | for _, l := range s.Layers { | |||
| 62 | + | if l.Layer == ports.LayerOverlay && l.Rect.W > 0 && l.Rect.H > 0 { | |||
| 63 | + | return ports.SurfaceContent{}, "overlay_surface" | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | c := surfaces[full.ID] | |||
| 67 | + | switch { | |||
| 68 | + | case c.DMABuf == nil: | |||
| 69 | + | return c, "not_dmabuf" | |||
| 70 | + | case len(c.Children) > 0: | |||
| 71 | + | return c, "subsurfaces" | |||
| 72 | + | case len(c.DMABuf.Planes) != 1: | |||
| 73 | + | return c, "multi_plane" | |||
| 74 | + | case c.Width != w || c.Height != h: | |||
| 75 | + | return c, "size_mismatch" | |||
| 76 | + | case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H: | |||
| 77 | + | 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): | |||
| 81 | + | return c, "geometry_crop" | |||
| 82 | + | } | |||
| 83 | + | return c, "" | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // clientFB is a client buffer imported as a KMS framebuffer. | |||
| 87 | + | type clientFB struct { | |||
| 88 | + | fbID uint32 | |||
| 89 | + | last time.Time // when last wanted on screen | |||
| 90 | + | // failed is the reason KMS refused the buffer; it is not retried. | |||
| 91 | + | failed string | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // scanoutFB returns the framebuffer of a client buffer, importing it on | |||
| 95 | + | // first use. A buffer KMS refuses is remembered with its reason. | |||
| 96 | + | func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { | |||
| 97 | + | if fb := o.clientFBs[b.ID]; fb != nil { | |||
| 98 | + | fb.last = now | |||
| 99 | + | return fb.fbID, fb.failed | |||
| 100 | + | } | |||
| 101 | + | fb := &clientFB{last: now} | |||
| 102 | + | o.clientFBs[b.ID] = fb | |||
| 103 | + | id, err := o.card.addClientFB(b) | |||
| 104 | + | if err != nil { | |||
| 105 | + | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") | |||
| 106 | + | fb.failed = "import_failed" | |||
| 107 | + | return 0, fb.failed | |||
| 108 | + | } | |||
| 109 | + | fb.fbID = id | |||
| 110 | + | return id, "" | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // scanoutFormat is the framebuffer format for a client format. The primary | |||
| 114 | + | // plane shows no alpha, and a legacy page flip keeps the format of the | |||
| 115 | + | // composed image (XRGB8888): alpha formats are scanned out as their X | |||
| 116 | + | // variant, others are refused. | |||
| 117 | + | func scanoutFormat(f uint32) (uint32, bool) { | |||
| 118 | + | switch f { | |||
| 119 | + | case fourccXRGB, fourccARGB: | |||
| 120 | + | return fourccXRGB, true | |||
| 121 | + | } | |||
| 122 | + | return 0, false | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | const ( | |||
| 126 | + | fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | |||
| 127 | + | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | |||
| 128 | + | ) | |||
| 129 | + | ||||
| 130 | + | // addClientFB imports a dmabuf as a framebuffer. The GEM handle belongs to | |||
| 131 | + | // the card fd and is shared by every import of the same buffer, so import, | |||
| 132 | + | // ADDFB2 and close run under the card lock and the handle is closed right | |||
| 133 | + | // away: the framebuffer keeps its own reference to the buffer. | |||
| 134 | + | func (c *Card) addClientFB(b *ports.DMABuf) (uint32, error) { | |||
| 135 | + | format, ok := scanoutFormat(b.Format) | |||
| 136 | + | if !ok { | |||
| 137 | + | return 0, errors.New("format not scanned out") | |||
| 138 | + | } | |||
| 139 | + | raw, err := b.Planes[0].File.SyscallConn() | |||
| 140 | + | if err != nil { | |||
| 141 | + | return 0, err | |||
| 142 | + | } | |||
| 143 | + | c.gemMu.Lock() | |||
| 144 | + | defer c.gemMu.Unlock() | |||
| 145 | + | var p primeHandle | |||
| 146 | + | var ioErr error | |||
| 147 | + | if err := raw.Control(func(fd uintptr) { | |||
| 148 | + | p.fd = int32(fd) | |||
| 149 | + | ioErr = ioctl(c.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | |||
| 150 | + | }); err != nil { | |||
| 151 | + | return 0, err | |||
| 152 | + | } | |||
| 153 | + | if ioErr != nil { | |||
| 154 | + | return 0, errors.Join(errors.New("prime import"), ioErr) | |||
| 155 | + | } | |||
| 156 | + | defer func() { | |||
| 157 | + | g := gemClose{handle: p.handle} | |||
| 158 | + | _ = ioctl(c.fd, ioctlGemClose, unsafe.Pointer(&g)) | |||
| 159 | + | }() | |||
| 160 | + | cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format} | |||
| 161 | + | cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = p.handle, b.Planes[0].Stride, b.Planes[0].Offset | |||
| 162 | + | if b.Modifier != modInvalid && (b.Modifier != 0 || c.modifiers) { | |||
| 163 | + | cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier | |||
| 164 | + | } | |||
| 165 | + | if err := ioctl(c.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | |||
| 166 | + | return 0, errors.Join(errors.New("add fb2"), err) | |||
| 167 | + | } | |||
| 168 | + | return cmd.fbID, nil | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all | |||
| 172 | + | // of them, except those on screen or queued. | |||
| 173 | + | func (o *Output) dropClientFBs(now time.Time, all bool) { | |||
| 174 | + | for id, fb := range o.clientFBs { | |||
| 175 | + | if id == o.shown || id == o.queued { | |||
| 176 | + | continue | |||
| 177 | + | } | |||
| 178 | + | if !all && now.Sub(fb.last) < scanoutIdleTTL { | |||
| 179 | + | continue | |||
| 180 | + | } | |||
| 181 | + | if fb.fbID != 0 { | |||
| 182 | + | v := fb.fbID | |||
| 183 | + | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v)) | |||
| 184 | + | } | |||
| 185 | + | delete(o.clientFBs, id) | |||
| 186 | + | } | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | func scanoutable(f uint32) bool { _, ok := scanoutFormat(f); return ok } | |||
A internal/adapters/drm/scanout_test.go +70 -0
| @@ -0,0 +1,70 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestScanoutCandidate(t *testing.T) { | |
| 10 | + | full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true} | |
| 11 | + | scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{full}} | |
| 12 | + | buf := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccARGB, Planes: []ports.DMABufPlane{{}}} | |
| 13 | + | game := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, DMABuf: buf} | |
| 14 | + | for _, tc := range []struct { | |
| 15 | + | name string | |
| 16 | + | scene func(s *ports.Scene) | |
| 17 | + | cont func(c *ports.SurfaceContent) | |
| 18 | + | reason string | |
| 19 | + | }{ | |
| 20 | + | {"scanout", nil, nil, ""}, | |
| 21 | + | {"hidden others ignored", func(s *ports.Scene) { | |
| 22 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Hidden: true}) | |
| 23 | + | }, nil, ""}, | |
| 24 | + | {"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"}, | |
| 25 | + | {"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"}, | |
| 26 | + | {"float above", func(s *ports.Scene) { | |
| 27 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Rect: ports.Rect{W: 10, H: 10}}) | |
| 28 | + | }, nil, "other_windows"}, | |
| 29 | + | {"not covering", func(s *ports.Scene) { s.Windows[0].Rect.W = 90 }, nil, "other_windows"}, | |
| 30 | + | {"overlay layer", func(s *ports.Scene) { | |
| 31 | + | s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}} | |
| 32 | + | }, nil, "overlay_surface"}, | |
| 33 | + | {"top layer covered", func(s *ports.Scene) { | |
| 34 | + | s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerTop, Rect: ports.Rect{W: 10, H: 10}}} | |
| 35 | + | }, nil, ""}, | |
| 36 | + | {"shm", nil, func(c *ports.SurfaceContent) { c.DMABuf, c.SHM = nil, &ports.SHMBuffer{} }, "not_dmabuf"}, | |
| 37 | + | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | |
| 38 | + | {"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"}, | |
| 40 | + | {"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} }, ""}, | |
| 47 | + | } { | |
| 48 | + | s := scene | |
| 49 | + | s.Windows = append([]ports.SceneWindow(nil), scene.Windows...) | |
| 50 | + | c := game | |
| 51 | + | if tc.scene != nil { | |
| 52 | + | tc.scene(&s) | |
| 53 | + | } | |
| 54 | + | if tc.cont != nil { | |
| 55 | + | tc.cont(&c) | |
| 56 | + | } | |
| 57 | + | if _, reason := scanoutCandidate(s, map[ports.WindowID]ports.SurfaceContent{1: c}, 200, 100); reason != tc.reason { | |
| 58 | + | t.Errorf("%s: reason %q, want %q", tc.name, reason, tc.reason) | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | ||
| 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) | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | } | |
| @@ -0,0 +1,70 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestScanoutCandidate(t *testing.T) { | |||
| 10 | + | full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true} | |||
| 11 | + | scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{full}} | |||
| 12 | + | buf := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccARGB, Planes: []ports.DMABufPlane{{}}} | |||
| 13 | + | game := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, DMABuf: buf} | |||
| 14 | + | for _, tc := range []struct { | |||
| 15 | + | name string | |||
| 16 | + | scene func(s *ports.Scene) | |||
| 17 | + | cont func(c *ports.SurfaceContent) | |||
| 18 | + | reason string | |||
| 19 | + | }{ | |||
| 20 | + | {"scanout", nil, nil, ""}, | |||
| 21 | + | {"hidden others ignored", func(s *ports.Scene) { | |||
| 22 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Hidden: true}) | |||
| 23 | + | }, nil, ""}, | |||
| 24 | + | {"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"}, | |||
| 25 | + | {"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"}, | |||
| 26 | + | {"float above", func(s *ports.Scene) { | |||
| 27 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Rect: ports.Rect{W: 10, H: 10}}) | |||
| 28 | + | }, nil, "other_windows"}, | |||
| 29 | + | {"not covering", func(s *ports.Scene) { s.Windows[0].Rect.W = 90 }, nil, "other_windows"}, | |||
| 30 | + | {"overlay layer", func(s *ports.Scene) { | |||
| 31 | + | s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}} | |||
| 32 | + | }, nil, "overlay_surface"}, | |||
| 33 | + | {"top layer covered", func(s *ports.Scene) { | |||
| 34 | + | s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerTop, Rect: ports.Rect{W: 10, H: 10}}} | |||
| 35 | + | }, nil, ""}, | |||
| 36 | + | {"shm", nil, func(c *ports.SurfaceContent) { c.DMABuf, c.SHM = nil, &ports.SHMBuffer{} }, "not_dmabuf"}, | |||
| 37 | + | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | |||
| 38 | + | {"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"}, | |||
| 40 | + | {"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} }, ""}, | |||
| 47 | + | } { | |||
| 48 | + | s := scene | |||
| 49 | + | s.Windows = append([]ports.SceneWindow(nil), scene.Windows...) | |||
| 50 | + | c := game | |||
| 51 | + | if tc.scene != nil { | |||
| 52 | + | tc.scene(&s) | |||
| 53 | + | } | |||
| 54 | + | if tc.cont != nil { | |||
| 55 | + | tc.cont(&c) | |||
| 56 | + | } | |||
| 57 | + | if _, reason := scanoutCandidate(s, map[ports.WindowID]ports.SurfaceContent{1: c}, 200, 100); reason != tc.reason { | |||
| 58 | + | t.Errorf("%s: reason %q, want %q", tc.name, reason, tc.reason) | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | ||||
| 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) | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | } | |||
M internal/adapters/headless/output.go +39 -0
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "image" | |
| 7 | 7 | "image/png" | |
| 8 | + | "maps" | |
| 8 | 9 | "os" | |
| 9 | 10 | "path/filepath" | |
| 10 | 11 | "time" | |
| @@ -22,6 +23,10 @@ type Options struct { | |||
| 22 | 23 | ScreenshotDir string | |
| 23 | 24 | Log zerowrap.Logger | |
| 24 | 25 | NewRenderer func(w, h int) (ports.Renderer, error) | |
| 26 | + | // Name and Presented report what the output has read after each | |
| 27 | + | // frame, so wayland can release client buffers (nil: no reports). | |
| 28 | + | Name string | |
| 29 | + | Presented chan<- ports.OutputPresented | |
| 25 | 30 | } | |
| 26 | 31 | ||
| 27 | 32 | func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange) error { | |
| @@ -36,7 +41,11 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 36 | 41 | frame := 0 | |
| 37 | 42 | var want ports.CursorChange | |
| 38 | 43 | cursorScale := -1.0 // not loaded yet | |
| 44 | + | seen := map[ports.WindowID]uint64{} | |
| 45 | + | // pending is a report the channel could not take, retried soon. | |
| 46 | + | var pending *ports.OutputPresented | |
| 39 | 47 | update := func(c ports.SurfaceContent) { | |
| 48 | + | seen[c.ID] = max(seen[c.ID], c.Seq) | |
| 40 | 49 | dirty = dirty || scene.Shows(c.ID) | |
| 41 | 50 | if c.Empty() { | |
| 42 | 51 | delete(surfaces, c.ID) | |
| @@ -48,9 +57,16 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 48 | 57 | if ctx.Err() != nil { | |
| 49 | 58 | return nil | |
| 50 | 59 | } | |
| 60 | + | var retry <-chan time.Time | |
| 61 | + | if pending != nil { | |
| 62 | + | retry = time.After(time.Millisecond) | |
| 63 | + | } | |
| 51 | 64 | select { | |
| 52 | 65 | case <-ctx.Done(): | |
| 53 | 66 | return nil | |
| 67 | + | case <-retry: | |
| 68 | + | pending = opts.send(pending) | |
| 69 | + | continue | |
| 54 | 70 | case s, ok := <-scenes: | |
| 55 | 71 | if !ok { | |
| 56 | 72 | scenes = nil | |
| @@ -88,6 +104,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 88 | 104 | } | |
| 89 | 105 | } | |
| 90 | 106 | if !haveScene || !dirty { | |
| 107 | + | // Contents not drawn are still read: report them. | |
| 108 | + | pending = opts.report(pending, seen) | |
| 91 | 109 | continue | |
| 92 | 110 | } | |
| 93 | 111 | dirty = false | |
| @@ -104,6 +122,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 104 | 122 | return fmt.Errorf("render frame: %w", err) | |
| 105 | 123 | } | |
| 106 | 124 | frame++ | |
| 125 | + | pending = opts.report(pending, seen) | |
| 107 | 126 | if opts.ScreenshotDir != "" { | |
| 108 | 127 | shot := r.Pixels() | |
| 109 | 128 | if opts.Cursor != nil { | |
| @@ -135,3 +154,23 @@ func writePNG(path string, img *image.RGBA) error { | |||
| 135 | 154 | } | |
| 136 | 155 | return os.Rename(f.Name(), path) | |
| 137 | 156 | } | |
| 157 | + | ||
| 158 | + | // report sends what the output read; a full channel drops it. | |
| 159 | + | // report sends what the output read, or returns it to retry when the | |
| 160 | + | // channel is full. Headless frames are not paced by a screen: Flip stays | |
| 161 | + | // false and frame callbacks follow the refresh timer. | |
| 162 | + | func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented { | |
| 163 | + | if opts.Presented == nil { | |
| 164 | + | return nil | |
| 165 | + | } | |
| 166 | + | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)}) | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |
| 170 | + | select { | |
| 171 | + | case opts.Presented <- *r: | |
| 172 | + | return nil | |
| 173 | + | default: | |
| 174 | + | return r | |
| 175 | + | } | |
| 176 | + | } | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "image" | 6 | "image" | ||
| 7 | "image/png" | 7 | "image/png" | ||
| 8 | + | "maps" | |||
| 8 | "os" | 9 | "os" | ||
| 9 | "path/filepath" | 10 | "path/filepath" | ||
| 10 | "time" | 11 | "time" | ||
| @@ -22,6 +23,10 @@ type Options struct { | |||||
| 22 | ScreenshotDir string | 23 | ScreenshotDir string | ||
| 23 | Log zerowrap.Logger | 24 | Log zerowrap.Logger | ||
| 24 | NewRenderer func(w, h int) (ports.Renderer, error) | 25 | NewRenderer func(w, h int) (ports.Renderer, error) | ||
| 26 | + | // Name and Presented report what the output has read after each | |||
| 27 | + | // frame, so wayland can release client buffers (nil: no reports). | |||
| 28 | + | Name string | |||
| 29 | + | Presented chan<- ports.OutputPresented | |||
| 25 | } | 30 | } | ||
| 26 | 31 | ||||
| 27 | func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange) error { | 32 | func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange) error { | ||
| @@ -36,7 +41,11 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 36 | frame := 0 | 41 | frame := 0 | ||
| 37 | var want ports.CursorChange | 42 | var want ports.CursorChange | ||
| 38 | cursorScale := -1.0 // not loaded yet | 43 | cursorScale := -1.0 // not loaded yet | ||
| 44 | + | seen := map[ports.WindowID]uint64{} | |||
| 45 | + | // pending is a report the channel could not take, retried soon. | |||
| 46 | + | var pending *ports.OutputPresented | |||
| 39 | update := func(c ports.SurfaceContent) { | 47 | update := func(c ports.SurfaceContent) { | ||
| 48 | + | seen[c.ID] = max(seen[c.ID], c.Seq) | |||
| 40 | dirty = dirty || scene.Shows(c.ID) | 49 | dirty = dirty || scene.Shows(c.ID) | ||
| 41 | if c.Empty() { | 50 | if c.Empty() { | ||
| 42 | delete(surfaces, c.ID) | 51 | delete(surfaces, c.ID) | ||
| @@ -48,9 +57,16 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 48 | if ctx.Err() != nil { | 57 | if ctx.Err() != nil { | ||
| 49 | return nil | 58 | return nil | ||
| 50 | } | 59 | } | ||
| 60 | + | var retry <-chan time.Time | |||
| 61 | + | if pending != nil { | |||
| 62 | + | retry = time.After(time.Millisecond) | |||
| 63 | + | } | |||
| 51 | select { | 64 | select { | ||
| 52 | case <-ctx.Done(): | 65 | case <-ctx.Done(): | ||
| 53 | return nil | 66 | return nil | ||
| 67 | + | case <-retry: | |||
| 68 | + | pending = opts.send(pending) | |||
| 69 | + | continue | |||
| 54 | case s, ok := <-scenes: | 70 | case s, ok := <-scenes: | ||
| 55 | if !ok { | 71 | if !ok { | ||
| 56 | scenes = nil | 72 | scenes = nil | ||
| @@ -88,6 +104,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 88 | } | 104 | } | ||
| 89 | } | 105 | } | ||
| 90 | if !haveScene || !dirty { | 106 | if !haveScene || !dirty { | ||
| 107 | + | // Contents not drawn are still read: report them. | |||
| 108 | + | pending = opts.report(pending, seen) | |||
| 91 | continue | 109 | continue | ||
| 92 | } | 110 | } | ||
| 93 | dirty = false | 111 | dirty = false | ||
| @@ -104,6 +122,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 104 | return fmt.Errorf("render frame: %w", err) | 122 | return fmt.Errorf("render frame: %w", err) | ||
| 105 | } | 123 | } | ||
| 106 | frame++ | 124 | frame++ | ||
| 125 | + | pending = opts.report(pending, seen) | |||
| 107 | if opts.ScreenshotDir != "" { | 126 | if opts.ScreenshotDir != "" { | ||
| 108 | shot := r.Pixels() | 127 | shot := r.Pixels() | ||
| 109 | if opts.Cursor != nil { | 128 | if opts.Cursor != nil { | ||
| @@ -135,3 +154,23 @@ func writePNG(path string, img *image.RGBA) error { | |||||
| 135 | } | 154 | } | ||
| 136 | return os.Rename(f.Name(), path) | 155 | return os.Rename(f.Name(), path) | ||
| 137 | } | 156 | } | ||
| 157 | + | ||||
| 158 | + | // report sends what the output read; a full channel drops it. | |||
| 159 | + | // report sends what the output read, or returns it to retry when the | |||
| 160 | + | // channel is full. Headless frames are not paced by a screen: Flip stays | |||
| 161 | + | // false and frame callbacks follow the refresh timer. | |||
| 162 | + | func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented { | |||
| 163 | + | if opts.Presented == nil { | |||
| 164 | + | return nil | |||
| 165 | + | } | |||
| 166 | + | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)}) | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |||
| 170 | + | select { | |||
| 171 | + | case opts.Presented <- *r: | |||
| 172 | + | return nil | |||
| 173 | + | default: | |||
| 174 | + | return r | |||
| 175 | + | } | |||
| 176 | + | } | |||
M internal/adapters/wayland/frames.go +11 -2
| @@ -5,6 +5,8 @@ import ( | |||
| 5 | 5 | "slices" | |
| 6 | 6 | "time" | |
| 7 | 7 | ||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |
| 9 | + | ||
| 8 | 10 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 9 | 11 | ) | |
| 10 | 12 | ||
| @@ -25,13 +27,15 @@ func (s *Server) pace(ctx context.Context) { | |||
| 25 | 27 | presented := s.channels.Presented | |
| 26 | 28 | for { | |
| 27 | 29 | flipped := map[string]bool{} | |
| 30 | + | var reports []ports.OutputPresented | |
| 28 | 31 | select { | |
| 29 | 32 | case <-ctx.Done(): | |
| 30 | 33 | return | |
| 31 | 34 | case <-s.display.Stopped(): | |
| 32 | 35 | return | |
| 33 | 36 | case p := <-presented: | |
| 34 | - | flipped[p.Output] = true | |
| 37 | + | flipped[p.Output] = flipped[p.Output] || p.Flip | |
| 38 | + | reports = append(reports, p) | |
| 35 | 39 | case <-s.frameReady: | |
| 36 | 40 | case <-timer.C: | |
| 37 | 41 | } | |
| @@ -41,7 +45,8 @@ func (s *Server) pace(ctx context.Context) { | |||
| 41 | 45 | for { | |
| 42 | 46 | select { | |
| 43 | 47 | case p := <-presented: | |
| 44 | - | flipped[p.Output] = true | |
| 48 | + | flipped[p.Output] = flipped[p.Output] || p.Flip | |
| 49 | + | reports = append(reports, p) | |
| 45 | 50 | default: | |
| 46 | 51 | break drain | |
| 47 | 52 | } | |
| @@ -54,6 +59,10 @@ func (s *Server) pace(ctx context.Context) { | |||
| 54 | 59 | for _, name := range due { | |
| 55 | 60 | s.sendFrames(name) | |
| 56 | 61 | } | |
| 62 | + | if s.releaseHeld(time.Now(), reports) && idle { | |
| 63 | + | // Held buffers wait for a flip or heldTimeout. | |
| 64 | + | wait, idle = heldTimeout, false | |
| 65 | + | } | |
| 57 | 66 | } | |
| 58 | 67 | }) { | |
| 59 | 68 | return | |
| @@ -5,6 +5,8 @@ import ( | |||||
| 5 | "slices" | 5 | "slices" | ||
| 6 | "time" | 6 | "time" | ||
| 7 | 7 | ||||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 9 | + | ||||
| 8 | "github.com/bnema/purego-libwayland/protocol/wayland" | 10 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 9 | ) | 11 | ) | ||
| 10 | 12 | ||||
| @@ -25,13 +27,15 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 25 | presented := s.channels.Presented | 27 | presented := s.channels.Presented | ||
| 26 | for { | 28 | for { | ||
| 27 | flipped := map[string]bool{} | 29 | flipped := map[string]bool{} | ||
| 30 | + | var reports []ports.OutputPresented | |||
| 28 | select { | 31 | select { | ||
| 29 | case <-ctx.Done(): | 32 | case <-ctx.Done(): | ||
| 30 | return | 33 | return | ||
| 31 | case <-s.display.Stopped(): | 34 | case <-s.display.Stopped(): | ||
| 32 | return | 35 | return | ||
| 33 | case p := <-presented: | 36 | case p := <-presented: | ||
| 34 | - | flipped[p.Output] = true | 37 | + | flipped[p.Output] = flipped[p.Output] || p.Flip |
| 38 | + | reports = append(reports, p) | |||
| 35 | case <-s.frameReady: | 39 | case <-s.frameReady: | ||
| 36 | case <-timer.C: | 40 | case <-timer.C: | ||
| 37 | } | 41 | } | ||
| @@ -41,7 +45,8 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 41 | for { | 45 | for { | ||
| 42 | select { | 46 | select { | ||
| 43 | case p := <-presented: | 47 | case p := <-presented: | ||
| 44 | - | flipped[p.Output] = true | 48 | + | flipped[p.Output] = flipped[p.Output] || p.Flip |
| 49 | + | reports = append(reports, p) | |||
| 45 | default: | 50 | default: | ||
| 46 | break drain | 51 | break drain | ||
| 47 | } | 52 | } | ||
| @@ -54,6 +59,10 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 54 | for _, name := range due { | 59 | for _, name := range due { | ||
| 55 | s.sendFrames(name) | 60 | s.sendFrames(name) | ||
| 56 | } | 61 | } | ||
| 62 | + | if s.releaseHeld(time.Now(), reports) && idle { | |||
| 63 | + | // Held buffers wait for a flip or heldTimeout. | |||
| 64 | + | wait, idle = heldTimeout, false | |||
| 65 | + | } | |||
| 57 | } | 66 | } | ||
| 58 | }) { | 67 | }) { | ||
| 59 | return | 68 | return | ||
A internal/adapters/wayland/release.go +108 -0
| @@ -0,0 +1,108 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "time" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Buffer release. Outputs read client buffers in place: the renderer | |
| 11 | + | // samples them when it draws, and an output scanning out a client buffer | |
| 12 | + | // (ADR 006) shows it until the next flip. A replaced wl_buffer therefore | |
| 13 | + | // goes back to the client (release) only when the output showing its | |
| 14 | + | // window reports (ports.OutputPresented) that it has a later content of | |
| 15 | + | // that window and neither shows nor queues the buffer. Outputs keep only | |
| 16 | + | // the latest content of a window, so they never draw an older buffer | |
| 17 | + | // again. heldTimeout guards against an output that stopped reporting | |
| 18 | + | // (switched away, stalled): a buffer it does not scan out is released then. | |
| 19 | + | ||
| 20 | + | // heldTimeout releases held buffers of an output that stopped reporting. | |
| 21 | + | const heldTimeout = 100 * time.Millisecond | |
| 22 | + | ||
| 23 | + | type heldBuffer struct { | |
| 24 | + | res *wayland.Buffer | |
| 25 | + | window ports.WindowID | |
| 26 | + | id uint64 // DMABuf ID, 0 for wl_shm | |
| 27 | + | after uint64 // the window's content Seq when the buffer was replaced | |
| 28 | + | at time.Time | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // releaseBuffer releases a replaced buffer, or holds it until the output | |
| 32 | + | // showing its window no longer reads it. | |
| 33 | + | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |
| 34 | + | if !b.Resource.Alive() { | |
| 35 | + | return | |
| 36 | + | } | |
| 37 | + | window, name := surf.root().windowID(), s.frameOutput(surf) | |
| 38 | + | if window == 0 || name == "" { | |
| 39 | + | // Not drawn (unmapped, hidden, a cursor): nothing reads it. | |
| 40 | + | b.SendRelease() | |
| 41 | + | return | |
| 42 | + | } | |
| 43 | + | h := heldBuffer{res: b, window: window, at: time.Now()} | |
| 44 | + | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | |
| 45 | + | h.id = d.buf.ID | |
| 46 | + | } | |
| 47 | + | s.contentMu.Lock() | |
| 48 | + | h.after = s.contentSeq[window] | |
| 49 | + | s.contentMu.Unlock() | |
| 50 | + | s.held = append(s.held, h) | |
| 51 | + | // Wake the pacer: it releases the buffer on the next output report. | |
| 52 | + | select { | |
| 53 | + | case s.frameReady <- struct{}{}: | |
| 54 | + | default: | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // releaseHeld records the output reports and releases the buffers no | |
| 59 | + | // output reads. Every output reads every window's content, so a buffer | |
| 60 | + | // waits until no live output shows or queues it and all of them have seen | |
| 61 | + | // a later content of its window (or heldTimeout passed). It reports | |
| 62 | + | // whether buffers are still held. | |
| 63 | + | func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool { | |
| 64 | + | for _, r := range reports { | |
| 65 | + | s.reports[r.Output] = r | |
| 66 | + | } | |
| 67 | + | live := make([]ports.OutputPresented, 0, len(s.outputs)) | |
| 68 | + | for name := range s.reports { | |
| 69 | + | if s.outputByNameExact(name) == nil { | |
| 70 | + | delete(s.reports, name) | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | for _, r := range s.reports { | |
| 74 | + | live = append(live, r) | |
| 75 | + | } | |
| 76 | + | kept := s.held[:0] | |
| 77 | + | for _, h := range s.held { | |
| 78 | + | if keepHeld(h, live, now) { | |
| 79 | + | kept = append(kept, h) | |
| 80 | + | continue | |
| 81 | + | } | |
| 82 | + | if h.res.Resource.Alive() { | |
| 83 | + | h.res.SendRelease() | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | clear(s.held[len(kept):]) | |
| 87 | + | s.held = kept | |
| 88 | + | return len(s.held) > 0 | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // keepHeld reports whether an output may still read a held buffer: one | |
| 92 | + | // scans it out or queued it, or one has not got a later content of its | |
| 93 | + | // window yet and heldTimeout has not passed. | |
| 94 | + | func keepHeld(h heldBuffer, reports []ports.OutputPresented, now time.Time) bool { | |
| 95 | + | seen := true | |
| 96 | + | for _, r := range reports { | |
| 97 | + | if h.id != 0 && (r.Shown == h.id || r.Queued == h.id) { | |
| 98 | + | return true | |
| 99 | + | } | |
| 100 | + | if r.Seen[h.window] <= h.after { | |
| 101 | + | seen = false | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | if len(reports) == 0 { | |
| 105 | + | seen = false | |
| 106 | + | } | |
| 107 | + | return !seen && now.Sub(h.at) < heldTimeout | |
| 108 | + | } | |
| @@ -0,0 +1,108 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "time" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Buffer release. Outputs read client buffers in place: the renderer | |||
| 11 | + | // samples them when it draws, and an output scanning out a client buffer | |||
| 12 | + | // (ADR 006) shows it until the next flip. A replaced wl_buffer therefore | |||
| 13 | + | // goes back to the client (release) only when the output showing its | |||
| 14 | + | // window reports (ports.OutputPresented) that it has a later content of | |||
| 15 | + | // that window and neither shows nor queues the buffer. Outputs keep only | |||
| 16 | + | // the latest content of a window, so they never draw an older buffer | |||
| 17 | + | // again. heldTimeout guards against an output that stopped reporting | |||
| 18 | + | // (switched away, stalled): a buffer it does not scan out is released then. | |||
| 19 | + | ||||
| 20 | + | // heldTimeout releases held buffers of an output that stopped reporting. | |||
| 21 | + | const heldTimeout = 100 * time.Millisecond | |||
| 22 | + | ||||
| 23 | + | type heldBuffer struct { | |||
| 24 | + | res *wayland.Buffer | |||
| 25 | + | window ports.WindowID | |||
| 26 | + | id uint64 // DMABuf ID, 0 for wl_shm | |||
| 27 | + | after uint64 // the window's content Seq when the buffer was replaced | |||
| 28 | + | at time.Time | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // releaseBuffer releases a replaced buffer, or holds it until the output | |||
| 32 | + | // showing its window no longer reads it. | |||
| 33 | + | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |||
| 34 | + | if !b.Resource.Alive() { | |||
| 35 | + | return | |||
| 36 | + | } | |||
| 37 | + | window, name := surf.root().windowID(), s.frameOutput(surf) | |||
| 38 | + | if window == 0 || name == "" { | |||
| 39 | + | // Not drawn (unmapped, hidden, a cursor): nothing reads it. | |||
| 40 | + | b.SendRelease() | |||
| 41 | + | return | |||
| 42 | + | } | |||
| 43 | + | h := heldBuffer{res: b, window: window, at: time.Now()} | |||
| 44 | + | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | |||
| 45 | + | h.id = d.buf.ID | |||
| 46 | + | } | |||
| 47 | + | s.contentMu.Lock() | |||
| 48 | + | h.after = s.contentSeq[window] | |||
| 49 | + | s.contentMu.Unlock() | |||
| 50 | + | s.held = append(s.held, h) | |||
| 51 | + | // Wake the pacer: it releases the buffer on the next output report. | |||
| 52 | + | select { | |||
| 53 | + | case s.frameReady <- struct{}{}: | |||
| 54 | + | default: | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // releaseHeld records the output reports and releases the buffers no | |||
| 59 | + | // output reads. Every output reads every window's content, so a buffer | |||
| 60 | + | // waits until no live output shows or queues it and all of them have seen | |||
| 61 | + | // a later content of its window (or heldTimeout passed). It reports | |||
| 62 | + | // whether buffers are still held. | |||
| 63 | + | func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool { | |||
| 64 | + | for _, r := range reports { | |||
| 65 | + | s.reports[r.Output] = r | |||
| 66 | + | } | |||
| 67 | + | live := make([]ports.OutputPresented, 0, len(s.outputs)) | |||
| 68 | + | for name := range s.reports { | |||
| 69 | + | if s.outputByNameExact(name) == nil { | |||
| 70 | + | delete(s.reports, name) | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | for _, r := range s.reports { | |||
| 74 | + | live = append(live, r) | |||
| 75 | + | } | |||
| 76 | + | kept := s.held[:0] | |||
| 77 | + | for _, h := range s.held { | |||
| 78 | + | if keepHeld(h, live, now) { | |||
| 79 | + | kept = append(kept, h) | |||
| 80 | + | continue | |||
| 81 | + | } | |||
| 82 | + | if h.res.Resource.Alive() { | |||
| 83 | + | h.res.SendRelease() | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | clear(s.held[len(kept):]) | |||
| 87 | + | s.held = kept | |||
| 88 | + | return len(s.held) > 0 | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // keepHeld reports whether an output may still read a held buffer: one | |||
| 92 | + | // scans it out or queued it, or one has not got a later content of its | |||
| 93 | + | // window yet and heldTimeout has not passed. | |||
| 94 | + | func keepHeld(h heldBuffer, reports []ports.OutputPresented, now time.Time) bool { | |||
| 95 | + | seen := true | |||
| 96 | + | for _, r := range reports { | |||
| 97 | + | if h.id != 0 && (r.Shown == h.id || r.Queued == h.id) { | |||
| 98 | + | return true | |||
| 99 | + | } | |||
| 100 | + | if r.Seen[h.window] <= h.after { | |||
| 101 | + | seen = false | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | if len(reports) == 0 { | |||
| 105 | + | seen = false | |||
| 106 | + | } | |||
| 107 | + | return !seen && now.Sub(h.at) < heldTimeout | |||
| 108 | + | } | |||
A internal/adapters/wayland/release_test.go +43 -0
| @@ -0,0 +1,43 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestKeepHeld(t *testing.T) { | |
| 11 | + | now := time.Unix(100, 0) | |
| 12 | + | dma := heldBuffer{window: 1, id: 7, after: 5, at: now} | |
| 13 | + | shm := heldBuffer{window: 1, after: 5, at: now} | |
| 14 | + | out := func(shown, queued, seq uint64) ports.OutputPresented { | |
| 15 | + | return ports.OutputPresented{Shown: shown, Queued: queued, Seen: map[ports.WindowID]uint64{1: seq}} | |
| 16 | + | } | |
| 17 | + | type R = []ports.OutputPresented | |
| 18 | + | for _, tc := range []struct { | |
| 19 | + | name string | |
| 20 | + | h heldBuffer | |
| 21 | + | reports R | |
| 22 | + | after time.Duration | |
| 23 | + | want bool | |
| 24 | + | }{ | |
| 25 | + | {"no report yet", dma, nil, 0, true}, | |
| 26 | + | {"no report times out", dma, nil, heldTimeout, false}, | |
| 27 | + | {"output has only this content", dma, R{out(0, 0, 5)}, 0, true}, | |
| 28 | + | {"later content seen", dma, R{out(0, 0, 6)}, 0, false}, | |
| 29 | + | {"scanned out", dma, R{out(7, 0, 6)}, time.Second, true}, | |
| 30 | + | {"queued for scanout", dma, R{out(0, 7, 6)}, time.Second, true}, | |
| 31 | + | {"another buffer scanned out", dma, R{out(8, 0, 6)}, 0, false}, | |
| 32 | + | {"silent output times out", dma, R{out(0, 0, 5)}, heldTimeout, false}, | |
| 33 | + | {"shm follows content", shm, R{out(0, 0, 6)}, 0, false}, | |
| 34 | + | // A window moved off output A: A still scans the buffer out. | |
| 35 | + | {"other output scans it out", dma, R{out(0, 0, 6), out(7, 0, 6)}, time.Second, true}, | |
| 36 | + | {"other output lags", dma, R{out(0, 0, 6), out(0, 0, 5)}, 0, true}, | |
| 37 | + | {"all outputs moved on", dma, R{out(0, 0, 6), out(0, 0, 6)}, 0, false}, | |
| 38 | + | } { | |
| 39 | + | if got := keepHeld(tc.h, tc.reports, now.Add(tc.after)); got != tc.want { | |
| 40 | + | t.Errorf("%s: %v", tc.name, got) | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | } | |
| @@ -0,0 +1,43 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestKeepHeld(t *testing.T) { | |||
| 11 | + | now := time.Unix(100, 0) | |||
| 12 | + | dma := heldBuffer{window: 1, id: 7, after: 5, at: now} | |||
| 13 | + | shm := heldBuffer{window: 1, after: 5, at: now} | |||
| 14 | + | out := func(shown, queued, seq uint64) ports.OutputPresented { | |||
| 15 | + | return ports.OutputPresented{Shown: shown, Queued: queued, Seen: map[ports.WindowID]uint64{1: seq}} | |||
| 16 | + | } | |||
| 17 | + | type R = []ports.OutputPresented | |||
| 18 | + | for _, tc := range []struct { | |||
| 19 | + | name string | |||
| 20 | + | h heldBuffer | |||
| 21 | + | reports R | |||
| 22 | + | after time.Duration | |||
| 23 | + | want bool | |||
| 24 | + | }{ | |||
| 25 | + | {"no report yet", dma, nil, 0, true}, | |||
| 26 | + | {"no report times out", dma, nil, heldTimeout, false}, | |||
| 27 | + | {"output has only this content", dma, R{out(0, 0, 5)}, 0, true}, | |||
| 28 | + | {"later content seen", dma, R{out(0, 0, 6)}, 0, false}, | |||
| 29 | + | {"scanned out", dma, R{out(7, 0, 6)}, time.Second, true}, | |||
| 30 | + | {"queued for scanout", dma, R{out(0, 7, 6)}, time.Second, true}, | |||
| 31 | + | {"another buffer scanned out", dma, R{out(8, 0, 6)}, 0, false}, | |||
| 32 | + | {"silent output times out", dma, R{out(0, 0, 5)}, heldTimeout, false}, | |||
| 33 | + | {"shm follows content", shm, R{out(0, 0, 6)}, 0, false}, | |||
| 34 | + | // A window moved off output A: A still scans the buffer out. | |||
| 35 | + | {"other output scans it out", dma, R{out(0, 0, 6), out(7, 0, 6)}, time.Second, true}, | |||
| 36 | + | {"other output lags", dma, R{out(0, 0, 6), out(0, 0, 5)}, 0, true}, | |||
| 37 | + | {"all outputs moved on", dma, R{out(0, 0, 6), out(0, 0, 6)}, 0, false}, | |||
| 38 | + | } { | |||
| 39 | + | if got := keepHeld(tc.h, tc.reports, now.Add(tc.after)); got != tc.want { | |||
| 40 | + | t.Errorf("%s: %v", tc.name, got) | |||
| 41 | + | } | |||
| 42 | + | } | |||
| 43 | + | } | |||
M internal/adapters/wayland/server.go +13 -8
| @@ -52,12 +52,16 @@ type Server struct { | |||
| 52 | 52 | awaiting map[string][]*wayland.Callback | |
| 53 | 53 | frameDue map[string]time.Time | |
| 54 | 54 | frameReady chan struct{} | |
| 55 | - | frames uint64 | |
| 56 | - | started time.Time | |
| 57 | - | surfaces map[*server.Resource]*surface | |
| 58 | - | buffers map[*server.Resource]clientBuffer | |
| 59 | - | dmabuf *dmabufGlobal | |
| 60 | - | serial uint32 | |
| 55 | + | // held buffers wait until no output reads them (release.go); reports | |
| 56 | + | // are the outputs' latest ports.OutputPresented. | |
| 57 | + | held []heldBuffer | |
| 58 | + | reports map[string]ports.OutputPresented | |
| 59 | + | frames uint64 | |
| 60 | + | started time.Time | |
| 61 | + | surfaces map[*server.Resource]*surface | |
| 62 | + | buffers map[*server.Resource]clientBuffer | |
| 63 | + | dmabuf *dmabufGlobal | |
| 64 | + | serial uint32 | |
| 61 | 65 | // press is the serial of the last button or key press, sent to | |
| 62 | 66 | // pressClient: popup grabs must come from it. | |
| 63 | 67 | press uint32 | |
| @@ -143,7 +147,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 143 | 147 | d.Close() | |
| 144 | 148 | return nil, err | |
| 145 | 149 | } | |
| 146 | - | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 150 | + | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 147 | 151 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | |
| 148 | 152 | if opts.Keymap != "" { | |
| 149 | 153 | fd, size, e := keymapFile(opts.Keymap) | |
| @@ -262,8 +266,9 @@ func (s *Server) emitContent(c ports.SurfaceContent) { | |||
| 262 | 266 | return | |
| 263 | 267 | } | |
| 264 | 268 | s.contentMu.Lock() | |
| 265 | - | s.contents[c.ID] = c | |
| 266 | 269 | s.contentSeq[c.ID]++ | |
| 270 | + | c.Seq = s.contentSeq[c.ID] | |
| 271 | + | s.contents[c.ID] = c | |
| 267 | 272 | close(s.contentNotify) | |
| 268 | 273 | s.contentNotify = make(chan struct{}) | |
| 269 | 274 | s.contentMu.Unlock() | |
| @@ -52,12 +52,16 @@ type Server struct { | |||||
| 52 | awaiting map[string][]*wayland.Callback | 52 | awaiting map[string][]*wayland.Callback | ||
| 53 | frameDue map[string]time.Time | 53 | frameDue map[string]time.Time | ||
| 54 | frameReady chan struct{} | 54 | frameReady chan struct{} | ||
| 55 | - | frames uint64 | 55 | + | // held buffers wait until no output reads them (release.go); reports |
| 56 | - | started time.Time | 56 | + | // are the outputs' latest ports.OutputPresented. |
| 57 | - | surfaces map[*server.Resource]*surface | 57 | + | held []heldBuffer |
| 58 | - | buffers map[*server.Resource]clientBuffer | 58 | + | reports map[string]ports.OutputPresented |
| 59 | - | dmabuf *dmabufGlobal | 59 | + | frames uint64 |
| 60 | - | serial uint32 | 60 | + | started time.Time |
| 61 | + | surfaces map[*server.Resource]*surface | |||
| 62 | + | buffers map[*server.Resource]clientBuffer | |||
| 63 | + | dmabuf *dmabufGlobal | |||
| 64 | + | serial uint32 | |||
| 61 | // press is the serial of the last button or key press, sent to | 65 | // press is the serial of the last button or key press, sent to | ||
| 62 | // pressClient: popup grabs must come from it. | 66 | // pressClient: popup grabs must come from it. | ||
| 63 | press uint32 | 67 | press uint32 | ||
| @@ -143,7 +147,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 143 | d.Close() | 147 | d.Close() | ||
| 144 | return nil, err | 148 | return nil, err | ||
| 145 | } | 149 | } | ||
| 146 | - | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | 150 | + | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} |
| 147 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | 151 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | ||
| 148 | if opts.Keymap != "" { | 152 | if opts.Keymap != "" { | ||
| 149 | fd, size, e := keymapFile(opts.Keymap) | 153 | fd, size, e := keymapFile(opts.Keymap) | ||
| @@ -262,8 +266,9 @@ func (s *Server) emitContent(c ports.SurfaceContent) { | |||||
| 262 | return | 266 | return | ||
| 263 | } | 267 | } | ||
| 264 | s.contentMu.Lock() | 268 | s.contentMu.Lock() | ||
| 265 | - | s.contents[c.ID] = c | |||
| 266 | s.contentSeq[c.ID]++ | 269 | s.contentSeq[c.ID]++ | ||
| 270 | + | c.Seq = s.contentSeq[c.ID] | |||
| 271 | + | s.contents[c.ID] = c | |||
| 267 | close(s.contentNotify) | 272 | close(s.contentNotify) | ||
| 268 | s.contentNotify = make(chan struct{}) | 273 | s.contentNotify = make(chan struct{}) | ||
| 269 | s.contentMu.Unlock() | 274 | s.contentMu.Unlock() | ||
M internal/adapters/wayland/surface.go +3 -5
| @@ -150,8 +150,8 @@ func (s *surface) Destroy(*wayland.Surface) { | |||
| 150 | 150 | } | |
| 151 | 151 | s.callbacks = nil | |
| 152 | 152 | // The client may reuse the buffer on another surface. | |
| 153 | - | if s.current != nil && s.current.Resource.Alive() { | |
| 154 | - | s.current.SendRelease() | |
| 153 | + | if s.current != nil { | |
| 154 | + | s.server.releaseBuffer(s, s.current) | |
| 155 | 155 | } | |
| 156 | 156 | s.current, s.pending = nil, nil | |
| 157 | 157 | if s.role != nil { | |
| @@ -189,9 +189,7 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 189 | 189 | } | |
| 190 | 190 | if s.attached { | |
| 191 | 191 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | |
| 192 | - | if s.current.Resource.Alive() { | |
| 193 | - | s.current.SendRelease() | |
| 194 | - | } | |
| 192 | + | s.server.releaseBuffer(s, s.current) | |
| 195 | 193 | } | |
| 196 | 194 | s.current = s.pending | |
| 197 | 195 | s.pending = nil | |
| @@ -150,8 +150,8 @@ func (s *surface) Destroy(*wayland.Surface) { | |||||
| 150 | } | 150 | } | ||
| 151 | s.callbacks = nil | 151 | s.callbacks = nil | ||
| 152 | // The client may reuse the buffer on another surface. | 152 | // The client may reuse the buffer on another surface. | ||
| 153 | - | if s.current != nil && s.current.Resource.Alive() { | 153 | + | if s.current != nil { |
| 154 | - | s.current.SendRelease() | 154 | + | s.server.releaseBuffer(s, s.current) |
| 155 | } | 155 | } | ||
| 156 | s.current, s.pending = nil, nil | 156 | s.current, s.pending = nil, nil | ||
| 157 | if s.role != nil { | 157 | if s.role != nil { | ||
| @@ -189,9 +189,7 @@ func (s *surface) Commit(*wayland.Surface) { | |||||
| 189 | } | 189 | } | ||
| 190 | if s.attached { | 190 | if s.attached { | ||
| 191 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | 191 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | ||
| 192 | - | if s.current.Resource.Alive() { | 192 | + | s.server.releaseBuffer(s, s.current) |
| 193 | - | s.current.SendRelease() | |||
| 194 | - | } | |||
| 195 | } | 193 | } | ||
| 196 | s.current = s.pending | 194 | s.current = s.pending | ||
| 197 | s.pending = nil | 195 | s.pending = nil | ||
M internal/app/app.go +4 -4
| @@ -64,7 +64,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 64 | 64 | var hw *drmBackend | |
| 65 | 65 | if opts.Backend == "drm" { | |
| 66 | 66 | var err error | |
| 67 | - | if hw, err = openDRM(ctx, opts.Config.Outputs); err != nil { | |
| 67 | + | if hw, err = openDRM(ctx, opts.Config.Outputs, opts.Config.Render.DirectScanout); err != nil { | |
| 68 | 68 | return err | |
| 69 | 69 | } | |
| 70 | 70 | defer hw.close() | |
| @@ -83,7 +83,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 83 | 83 | renderScenes := make(chan []ports.Scene, 1) | |
| 84 | 84 | contents := make(chan ports.SurfaceContent, 64) | |
| 85 | 85 | cursorChanges := make(chan ports.CursorChange, 1) | |
| 86 | - | presented := make(chan ports.OutputPresented, 8) | |
| 86 | + | presented := make(chan ports.OutputPresented, 64) | |
| 87 | 87 | 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} | |
| 88 | 88 | c, err := core.New(opts.Config, ch) | |
| 89 | 89 | if err != nil { | |
| @@ -208,12 +208,12 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 208 | 208 | } | |
| 209 | 209 | if hw != nil { | |
| 210 | 210 | done <- safe("output", func() error { | |
| 211 | - | want := func() drm.Want { return wantFromConfig(opts.Config.Outputs) } | |
| 211 | + | want := func() drm.Want { return wantFromConfig(opts.Config.Outputs, opts.Config.Render.DirectScanout) } | |
| 212 | 212 | return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm")) | |
| 213 | 213 | }) | |
| 214 | 214 | return | |
| 215 | 215 | } | |
| 216 | - | done <- runHeadless(ctx, sizes, opts.ScreenshotDir, output, renderScenes, contents, cursorChanges, curs, newRenderer, logging.For(ctx, "render")) | |
| 216 | + | done <- runHeadless(ctx, sizes, opts.ScreenshotDir, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "render")) | |
| 217 | 217 | }() | |
| 218 | 218 | ||
| 219 | 219 | if hw != nil { | |
| @@ -64,7 +64,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 64 | var hw *drmBackend | 64 | var hw *drmBackend | ||
| 65 | if opts.Backend == "drm" { | 65 | if opts.Backend == "drm" { | ||
| 66 | var err error | 66 | var err error | ||
| 67 | - | if hw, err = openDRM(ctx, opts.Config.Outputs); err != nil { | 67 | + | if hw, err = openDRM(ctx, opts.Config.Outputs, opts.Config.Render.DirectScanout); err != nil { |
| 68 | return err | 68 | return err | ||
| 69 | } | 69 | } | ||
| 70 | defer hw.close() | 70 | defer hw.close() | ||
| @@ -83,7 +83,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 83 | renderScenes := make(chan []ports.Scene, 1) | 83 | renderScenes := make(chan []ports.Scene, 1) | ||
| 84 | contents := make(chan ports.SurfaceContent, 64) | 84 | contents := make(chan ports.SurfaceContent, 64) | ||
| 85 | cursorChanges := make(chan ports.CursorChange, 1) | 85 | cursorChanges := make(chan ports.CursorChange, 1) | ||
| 86 | - | presented := make(chan ports.OutputPresented, 8) | 86 | + | presented := make(chan ports.OutputPresented, 64) |
| 87 | 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} | 87 | 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} | ||
| 88 | c, err := core.New(opts.Config, ch) | 88 | c, err := core.New(opts.Config, ch) | ||
| 89 | if err != nil { | 89 | if err != nil { | ||
| @@ -208,12 +208,12 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 208 | } | 208 | } | ||
| 209 | if hw != nil { | 209 | if hw != nil { | ||
| 210 | done <- safe("output", func() error { | 210 | done <- safe("output", func() error { | ||
| 211 | - | want := func() drm.Want { return wantFromConfig(opts.Config.Outputs) } | 211 | + | want := func() drm.Want { return wantFromConfig(opts.Config.Outputs, opts.Config.Render.DirectScanout) } |
| 212 | return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm")) | 212 | return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm")) | ||
| 213 | }) | 213 | }) | ||
| 214 | return | 214 | return | ||
| 215 | } | 215 | } | ||
| 216 | - | done <- runHeadless(ctx, sizes, opts.ScreenshotDir, output, renderScenes, contents, cursorChanges, curs, newRenderer, logging.For(ctx, "render")) | 216 | + | done <- runHeadless(ctx, sizes, opts.ScreenshotDir, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "render")) |
| 217 | }() | 217 | }() | ||
| 218 | 218 | ||||
| 219 | if hw != nil { | 219 | if hw != nil { | ||
M internal/app/drm.go +4 -4
| @@ -42,14 +42,14 @@ func safe(name string, fn func() error) (err error) { | |||
| 42 | 42 | ||
| 43 | 43 | // openDRM opens the seat and every card with a connected display. Outputs | |
| 44 | 44 | // are found by the first Scan of each card. | |
| 45 | - | func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, error) { | |
| 45 | + | func openDRM(ctx context.Context, outputs []ports.OutputConfig, scanout bool) (*drmBackend, error) { | |
| 46 | 46 | log := logging.For(ctx, "drm") | |
| 47 | 47 | s, err := seat.Open(ctx, logging.For(ctx, "seat")) | |
| 48 | 48 | if err != nil { | |
| 49 | 49 | log.Error().Err(err).Msg("open seat") | |
| 50 | 50 | return nil, fmt.Errorf("open seat: %w", err) | |
| 51 | 51 | } | |
| 52 | - | want := wantFromConfig(outputs) | |
| 52 | + | want := wantFromConfig(outputs, scanout) | |
| 53 | 53 | paths, _ := filepath.Glob("/dev/dri/card[0-9]*") | |
| 54 | 54 | sort.Strings(paths) | |
| 55 | 55 | b := &drmBackend{seat: s} | |
| @@ -79,8 +79,8 @@ func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, er | |||
| 79 | 79 | } | |
| 80 | 80 | ||
| 81 | 81 | // wantFromConfig turns output.<name> entries into connector choices. | |
| 82 | - | func wantFromConfig(outputs []ports.OutputConfig) drm.Want { | |
| 83 | - | want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}} | |
| 82 | + | func wantFromConfig(outputs []ports.OutputConfig, scanout bool) drm.Want { | |
| 83 | + | want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}, NoScanout: !scanout} | |
| 84 | 84 | for _, o := range outputs { | |
| 85 | 85 | if o.Off { | |
| 86 | 86 | want.Disabled[o.Name] = true | |
| @@ -42,14 +42,14 @@ func safe(name string, fn func() error) (err error) { | |||||
| 42 | 42 | ||||
| 43 | // openDRM opens the seat and every card with a connected display. Outputs | 43 | // openDRM opens the seat and every card with a connected display. Outputs | ||
| 44 | // are found by the first Scan of each card. | 44 | // are found by the first Scan of each card. | ||
| 45 | - | func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, error) { | 45 | + | func openDRM(ctx context.Context, outputs []ports.OutputConfig, scanout bool) (*drmBackend, error) { |
| 46 | log := logging.For(ctx, "drm") | 46 | log := logging.For(ctx, "drm") | ||
| 47 | s, err := seat.Open(ctx, logging.For(ctx, "seat")) | 47 | s, err := seat.Open(ctx, logging.For(ctx, "seat")) | ||
| 48 | if err != nil { | 48 | if err != nil { | ||
| 49 | log.Error().Err(err).Msg("open seat") | 49 | log.Error().Err(err).Msg("open seat") | ||
| 50 | return nil, fmt.Errorf("open seat: %w", err) | 50 | return nil, fmt.Errorf("open seat: %w", err) | ||
| 51 | } | 51 | } | ||
| 52 | - | want := wantFromConfig(outputs) | 52 | + | want := wantFromConfig(outputs, scanout) |
| 53 | paths, _ := filepath.Glob("/dev/dri/card[0-9]*") | 53 | paths, _ := filepath.Glob("/dev/dri/card[0-9]*") | ||
| 54 | sort.Strings(paths) | 54 | sort.Strings(paths) | ||
| 55 | b := &drmBackend{seat: s} | 55 | b := &drmBackend{seat: s} | ||
| @@ -79,8 +79,8 @@ func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, er | |||||
| 79 | } | 79 | } | ||
| 80 | 80 | ||||
| 81 | // wantFromConfig turns output.<name> entries into connector choices. | 81 | // wantFromConfig turns output.<name> entries into connector choices. | ||
| 82 | - | func wantFromConfig(outputs []ports.OutputConfig) drm.Want { | 82 | + | func wantFromConfig(outputs []ports.OutputConfig, scanout bool) drm.Want { |
| 83 | - | want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}} | 83 | + | want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}, NoScanout: !scanout} |
| 84 | for _, o := range outputs { | 84 | for _, o := range outputs { | ||
| 85 | if o.Off { | 85 | if o.Off { | ||
| 86 | want.Disabled[o.Name] = true | 86 | want.Disabled[o.Name] = true | ||
M internal/app/drm_test.go +1 -1
| @@ -7,7 +7,7 @@ import ( | |||
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | 9 | func TestWantFromConfig(t *testing.T) { | |
| 10 | - | want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}) | |
| 10 | + | want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}, true) | |
| 11 | 11 | if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 { | |
| 12 | 12 | t.Fatalf("%+v", want) | |
| 13 | 13 | } | |
| @@ -7,7 +7,7 @@ import ( | |||||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | func TestWantFromConfig(t *testing.T) { | 9 | func TestWantFromConfig(t *testing.T) { | ||
| 10 | - | want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}) | 10 | + | want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}, true) |
| 11 | if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 { | 11 | if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 { | ||
| 12 | t.Fatalf("%+v", want) | 12 | t.Fatalf("%+v", want) | ||
| 13 | } | 13 | } | ||
M internal/app/headless.go +2 -2
| @@ -13,7 +13,7 @@ import ( | |||
| 13 | 13 | ||
| 14 | 14 | // runHeadless drives one virtual output per size, named HEADLESS-1, -2, ... | |
| 15 | 15 | // With several outputs, screenshots go to a subdirectory per output. | |
| 16 | - | func runHeadless(ctx context.Context, sizes [][2]int, shots string, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { | |
| 16 | + | func runHeadless(ctx context.Context, sizes [][2]int, shots string, 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 { | |
| 17 | 17 | set := newOutputSet() | |
| 18 | 18 | for i, size := range sizes { | |
| 19 | 19 | name := fmt.Sprintf("HEADLESS-%d", i+1) | |
| @@ -26,7 +26,7 @@ func runHeadless(ctx context.Context, sizes [][2]int, shots string, events chan< | |||
| 26 | 26 | } | |
| 27 | 27 | cur := &headless.Cursor{} | |
| 28 | 28 | curs.set(name, cur) | |
| 29 | - | opts := headless.Options{Cursor: cur, LoadCursor: loadCursor, Width: size[0], Height: size[1], ScreenshotDir: dir, Log: log, NewRenderer: newRenderer} | |
| 29 | + | opts := headless.Options{Cursor: cur, LoadCursor: loadCursor, Width: size[0], Height: size[1], ScreenshotDir: dir, Log: log, NewRenderer: newRenderer, Name: name, Presented: presented} | |
| 30 | 30 | set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange) error { | |
| 31 | 31 | return headless.Run(octx, opts, sc, cc, cu) | |
| 32 | 32 | }) | |
| @@ -13,7 +13,7 @@ import ( | |||||
| 13 | 13 | ||||
| 14 | // runHeadless drives one virtual output per size, named HEADLESS-1, -2, ... | 14 | // runHeadless drives one virtual output per size, named HEADLESS-1, -2, ... | ||
| 15 | // With several outputs, screenshots go to a subdirectory per output. | 15 | // With several outputs, screenshots go to a subdirectory per output. | ||
| 16 | - | func runHeadless(ctx context.Context, sizes [][2]int, shots string, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { | 16 | + | func runHeadless(ctx context.Context, sizes [][2]int, shots string, 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 { |
| 17 | set := newOutputSet() | 17 | set := newOutputSet() | ||
| 18 | for i, size := range sizes { | 18 | for i, size := range sizes { | ||
| 19 | name := fmt.Sprintf("HEADLESS-%d", i+1) | 19 | name := fmt.Sprintf("HEADLESS-%d", i+1) | ||
| @@ -26,7 +26,7 @@ func runHeadless(ctx context.Context, sizes [][2]int, shots string, events chan< | |||||
| 26 | } | 26 | } | ||
| 27 | cur := &headless.Cursor{} | 27 | cur := &headless.Cursor{} | ||
| 28 | curs.set(name, cur) | 28 | curs.set(name, cur) | ||
| 29 | - | opts := headless.Options{Cursor: cur, LoadCursor: loadCursor, Width: size[0], Height: size[1], ScreenshotDir: dir, Log: log, NewRenderer: newRenderer} | 29 | + | opts := headless.Options{Cursor: cur, LoadCursor: loadCursor, Width: size[0], Height: size[1], ScreenshotDir: dir, Log: log, NewRenderer: newRenderer, Name: name, Presented: presented} |
| 30 | set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange) error { | 30 | set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange) error { | ||
| 31 | return headless.Run(octx, opts, sc, cc, cu) | 31 | return headless.Run(octx, opts, sc, cc, cu) | ||
| 32 | }) | 32 | }) | ||
M internal/ports/messages.go +16 -4
| @@ -278,9 +278,19 @@ type OutputAdded struct{ Info OutputInfo } | |||
| 278 | 278 | ||
| 279 | 279 | func (OutputAdded) outputEvent() {} | |
| 280 | 280 | ||
| 281 | - | // OutputPresented carries output → wayland that a frame reached the screen | |
| 282 | - | // (a page flip completed): frame callbacks of the surfaces on it are due. | |
| 283 | - | type OutputPresented struct{ Output string } | |
| 281 | + | // OutputPresented carries output → wayland what an output shows and reads. | |
| 282 | + | // Flip is set when a page flip completed: frame callbacks of the surfaces | |
| 283 | + | // on it are due. Shown and Queued are the DMABuf IDs scanned out directly | |
| 284 | + | // (0: a composed image); Seen is the latest content Seq the output got per | |
| 285 | + | // window. A replaced client buffer is released once every output that | |
| 286 | + | // reports has seen a later content of its window and neither shows nor | |
| 287 | + | // queues it. | |
| 288 | + | type OutputPresented struct { | |
| 289 | + | Output string | |
| 290 | + | Flip bool | |
| 291 | + | Shown, Queued uint64 | |
| 292 | + | Seen map[WindowID]uint64 | |
| 293 | + | } | |
| 284 | 294 | ||
| 285 | 295 | // OutputRemoved carries output → core an unplugged display. | |
| 286 | 296 | type OutputRemoved struct{ Name string } | |
| @@ -520,7 +530,9 @@ type SceneLayer struct { | |||
| 520 | 530 | // Geometry is the part of the surface that is the window (xdg window | |
| 521 | 531 | // geometry), the rest being client shadows. | |
| 522 | 532 | type SurfaceContent struct { | |
| 523 | - | ID WindowID | |
| 533 | + | ID WindowID | |
| 534 | + | // Seq counts the window's contents (wayland sets it), for release. | |
| 535 | + | Seq uint64 | |
| 524 | 536 | Width, Height int | |
| 525 | 537 | LogicalW, LogicalH int | |
| 526 | 538 | Opaque bool // x formats: ignore the alpha byte | |
| @@ -278,9 +278,19 @@ type OutputAdded struct{ Info OutputInfo } | |||||
| 278 | 278 | ||||
| 279 | func (OutputAdded) outputEvent() {} | 279 | func (OutputAdded) outputEvent() {} | ||
| 280 | 280 | ||||
| 281 | - | // OutputPresented carries output → wayland that a frame reached the screen | 281 | + | // OutputPresented carries output → wayland what an output shows and reads. |
| 282 | - | // (a page flip completed): frame callbacks of the surfaces on it are due. | 282 | + | // Flip is set when a page flip completed: frame callbacks of the surfaces |
| 283 | - | type OutputPresented struct{ Output string } | 283 | + | // on it are due. Shown and Queued are the DMABuf IDs scanned out directly |
| 284 | + | // (0: a composed image); Seen is the latest content Seq the output got per | |||
| 285 | + | // window. A replaced client buffer is released once every output that | |||
| 286 | + | // reports has seen a later content of its window and neither shows nor | |||
| 287 | + | // queues it. | |||
| 288 | + | type OutputPresented struct { | |||
| 289 | + | Output string | |||
| 290 | + | Flip bool | |||
| 291 | + | Shown, Queued uint64 | |||
| 292 | + | Seen map[WindowID]uint64 | |||
| 293 | + | } | |||
| 284 | 294 | ||||
| 285 | // OutputRemoved carries output → core an unplugged display. | 295 | // OutputRemoved carries output → core an unplugged display. | ||
| 286 | type OutputRemoved struct{ Name string } | 296 | type OutputRemoved struct{ Name string } | ||
| @@ -520,7 +530,9 @@ type SceneLayer struct { | |||||
| 520 | // Geometry is the part of the surface that is the window (xdg window | 530 | // Geometry is the part of the surface that is the window (xdg window | ||
| 521 | // geometry), the rest being client shadows. | 531 | // geometry), the rest being client shadows. | ||
| 522 | type SurfaceContent struct { | 532 | type SurfaceContent struct { | ||
| 523 | - | ID WindowID | 533 | + | ID WindowID |
| 534 | + | // Seq counts the window's contents (wayland sets it), for release. | |||
| 535 | + | Seq uint64 | |||
| 524 | Width, Height int | 536 | Width, Height int | ||
| 525 | LogicalW, LogicalH int | 537 | LogicalW, LogicalH int | ||
| 526 | Opaque bool // x formats: ignore the alpha byte | 538 | Opaque bool // x formats: ignore the alpha byte | ||