fix(drm): release client buffers on output reports, share GEM handles per card
- commit
- 34cdd04391b48d29cae792a80be16e455207526f
- author
- brice · · 2026-09-25 16:49 UTC
- parent
- 4472471
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +267 -167
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/output.go +36 -16
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | + | "maps" | |
| 7 | 8 | "slices" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| @@ -31,9 +32,9 @@ type Output struct { | |||
| 31 | 32 | log zerowrap.Logger | |
| 32 | 33 | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | |
| 33 | 34 | // and the buffer on screen and queued (0: the composed image). | |
| 35 | + | card *Card | |
| 34 | 36 | scanout bool | |
| 35 | 37 | clientFBs map[uint64]*clientFB | |
| 36 | - | handles map[uint32]int | |
| 37 | 38 | shown, queued uint64 | |
| 38 | 39 | reason string // why the last frame was composed ("" = scanout) | |
| 39 | 40 | } | |
| @@ -45,7 +46,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu | |||
| 45 | 46 | // newOutput allocates buffers and the cursor for a connector on crtc. | |
| 46 | 47 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | |
| 47 | 48 | log := card.log | |
| 48 | - | o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, handles: map[uint32]int{}, reason: "start"} | |
| 49 | + | 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"} | |
| 49 | 50 | var err error | |
| 50 | 51 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 51 | 52 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | |
| @@ -115,13 +116,7 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||
| 115 | 116 | } | |
| 116 | 117 | var fb uint32 | |
| 117 | 118 | if reason == "" { | |
| 118 | - | var err error | |
| 119 | - | if fb, err = o.scanoutFB(c.DMABuf); err != nil { | |
| 120 | - | if !errors.Is(err, errScanoutRefused) { | |
| 121 | - | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout import failed") | |
| 122 | - | } | |
| 123 | - | reason = "import_failed" | |
| 124 | - | } | |
| 119 | + | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) | |
| 125 | 120 | } | |
| 126 | 121 | if reason != o.reason { | |
| 127 | 122 | o.log.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout") | |
| @@ -138,8 +133,9 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||
| 138 | 133 | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | |
| 139 | 134 | // KMS refuses this buffer on the plane: compose it instead. | |
| 140 | 135 | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | |
| 141 | - | o.clientFBs[c.DMABuf.ID].failed = true | |
| 136 | + | o.clientFBs[c.DMABuf.ID].failed = "flip_refused" | |
| 142 | 137 | o.reason = "flip_refused" | |
| 138 | + | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | |
| 143 | 139 | return false, nil | |
| 144 | 140 | } | |
| 145 | 141 | return true, err | |
| @@ -168,7 +164,7 @@ func (o *Output) Close() { | |||
| 168 | 164 | } | |
| 169 | 165 | } | |
| 170 | 166 | o.shown, o.queued = 0, 0 | |
| 171 | - | o.dropClientFBs(true) | |
| 167 | + | o.dropClientFBs(time.Now(), true) | |
| 172 | 168 | } | |
| 173 | 169 | ||
| 174 | 170 | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | |
| @@ -199,7 +195,16 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 199 | 195 | frame := 0 | |
| 200 | 196 | stats := time.NewTicker(10 * time.Second) | |
| 201 | 197 | defer stats.Stop() | |
| 198 | + | // seen is the latest content Seq per window, reported to wayland with | |
| 199 | + | // what the output scans out, after each frame decision. | |
| 200 | + | seen := map[ports.WindowID]uint64{} | |
| 201 | + | reportDirty := false | |
| 202 | + | flipped := false | |
| 202 | 203 | for { | |
| 204 | + | if flipped || reportDirty { | |
| 205 | + | o.report(presented, flipped, seen) | |
| 206 | + | flipped, reportDirty = false, false | |
| 207 | + | } | |
| 203 | 208 | select { | |
| 204 | 209 | case <-ctx.Done(): | |
| 205 | 210 | return nil | |
| @@ -226,11 +231,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 226 | 231 | o.pending = false | |
| 227 | 232 | o.flips += n | |
| 228 | 233 | o.shown = o.queued | |
| 229 | - | o.dropClientFBs(false) | |
| 230 | - | select { | |
| 231 | - | case presented <- ports.OutputPresented{Output: o.conn.name, Scanout: o.shown}: | |
| 232 | - | default: | |
| 233 | - | } | |
| 234 | + | o.queued = 0 | |
| 235 | + | flipped = true | |
| 234 | 236 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | |
| 235 | 237 | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | |
| 236 | 238 | } | |
| @@ -253,9 +255,14 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 253 | 255 | } else { | |
| 254 | 256 | surfaces[c.ID] = c | |
| 255 | 257 | } | |
| 258 | + | if c.Seq > seen[c.ID] { | |
| 259 | + | seen[c.ID] = c.Seq | |
| 260 | + | reportDirty = true | |
| 261 | + | } | |
| 256 | 262 | // Windows on other outputs or workspaces do not need a frame. | |
| 257 | 263 | dirty = dirty || scene.Shows(c.ID) | |
| 258 | 264 | case <-stats.C: | |
| 265 | + | o.dropClientFBs(time.Now(), false) | |
| 259 | 266 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | |
| 260 | 267 | if o.cursor != nil { | |
| 261 | 268 | cs := o.cursor.TakeStats() | |
| @@ -291,6 +298,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 291 | 298 | return fmt.Errorf("page flip: %w", err) | |
| 292 | 299 | } | |
| 293 | 300 | dirty = false | |
| 301 | + | reportDirty = true | |
| 294 | 302 | frame++ | |
| 295 | 303 | 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") | |
| 296 | 304 | } | |
| @@ -308,3 +316,15 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6 | |||
| 308 | 316 | } | |
| 309 | 317 | 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") | |
| 310 | 318 | } | |
| 319 | + | ||
| 320 | + | // report tells wayland what the output shows and has read, so replaced | |
| 321 | + | // client buffers can be released. It must follow the frame decision: a | |
| 322 | + | // buffer is safe once a later content is seen and it is neither shown nor | |
| 323 | + | // queued. A full channel drops the report; the next one carries the state. | |
| 324 | + | func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) { | |
| 325 | + | r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | |
| 326 | + | select { | |
| 327 | + | case presented <- r: | |
| 328 | + | default: | |
| 329 | + | } | |
| 330 | + | } | |
| @@ -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 | ||||
| @@ -31,9 +32,9 @@ type Output struct { | |||||
| 31 | log zerowrap.Logger | 32 | log zerowrap.Logger | ||
| 32 | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | 33 | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | ||
| 33 | // and the buffer on screen and queued (0: the composed image). | 34 | // and the buffer on screen and queued (0: the composed image). | ||
| 35 | + | card *Card | |||
| 34 | scanout bool | 36 | scanout bool | ||
| 35 | clientFBs map[uint64]*clientFB | 37 | clientFBs map[uint64]*clientFB | ||
| 36 | - | handles map[uint32]int | |||
| 37 | shown, queued uint64 | 38 | shown, queued uint64 | ||
| 38 | reason string // why the last frame was composed ("" = scanout) | 39 | reason string // why the last frame was composed ("" = scanout) | ||
| 39 | } | 40 | } | ||
| @@ -45,7 +46,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu | |||||
| 45 | // newOutput allocates buffers and the cursor for a connector on crtc. | 46 | // newOutput allocates buffers and the cursor for a connector on crtc. | ||
| 46 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | 47 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | ||
| 47 | log := card.log | 48 | log := card.log | ||
| 48 | - | o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, handles: map[uint32]int{}, reason: "start"} | 49 | + | 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"} |
| 49 | var err error | 50 | var err error | ||
| 50 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | 51 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | ||
| 51 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | 52 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | ||
| @@ -115,13 +116,7 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||||
| 115 | } | 116 | } | ||
| 116 | var fb uint32 | 117 | var fb uint32 | ||
| 117 | if reason == "" { | 118 | if reason == "" { | ||
| 118 | - | var err error | 119 | + | fb, reason = o.scanoutFB(c.DMABuf, time.Now()) |
| 119 | - | if fb, err = o.scanoutFB(c.DMABuf); err != nil { | |||
| 120 | - | if !errors.Is(err, errScanoutRefused) { | |||
| 121 | - | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout import failed") | |||
| 122 | - | } | |||
| 123 | - | reason = "import_failed" | |||
| 124 | - | } | |||
| 125 | } | 120 | } | ||
| 126 | if reason != o.reason { | 121 | if reason != o.reason { | ||
| 127 | o.log.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout") | 122 | o.log.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout") | ||
| @@ -138,8 +133,9 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||||
| 138 | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | 133 | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | ||
| 139 | // KMS refuses this buffer on the plane: compose it instead. | 134 | // KMS refuses this buffer on the plane: compose it instead. | ||
| 140 | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | 135 | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | ||
| 141 | - | o.clientFBs[c.DMABuf.ID].failed = true | 136 | + | o.clientFBs[c.DMABuf.ID].failed = "flip_refused" |
| 142 | o.reason = "flip_refused" | 137 | o.reason = "flip_refused" | ||
| 138 | + | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | |||
| 143 | return false, nil | 139 | return false, nil | ||
| 144 | } | 140 | } | ||
| 145 | return true, err | 141 | return true, err | ||
| @@ -168,7 +164,7 @@ func (o *Output) Close() { | |||||
| 168 | } | 164 | } | ||
| 169 | } | 165 | } | ||
| 170 | o.shown, o.queued = 0, 0 | 166 | o.shown, o.queued = 0, 0 | ||
| 171 | - | o.dropClientFBs(true) | 167 | + | o.dropClientFBs(time.Now(), true) |
| 172 | } | 168 | } | ||
| 173 | 169 | ||||
| 174 | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | 170 | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | ||
| @@ -199,7 +195,16 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 199 | frame := 0 | 195 | frame := 0 | ||
| 200 | stats := time.NewTicker(10 * time.Second) | 196 | stats := time.NewTicker(10 * time.Second) | ||
| 201 | defer stats.Stop() | 197 | defer stats.Stop() | ||
| 198 | + | // seen is the latest content Seq per window, reported to wayland with | |||
| 199 | + | // what the output scans out, after each frame decision. | |||
| 200 | + | seen := map[ports.WindowID]uint64{} | |||
| 201 | + | reportDirty := false | |||
| 202 | + | flipped := false | |||
| 202 | for { | 203 | for { | ||
| 204 | + | if flipped || reportDirty { | |||
| 205 | + | o.report(presented, flipped, seen) | |||
| 206 | + | flipped, reportDirty = false, false | |||
| 207 | + | } | |||
| 203 | select { | 208 | select { | ||
| 204 | case <-ctx.Done(): | 209 | case <-ctx.Done(): | ||
| 205 | return nil | 210 | return nil | ||
| @@ -226,11 +231,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 226 | o.pending = false | 231 | o.pending = false | ||
| 227 | o.flips += n | 232 | o.flips += n | ||
| 228 | o.shown = o.queued | 233 | o.shown = o.queued | ||
| 229 | - | o.dropClientFBs(false) | 234 | + | o.queued = 0 |
| 230 | - | select { | 235 | + | flipped = true |
| 231 | - | case presented <- ports.OutputPresented{Output: o.conn.name, Scanout: o.shown}: | |||
| 232 | - | default: | |||
| 233 | - | } | |||
| 234 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | 236 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | ||
| 235 | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | 237 | o.log.Info().Dur("flip_ms", d).Msg("slow flip") | ||
| 236 | } | 238 | } | ||
| @@ -253,9 +255,14 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 253 | } else { | 255 | } else { | ||
| 254 | surfaces[c.ID] = c | 256 | surfaces[c.ID] = c | ||
| 255 | } | 257 | } | ||
| 258 | + | if c.Seq > seen[c.ID] { | |||
| 259 | + | seen[c.ID] = c.Seq | |||
| 260 | + | reportDirty = true | |||
| 261 | + | } | |||
| 256 | // Windows on other outputs or workspaces do not need a frame. | 262 | // Windows on other outputs or workspaces do not need a frame. | ||
| 257 | dirty = dirty || scene.Shows(c.ID) | 263 | dirty = dirty || scene.Shows(c.ID) | ||
| 258 | case <-stats.C: | 264 | case <-stats.C: | ||
| 265 | + | o.dropClientFBs(time.Now(), false) | |||
| 259 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | 266 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | ||
| 260 | if o.cursor != nil { | 267 | if o.cursor != nil { | ||
| 261 | cs := o.cursor.TakeStats() | 268 | cs := o.cursor.TakeStats() | ||
| @@ -291,6 +298,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 291 | return fmt.Errorf("page flip: %w", err) | 298 | return fmt.Errorf("page flip: %w", err) | ||
| 292 | } | 299 | } | ||
| 293 | dirty = false | 300 | dirty = false | ||
| 301 | + | reportDirty = true | |||
| 294 | frame++ | 302 | frame++ | ||
| 295 | 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") | 303 | 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") | ||
| 296 | } | 304 | } | ||
| @@ -308,3 +316,15 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6 | |||||
| 308 | } | 316 | } | ||
| 309 | 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") | 317 | 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") | ||
| 310 | } | 318 | } | ||
| 319 | + | ||||
| 320 | + | // report tells wayland what the output shows and has read, so replaced | |||
| 321 | + | // client buffers can be released. It must follow the frame decision: a | |||
| 322 | + | // buffer is safe once a later content is seen and it is neither shown nor | |||
| 323 | + | // queued. A full channel drops the report; the next one carries the state. | |||
| 324 | + | func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) { | |||
| 325 | + | r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | |||
| 326 | + | select { | |||
| 327 | + | case presented <- r: | |||
| 328 | + | default: | |||
| 329 | + | } | |||
| 330 | + | } | |||
M internal/adapters/drm/scanout.go +73 -60
| @@ -2,7 +2,7 @@ package drm | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | - | "os" | |
| 5 | + | "time" | |
| 6 | 6 | "unsafe" | |
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/nefertty/internal/ports" | |
| @@ -21,9 +21,10 @@ const ( | |||
| 21 | 21 | ioctlPrimeFDToHandle = 0xC00C642E | |
| 22 | 22 | ioctlAddFB2 = 0xC06864B8 | |
| 23 | 23 | ||
| 24 | - | fbModifiers = 1 << 1 | |
| 25 | - | modInvalid = 0x00ffffffffffffff | |
| 26 | - | scanoutIdleTTL = 240 // flips a cached framebuffer survives unused | |
| 24 | + | fbModifiers = 1 << 1 | |
| 25 | + | capAddFB2Modifiers = 0x10 | |
| 26 | + | modInvalid = 0x00ffffffffffffff | |
| 27 | + | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | |
| 27 | 28 | ) | |
| 28 | 29 | ||
| 29 | 30 | type primeHandle struct { | |
| @@ -72,6 +73,10 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 72 | 73 | return c, "multi_plane" | |
| 73 | 74 | case c.Width != w || c.Height != h: | |
| 74 | 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" | |
| 75 | 80 | case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH): | |
| 76 | 81 | return c, "geometry_crop" | |
| 77 | 82 | } | |
| @@ -80,97 +85,105 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 80 | 85 | ||
| 81 | 86 | // clientFB is a client buffer imported as a KMS framebuffer. | |
| 82 | 87 | type clientFB struct { | |
| 83 | - | fbID, handle uint32 | |
| 84 | - | last int // flip count when last shown | |
| 85 | - | failed bool | |
| 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 | |
| 86 | 92 | } | |
| 87 | 93 | ||
| 88 | 94 | // scanoutFB returns the framebuffer of a client buffer, importing it on | |
| 89 | - | // first use. A buffer KMS refuses is remembered and never retried. | |
| 90 | - | func (o *Output) scanoutFB(b *ports.DMABuf) (uint32, error) { | |
| 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) { | |
| 91 | 97 | if fb := o.clientFBs[b.ID]; fb != nil { | |
| 92 | - | fb.last = o.flips | |
| 93 | - | if fb.failed { | |
| 94 | - | return 0, errScanoutRefused | |
| 95 | - | } | |
| 96 | - | return fb.fbID, nil | |
| 98 | + | fb.last = now | |
| 99 | + | return fb.fbID, fb.failed | |
| 97 | 100 | } | |
| 98 | - | fb := &clientFB{last: o.flips} | |
| 101 | + | fb := &clientFB{last: now} | |
| 99 | 102 | o.clientFBs[b.ID] = fb | |
| 100 | - | handle, err := o.importHandle(b.Planes[0].File) | |
| 103 | + | id, err := o.card.addClientFB(b) | |
| 101 | 104 | if err != nil { | |
| 102 | - | fb.failed = true | |
| 103 | - | return 0, errors.Join(errors.New("prime import"), err) | |
| 104 | - | } | |
| 105 | - | cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: b.Format} | |
| 106 | - | cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = handle, b.Planes[0].Stride, b.Planes[0].Offset | |
| 107 | - | if b.Modifier != modInvalid { | |
| 108 | - | cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier | |
| 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 | |
| 109 | 108 | } | |
| 110 | - | if err := ioctl(o.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | |
| 111 | - | o.closeHandle(handle) | |
| 112 | - | fb.failed = true | |
| 113 | - | return 0, errors.Join(errors.New("add fb2"), err) | |
| 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 | |
| 114 | 121 | } | |
| 115 | - | fb.fbID, fb.handle = cmd.fbID, handle | |
| 116 | - | return fb.fbID, nil | |
| 122 | + | return 0, false | |
| 117 | 123 | } | |
| 118 | 124 | ||
| 119 | - | var errScanoutRefused = errors.New("buffer refused for scanout") | |
| 125 | + | const ( | |
| 126 | + | fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 127 | + | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | |
| 128 | + | ) | |
| 120 | 129 | ||
| 121 | - | // importHandle turns a dmabuf into a GEM handle on the card. The kernel | |
| 122 | - | // returns the same handle for the same buffer object: handles are counted | |
| 123 | - | // and closed with their last framebuffer. | |
| 124 | - | func (o *Output) importHandle(f *os.File) (uint32, error) { | |
| 125 | - | raw, err := f.SyscallConn() | |
| 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() | |
| 126 | 140 | if err != nil { | |
| 127 | 141 | return 0, err | |
| 128 | 142 | } | |
| 143 | + | c.gemMu.Lock() | |
| 144 | + | defer c.gemMu.Unlock() | |
| 129 | 145 | var p primeHandle | |
| 130 | 146 | var ioErr error | |
| 131 | 147 | if err := raw.Control(func(fd uintptr) { | |
| 132 | 148 | p.fd = int32(fd) | |
| 133 | - | ioErr = ioctl(o.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | |
| 149 | + | ioErr = ioctl(c.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | |
| 134 | 150 | }); err != nil { | |
| 135 | 151 | return 0, err | |
| 136 | 152 | } | |
| 137 | 153 | if ioErr != nil { | |
| 138 | - | return 0, ioErr | |
| 154 | + | return 0, errors.Join(errors.New("prime import"), ioErr) | |
| 139 | 155 | } | |
| 140 | - | o.handles[p.handle]++ | |
| 141 | - | return p.handle, nil | |
| 142 | - | } | |
| 143 | - | ||
| 144 | - | func (o *Output) closeHandle(handle uint32) { | |
| 145 | - | if o.handles[handle]--; o.handles[handle] > 0 { | |
| 146 | - | return | |
| 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) | |
| 147 | 167 | } | |
| 148 | - | delete(o.handles, handle) | |
| 149 | - | c := gemClose{handle: handle} | |
| 150 | - | _ = ioctl(o.fd, ioctlGemClose, unsafe.Pointer(&c)) | |
| 168 | + | return cmd.fbID, nil | |
| 151 | 169 | } | |
| 152 | 170 | ||
| 153 | - | // dropClientFBs frees framebuffers unused for scanoutIdleTTL flips, or all | |
| 171 | + | // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all | |
| 154 | 172 | // of them, except those on screen or queued. | |
| 155 | - | func (o *Output) dropClientFBs(all bool) { | |
| 173 | + | func (o *Output) dropClientFBs(now time.Time, all bool) { | |
| 156 | 174 | for id, fb := range o.clientFBs { | |
| 157 | 175 | if id == o.shown || id == o.queued { | |
| 158 | 176 | continue | |
| 159 | 177 | } | |
| 160 | - | if !all && o.flips-fb.last < scanoutIdleTTL { | |
| 178 | + | if !all && now.Sub(fb.last) < scanoutIdleTTL { | |
| 161 | 179 | continue | |
| 162 | 180 | } | |
| 163 | - | o.freeFB(fb) | |
| 181 | + | if fb.fbID != 0 { | |
| 182 | + | v := fb.fbID | |
| 183 | + | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v)) | |
| 184 | + | } | |
| 164 | 185 | delete(o.clientFBs, id) | |
| 165 | 186 | } | |
| 166 | 187 | } | |
| 167 | 188 | ||
| 168 | - | func (o *Output) freeFB(fb *clientFB) { | |
| 169 | - | if fb.fbID != 0 { | |
| 170 | - | id := fb.fbID | |
| 171 | - | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&id)) | |
| 172 | - | } | |
| 173 | - | if fb.handle != 0 { | |
| 174 | - | o.closeHandle(fb.handle) | |
| 175 | - | } | |
| 176 | - | } | |
| 189 | + | func scanoutable(f uint32) bool { _, ok := scanoutFormat(f); return ok } | |
| @@ -2,7 +2,7 @@ package drm | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | - | "os" | 5 | + | "time" |
| 6 | "unsafe" | 6 | "unsafe" | ||
| 7 | 7 | ||||
| 8 | "github.com/bnema/nefertty/internal/ports" | 8 | "github.com/bnema/nefertty/internal/ports" | ||
| @@ -21,9 +21,10 @@ const ( | |||||
| 21 | ioctlPrimeFDToHandle = 0xC00C642E | 21 | ioctlPrimeFDToHandle = 0xC00C642E | ||
| 22 | ioctlAddFB2 = 0xC06864B8 | 22 | ioctlAddFB2 = 0xC06864B8 | ||
| 23 | 23 | ||||
| 24 | - | fbModifiers = 1 << 1 | 24 | + | fbModifiers = 1 << 1 |
| 25 | - | modInvalid = 0x00ffffffffffffff | 25 | + | capAddFB2Modifiers = 0x10 |
| 26 | - | scanoutIdleTTL = 240 // flips a cached framebuffer survives unused | 26 | + | modInvalid = 0x00ffffffffffffff |
| 27 | + | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | |||
| 27 | ) | 28 | ) | ||
| 28 | 29 | ||||
| 29 | type primeHandle struct { | 30 | type primeHandle struct { | ||
| @@ -72,6 +73,10 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 72 | return c, "multi_plane" | 73 | return c, "multi_plane" | ||
| 73 | case c.Width != w || c.Height != h: | 74 | case c.Width != w || c.Height != h: | ||
| 74 | return c, "size_mismatch" | 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" | |||
| 75 | case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH): | 80 | case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH): | ||
| 76 | return c, "geometry_crop" | 81 | return c, "geometry_crop" | ||
| 77 | } | 82 | } | ||
| @@ -80,97 +85,105 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 80 | 85 | ||||
| 81 | // clientFB is a client buffer imported as a KMS framebuffer. | 86 | // clientFB is a client buffer imported as a KMS framebuffer. | ||
| 82 | type clientFB struct { | 87 | type clientFB struct { | ||
| 83 | - | fbID, handle uint32 | 88 | + | fbID uint32 |
| 84 | - | last int // flip count when last shown | 89 | + | last time.Time // when last wanted on screen |
| 85 | - | failed bool | 90 | + | // failed is the reason KMS refused the buffer; it is not retried. |
| 91 | + | failed string | |||
| 86 | } | 92 | } | ||
| 87 | 93 | ||||
| 88 | // scanoutFB returns the framebuffer of a client buffer, importing it on | 94 | // scanoutFB returns the framebuffer of a client buffer, importing it on | ||
| 89 | - | // first use. A buffer KMS refuses is remembered and never retried. | 95 | + | // first use. A buffer KMS refuses is remembered with its reason. |
| 90 | - | func (o *Output) scanoutFB(b *ports.DMABuf) (uint32, error) { | 96 | + | func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) { |
| 91 | if fb := o.clientFBs[b.ID]; fb != nil { | 97 | if fb := o.clientFBs[b.ID]; fb != nil { | ||
| 92 | - | fb.last = o.flips | 98 | + | fb.last = now |
| 93 | - | if fb.failed { | 99 | + | return fb.fbID, fb.failed |
| 94 | - | return 0, errScanoutRefused | |||
| 95 | - | } | |||
| 96 | - | return fb.fbID, nil | |||
| 97 | } | 100 | } | ||
| 98 | - | fb := &clientFB{last: o.flips} | 101 | + | fb := &clientFB{last: now} |
| 99 | o.clientFBs[b.ID] = fb | 102 | o.clientFBs[b.ID] = fb | ||
| 100 | - | handle, err := o.importHandle(b.Planes[0].File) | 103 | + | id, err := o.card.addClientFB(b) |
| 101 | if err != nil { | 104 | if err != nil { | ||
| 102 | - | fb.failed = true | 105 | + | o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed") |
| 103 | - | return 0, errors.Join(errors.New("prime import"), err) | 106 | + | fb.failed = "import_failed" |
| 104 | - | } | 107 | + | return 0, fb.failed |
| 105 | - | cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: b.Format} | |||
| 106 | - | cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = handle, b.Planes[0].Stride, b.Planes[0].Offset | |||
| 107 | - | if b.Modifier != modInvalid { | |||
| 108 | - | cmd.flags, cmd.modifiers[0] = fbModifiers, b.Modifier | |||
| 109 | } | 108 | } | ||
| 110 | - | if err := ioctl(o.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil { | 109 | + | fb.fbID = id |
| 111 | - | o.closeHandle(handle) | 110 | + | return id, "" |
| 112 | - | fb.failed = true | 111 | + | } |
| 113 | - | return 0, errors.Join(errors.New("add fb2"), err) | 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 | |||
| 114 | } | 121 | } | ||
| 115 | - | fb.fbID, fb.handle = cmd.fbID, handle | 122 | + | return 0, false |
| 116 | - | return fb.fbID, nil | |||
| 117 | } | 123 | } | ||
| 118 | 124 | ||||
| 119 | - | var errScanoutRefused = errors.New("buffer refused for scanout") | 125 | + | const ( |
| 126 | + | fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24 | |||
| 127 | + | fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24 | |||
| 128 | + | ) | |||
| 120 | 129 | ||||
| 121 | - | // importHandle turns a dmabuf into a GEM handle on the card. The kernel | 130 | + | // addClientFB imports a dmabuf as a framebuffer. The GEM handle belongs to |
| 122 | - | // returns the same handle for the same buffer object: handles are counted | 131 | + | // the card fd and is shared by every import of the same buffer, so import, |
| 123 | - | // and closed with their last framebuffer. | 132 | + | // ADDFB2 and close run under the card lock and the handle is closed right |
| 124 | - | func (o *Output) importHandle(f *os.File) (uint32, error) { | 133 | + | // away: the framebuffer keeps its own reference to the buffer. |
| 125 | - | raw, err := f.SyscallConn() | 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() | |||
| 126 | if err != nil { | 140 | if err != nil { | ||
| 127 | return 0, err | 141 | return 0, err | ||
| 128 | } | 142 | } | ||
| 143 | + | c.gemMu.Lock() | |||
| 144 | + | defer c.gemMu.Unlock() | |||
| 129 | var p primeHandle | 145 | var p primeHandle | ||
| 130 | var ioErr error | 146 | var ioErr error | ||
| 131 | if err := raw.Control(func(fd uintptr) { | 147 | if err := raw.Control(func(fd uintptr) { | ||
| 132 | p.fd = int32(fd) | 148 | p.fd = int32(fd) | ||
| 133 | - | ioErr = ioctl(o.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | 149 | + | ioErr = ioctl(c.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) |
| 134 | }); err != nil { | 150 | }); err != nil { | ||
| 135 | return 0, err | 151 | return 0, err | ||
| 136 | } | 152 | } | ||
| 137 | if ioErr != nil { | 153 | if ioErr != nil { | ||
| 138 | - | return 0, ioErr | 154 | + | return 0, errors.Join(errors.New("prime import"), ioErr) |
| 139 | } | 155 | } | ||
| 140 | - | o.handles[p.handle]++ | 156 | + | defer func() { |
| 141 | - | return p.handle, nil | 157 | + | g := gemClose{handle: p.handle} |
| 142 | - | } | 158 | + | _ = ioctl(c.fd, ioctlGemClose, unsafe.Pointer(&g)) |
| 143 | - | 159 | + | }() | |
| 144 | - | func (o *Output) closeHandle(handle uint32) { | 160 | + | cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format} |
| 145 | - | if o.handles[handle]--; o.handles[handle] > 0 { | 161 | + | cmd.handles[0], cmd.pitches[0], cmd.offsets[0] = p.handle, b.Planes[0].Stride, b.Planes[0].Offset |
| 146 | - | return | 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) | |||
| 147 | } | 167 | } | ||
| 148 | - | delete(o.handles, handle) | 168 | + | return cmd.fbID, nil |
| 149 | - | c := gemClose{handle: handle} | |||
| 150 | - | _ = ioctl(o.fd, ioctlGemClose, unsafe.Pointer(&c)) | |||
| 151 | } | 169 | } | ||
| 152 | 170 | ||||
| 153 | - | // dropClientFBs frees framebuffers unused for scanoutIdleTTL flips, or all | 171 | + | // dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all |
| 154 | // of them, except those on screen or queued. | 172 | // of them, except those on screen or queued. | ||
| 155 | - | func (o *Output) dropClientFBs(all bool) { | 173 | + | func (o *Output) dropClientFBs(now time.Time, all bool) { |
| 156 | for id, fb := range o.clientFBs { | 174 | for id, fb := range o.clientFBs { | ||
| 157 | if id == o.shown || id == o.queued { | 175 | if id == o.shown || id == o.queued { | ||
| 158 | continue | 176 | continue | ||
| 159 | } | 177 | } | ||
| 160 | - | if !all && o.flips-fb.last < scanoutIdleTTL { | 178 | + | if !all && now.Sub(fb.last) < scanoutIdleTTL { |
| 161 | continue | 179 | continue | ||
| 162 | } | 180 | } | ||
| 163 | - | o.freeFB(fb) | 181 | + | if fb.fbID != 0 { |
| 182 | + | v := fb.fbID | |||
| 183 | + | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&v)) | |||
| 184 | + | } | |||
| 164 | delete(o.clientFBs, id) | 185 | delete(o.clientFBs, id) | ||
| 165 | } | 186 | } | ||
| 166 | } | 187 | } | ||
| 167 | 188 | ||||
| 168 | - | func (o *Output) freeFB(fb *clientFB) { | 189 | + | func scanoutable(f uint32) bool { _, ok := scanoutFormat(f); return ok } |
| 169 | - | if fb.fbID != 0 { | |||
| 170 | - | id := fb.fbID | |||
| 171 | - | _ = ioctl(o.fd, ioctlRmFB, unsafe.Pointer(&id)) | |||
| 172 | - | } | |||
| 173 | - | if fb.handle != 0 { | |||
| 174 | - | o.closeHandle(fb.handle) | |||
| 175 | - | } | |||
| 176 | - | } | |||
M internal/adapters/drm/scanout_test.go +16 -1
| @@ -9,7 +9,7 @@ import ( | |||
| 9 | 9 | func TestScanoutCandidate(t *testing.T) { | |
| 10 | 10 | full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true} | |
| 11 | 11 | scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{full}} | |
| 12 | - | buf := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{}}} | |
| 12 | + | buf := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccARGB, Planes: []ports.DMABufPlane{{}}} | |
| 13 | 13 | game := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, DMABuf: buf} | |
| 14 | 14 | for _, tc := range []struct { | |
| 15 | 15 | name string | |
| @@ -37,6 +37,12 @@ func TestScanoutCandidate(t *testing.T) { | |||
| 37 | 37 | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | |
| 38 | 38 | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"}, | |
| 39 | 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"}, | |
| 40 | 46 | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | |
| 41 | 47 | } { | |
| 42 | 48 | s := scene | |
| @@ -53,3 +59,12 @@ func TestScanoutCandidate(t *testing.T) { | |||
| 53 | 59 | } | |
| 54 | 60 | } | |
| 55 | 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 | + | } | |
| @@ -9,7 +9,7 @@ import ( | |||||
| 9 | func TestScanoutCandidate(t *testing.T) { | 9 | func TestScanoutCandidate(t *testing.T) { | ||
| 10 | full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true} | 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}} | 11 | scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{full}} | ||
| 12 | - | buf := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{}}} | 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} | 13 | game := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, DMABuf: buf} | ||
| 14 | for _, tc := range []struct { | 14 | for _, tc := range []struct { | ||
| 15 | name string | 15 | name string | ||
| @@ -37,6 +37,12 @@ func TestScanoutCandidate(t *testing.T) { | |||||
| 37 | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | 37 | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | ||
| 38 | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"}, | 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"}, | 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"}, | |||
| 40 | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | 46 | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | ||
| 41 | } { | 47 | } { | ||
| 42 | s := scene | 48 | s := scene | ||
| @@ -53,3 +59,12 @@ func TestScanoutCandidate(t *testing.T) { | |||||
| 53 | } | 59 | } | ||
| 54 | } | 60 | } | ||
| 55 | } | 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 +23 -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,9 @@ 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{} | |
| 39 | 45 | update := func(c ports.SurfaceContent) { | |
| 46 | + | seen[c.ID] = max(seen[c.ID], c.Seq) | |
| 40 | 47 | dirty = dirty || scene.Shows(c.ID) | |
| 41 | 48 | if c.Empty() { | |
| 42 | 49 | delete(surfaces, c.ID) | |
| @@ -88,6 +95,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 88 | 95 | } | |
| 89 | 96 | } | |
| 90 | 97 | if !haveScene || !dirty { | |
| 98 | + | // Contents not drawn are still read: report them. | |
| 99 | + | opts.report(false, seen) | |
| 91 | 100 | continue | |
| 92 | 101 | } | |
| 93 | 102 | dirty = false | |
| @@ -104,6 +113,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 104 | 113 | return fmt.Errorf("render frame: %w", err) | |
| 105 | 114 | } | |
| 106 | 115 | frame++ | |
| 116 | + | opts.report(false, seen) | |
| 107 | 117 | if opts.ScreenshotDir != "" { | |
| 108 | 118 | shot := r.Pixels() | |
| 109 | 119 | if opts.Cursor != nil { | |
| @@ -135,3 +145,16 @@ func writePNG(path string, img *image.RGBA) error { | |||
| 135 | 145 | } | |
| 136 | 146 | return os.Rename(f.Name(), path) | |
| 137 | 147 | } | |
| 148 | + | ||
| 149 | + | // report sends what the output read; a full channel drops it. | |
| 150 | + | // Headless frames are not paced by a screen: Flip stays false and frame | |
| 151 | + | // callbacks follow the refresh timer. | |
| 152 | + | func (opts Options) report(flip bool, seen map[ports.WindowID]uint64) { | |
| 153 | + | if opts.Presented == nil { | |
| 154 | + | return | |
| 155 | + | } | |
| 156 | + | select { | |
| 157 | + | case opts.Presented <- ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: maps.Clone(seen)}: | |
| 158 | + | default: | |
| 159 | + | } | |
| 160 | + | } | |
| @@ -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,9 @@ 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{} | |||
| 39 | update := func(c ports.SurfaceContent) { | 45 | update := func(c ports.SurfaceContent) { | ||
| 46 | + | seen[c.ID] = max(seen[c.ID], c.Seq) | |||
| 40 | dirty = dirty || scene.Shows(c.ID) | 47 | dirty = dirty || scene.Shows(c.ID) | ||
| 41 | if c.Empty() { | 48 | if c.Empty() { | ||
| 42 | delete(surfaces, c.ID) | 49 | delete(surfaces, c.ID) | ||
| @@ -88,6 +95,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 88 | } | 95 | } | ||
| 89 | } | 96 | } | ||
| 90 | if !haveScene || !dirty { | 97 | if !haveScene || !dirty { | ||
| 98 | + | // Contents not drawn are still read: report them. | |||
| 99 | + | opts.report(false, seen) | |||
| 91 | continue | 100 | continue | ||
| 92 | } | 101 | } | ||
| 93 | dirty = false | 102 | dirty = false | ||
| @@ -104,6 +113,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 104 | return fmt.Errorf("render frame: %w", err) | 113 | return fmt.Errorf("render frame: %w", err) | ||
| 105 | } | 114 | } | ||
| 106 | frame++ | 115 | frame++ | ||
| 116 | + | opts.report(false, seen) | |||
| 107 | if opts.ScreenshotDir != "" { | 117 | if opts.ScreenshotDir != "" { | ||
| 108 | shot := r.Pixels() | 118 | shot := r.Pixels() | ||
| 109 | if opts.Cursor != nil { | 119 | if opts.Cursor != nil { | ||
| @@ -135,3 +145,16 @@ func writePNG(path string, img *image.RGBA) error { | |||||
| 135 | } | 145 | } | ||
| 136 | return os.Rename(f.Name(), path) | 146 | return os.Rename(f.Name(), path) | ||
| 137 | } | 147 | } | ||
| 148 | + | ||||
| 149 | + | // report sends what the output read; a full channel drops it. | |||
| 150 | + | // Headless frames are not paced by a screen: Flip stays false and frame | |||
| 151 | + | // callbacks follow the refresh timer. | |||
| 152 | + | func (opts Options) report(flip bool, seen map[ports.WindowID]uint64) { | |||
| 153 | + | if opts.Presented == nil { | |||
| 154 | + | return | |||
| 155 | + | } | |||
| 156 | + | select { | |||
| 157 | + | case opts.Presented <- ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: maps.Clone(seen)}: | |||
| 158 | + | default: | |||
| 159 | + | } | |||
| 160 | + | } | |||
M internal/adapters/wayland/frames.go +8 -4
| @@ -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,14 +27,15 @@ func (s *Server) pace(ctx context.Context) { | |||
| 25 | 27 | presented := s.channels.Presented | |
| 26 | 28 | for { | |
| 27 | 29 | flipped := map[string]bool{} | |
| 28 | - | scanned := map[string]uint64{} | |
| 30 | + | var reports []ports.OutputPresented | |
| 29 | 31 | select { | |
| 30 | 32 | case <-ctx.Done(): | |
| 31 | 33 | return | |
| 32 | 34 | case <-s.display.Stopped(): | |
| 33 | 35 | return | |
| 34 | 36 | case p := <-presented: | |
| 35 | - | flipped[p.Output], scanned[p.Output] = true, p.Scanout | |
| 37 | + | flipped[p.Output] = flipped[p.Output] || p.Flip | |
| 38 | + | reports = append(reports, p) | |
| 36 | 39 | case <-s.frameReady: | |
| 37 | 40 | case <-timer.C: | |
| 38 | 41 | } | |
| @@ -42,7 +45,8 @@ func (s *Server) pace(ctx context.Context) { | |||
| 42 | 45 | for { | |
| 43 | 46 | select { | |
| 44 | 47 | case p := <-presented: | |
| 45 | - | flipped[p.Output], scanned[p.Output] = true, p.Scanout | |
| 48 | + | flipped[p.Output] = flipped[p.Output] || p.Flip | |
| 49 | + | reports = append(reports, p) | |
| 46 | 50 | default: | |
| 47 | 51 | break drain | |
| 48 | 52 | } | |
| @@ -55,7 +59,7 @@ func (s *Server) pace(ctx context.Context) { | |||
| 55 | 59 | for _, name := range due { | |
| 56 | 60 | s.sendFrames(name) | |
| 57 | 61 | } | |
| 58 | - | if s.releaseHeld(time.Now(), scanned) && idle { | |
| 62 | + | if s.releaseHeld(time.Now(), reports) && idle { | |
| 59 | 63 | // Held buffers wait for a flip or heldTimeout. | |
| 60 | 64 | wait, idle = heldTimeout, false | |
| 61 | 65 | } | |
| @@ -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,14 +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{} | ||
| 28 | - | scanned := map[string]uint64{} | 30 | + | var reports []ports.OutputPresented |
| 29 | select { | 31 | select { | ||
| 30 | case <-ctx.Done(): | 32 | case <-ctx.Done(): | ||
| 31 | return | 33 | return | ||
| 32 | case <-s.display.Stopped(): | 34 | case <-s.display.Stopped(): | ||
| 33 | return | 35 | return | ||
| 34 | case p := <-presented: | 36 | case p := <-presented: | ||
| 35 | - | flipped[p.Output], scanned[p.Output] = true, p.Scanout | 37 | + | flipped[p.Output] = flipped[p.Output] || p.Flip |
| 38 | + | reports = append(reports, p) | |||
| 36 | case <-s.frameReady: | 39 | case <-s.frameReady: | ||
| 37 | case <-timer.C: | 40 | case <-timer.C: | ||
| 38 | } | 41 | } | ||
| @@ -42,7 +45,8 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 42 | for { | 45 | for { | ||
| 43 | select { | 46 | select { | ||
| 44 | case p := <-presented: | 47 | case p := <-presented: | ||
| 45 | - | flipped[p.Output], scanned[p.Output] = true, p.Scanout | 48 | + | flipped[p.Output] = flipped[p.Output] || p.Flip |
| 49 | + | reports = append(reports, p) | |||
| 46 | default: | 50 | default: | ||
| 47 | break drain | 51 | break drain | ||
| 48 | } | 52 | } | ||
| @@ -55,7 +59,7 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 55 | for _, name := range due { | 59 | for _, name := range due { | ||
| 56 | s.sendFrames(name) | 60 | s.sendFrames(name) | ||
| 57 | } | 61 | } | ||
| 58 | - | if s.releaseHeld(time.Now(), scanned) && idle { | 62 | + | if s.releaseHeld(time.Now(), reports) && idle { |
| 59 | // Held buffers wait for a flip or heldTimeout. | 63 | // Held buffers wait for a flip or heldTimeout. | ||
| 60 | wait, idle = heldTimeout, false | 64 | wait, idle = heldTimeout, false | ||
| 61 | } | 65 | } | ||
M internal/adapters/wayland/release.go +57 -56
| @@ -3,98 +3,99 @@ package wayland | |||
| 3 | 3 | import ( | |
| 4 | 4 | "time" | |
| 5 | 5 | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 6 | 7 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 7 | 8 | ) | |
| 8 | 9 | ||
| 9 | - | // Buffer release. A replaced wl_buffer goes back to the client (release) | |
| 10 | - | // when nothing reads it any more. Composed outputs copy buffers into their | |
| 11 | - | // own image, so buffers are released at once. While an output scans out a | |
| 12 | - | // client buffer directly (ADR 006), a replaced GPU buffer of a surface on | |
| 13 | - | // it may be on screen or queued for a page flip: it is held until the | |
| 14 | - | // output presents a frame that scans out another buffer, or heldTimeout | |
| 15 | - | // passes without a flip. The first buffer of a switch to scanout is not | |
| 16 | - | // held (known limitation: it may be released while its flip is queued). | |
| 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. | |
| 17 | 19 | ||
| 18 | - | // heldTimeout releases held buffers of an output that stopped flipping; a | |
| 19 | - | // queued flip completes well within it. | |
| 20 | + | // heldTimeout releases held buffers of an output that stopped reporting. | |
| 20 | 21 | const heldTimeout = 100 * time.Millisecond | |
| 21 | 22 | ||
| 22 | 23 | type heldBuffer struct { | |
| 23 | - | res *wayland.Buffer | |
| 24 | - | id uint64 | |
| 25 | - | at time.Time | |
| 24 | + | res *wayland.Buffer | |
| 25 | + | window ports.WindowID | |
| 26 | + | output string | |
| 27 | + | id uint64 // DMABuf ID, 0 for wl_shm | |
| 28 | + | after uint64 // the window's content Seq when the buffer was replaced | |
| 29 | + | at time.Time | |
| 26 | 30 | } | |
| 27 | 31 | ||
| 28 | - | // releaseBuffer releases a replaced buffer, or holds a GPU buffer until | |
| 29 | - | // its output flipped away from it. | |
| 32 | + | // releaseBuffer releases a replaced buffer, or holds it until the output | |
| 33 | + | // showing its window no longer reads it. | |
| 30 | 34 | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |
| 31 | 35 | if !b.Resource.Alive() { | |
| 32 | 36 | return | |
| 33 | 37 | } | |
| 34 | - | d, ok := s.buffers[b.Resource].(*dmabufBuffer) | |
| 35 | - | name := s.frameOutput(surf) | |
| 36 | - | // Only an output scanning out a client buffer reads it after the | |
| 37 | - | // frame: composed outputs copied it already. | |
| 38 | - | if !ok || name == "" || s.scanned[name] == 0 { | |
| 38 | + | window, name := surf.root().windowID(), s.frameOutput(surf) | |
| 39 | + | if window == 0 || name == "" { | |
| 40 | + | // Not drawn (unmapped, hidden, a cursor): nothing reads it. | |
| 39 | 41 | b.SendRelease() | |
| 40 | 42 | return | |
| 41 | 43 | } | |
| 42 | - | s.held[name] = append(s.held[name], heldBuffer{res: b, id: d.buf.ID, at: time.Now()}) | |
| 43 | - | // Wake the pacer: it releases the buffer after the next flip. | |
| 44 | + | h := heldBuffer{res: b, window: window, output: name, at: time.Now()} | |
| 45 | + | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | |
| 46 | + | h.id = d.buf.ID | |
| 47 | + | } | |
| 48 | + | s.contentMu.Lock() | |
| 49 | + | h.after = s.contentSeq[window] | |
| 50 | + | s.contentMu.Unlock() | |
| 51 | + | s.held = append(s.held, h) | |
| 52 | + | // Wake the pacer: it releases the buffer on the next output report. | |
| 44 | 53 | select { | |
| 45 | 54 | case s.frameReady <- struct{}{}: | |
| 46 | 55 | default: | |
| 47 | 56 | } | |
| 48 | 57 | } | |
| 49 | 58 | ||
| 50 | - | // releaseHeld records the buffers scanned out by the outputs that flipped | |
| 51 | - | // and releases held buffers no output shows. It reports whether buffers | |
| 52 | - | // are still held. | |
| 53 | - | func (s *Server) releaseHeld(now time.Time, scanned map[string]uint64) bool { | |
| 54 | - | for name, id := range scanned { | |
| 55 | - | if id == 0 { | |
| 56 | - | delete(s.scanned, name) | |
| 57 | - | } else { | |
| 58 | - | s.scanned[name] = id | |
| 59 | - | } | |
| 59 | + | // releaseHeld records the output reports and releases the buffers no | |
| 60 | + | // output reads. It reports whether buffers are still held. | |
| 61 | + | func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool { | |
| 62 | + | for _, r := range reports { | |
| 63 | + | s.reports[r.Output] = r | |
| 60 | 64 | } | |
| 61 | 65 | live := map[string]bool{} | |
| 62 | 66 | for _, o := range s.outputs { | |
| 63 | 67 | live[o.name()] = true | |
| 64 | 68 | } | |
| 65 | - | for name := range s.scanned { | |
| 69 | + | for name := range s.reports { | |
| 66 | 70 | if !live[name] { | |
| 67 | - | delete(s.scanned, name) | |
| 71 | + | delete(s.reports, name) | |
| 68 | 72 | } | |
| 69 | 73 | } | |
| 70 | - | for name, list := range s.held { | |
| 71 | - | _, flipped := scanned[name] | |
| 72 | - | kept := list[:0] | |
| 73 | - | for _, h := range list { | |
| 74 | - | if keepHeld(h, s.scanned[name], live[name], flipped, now) { | |
| 75 | - | kept = append(kept, h) | |
| 76 | - | continue | |
| 77 | - | } | |
| 78 | - | if h.res.Resource.Alive() { | |
| 79 | - | h.res.SendRelease() | |
| 80 | - | } | |
| 74 | + | kept := s.held[:0] | |
| 75 | + | for _, h := range s.held { | |
| 76 | + | r, reported := s.reports[h.output] | |
| 77 | + | if live[h.output] && keepHeld(h, r, reported, now) { | |
| 78 | + | kept = append(kept, h) | |
| 79 | + | continue | |
| 81 | 80 | } | |
| 82 | - | if len(kept) == 0 { | |
| 83 | - | delete(s.held, name) | |
| 84 | - | } else { | |
| 85 | - | s.held[name] = kept | |
| 81 | + | if h.res.Resource.Alive() { | |
| 82 | + | h.res.SendRelease() | |
| 86 | 83 | } | |
| 87 | 84 | } | |
| 85 | + | clear(s.held[len(kept):]) | |
| 86 | + | s.held = kept | |
| 88 | 87 | return len(s.held) > 0 | |
| 89 | 88 | } | |
| 90 | 89 | ||
| 91 | - | // keepHeld reports whether a held buffer must stay with the client's | |
| 92 | - | // compositor: its output scans it out, or has not flipped since it was | |
| 93 | - | // replaced (a queued flip may still show it) and heldTimeout has not | |
| 94 | - | // passed. A buffer of an unplugged output is released. | |
| 95 | - | func keepHeld(h heldBuffer, scanned uint64, live, flipped bool, now time.Time) bool { | |
| 96 | - | if scanned == h.id { | |
| 90 | + | // keepHeld reports whether the output (last report r) may still read a | |
| 91 | + | // held buffer: it scans it out or queued it, it has not got a later | |
| 92 | + | // content of the window yet, or it has not reported within heldTimeout. | |
| 93 | + | func keepHeld(h heldBuffer, r ports.OutputPresented, reported bool, now time.Time) bool { | |
| 94 | + | if reported && h.id != 0 && (r.Shown == h.id || r.Queued == h.id) { | |
| 97 | 95 | return true | |
| 98 | 96 | } | |
| 99 | - | return live && !flipped && now.Sub(h.at) < heldTimeout | |
| 97 | + | if reported && r.Seen[h.window] > h.after { | |
| 98 | + | return false | |
| 99 | + | } | |
| 100 | + | return now.Sub(h.at) < heldTimeout | |
| 100 | 101 | } | |
| @@ -3,98 +3,99 @@ package wayland | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "time" | 4 | "time" | ||
| 5 | 5 | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 6 | "github.com/bnema/purego-libwayland/protocol/wayland" | 7 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 7 | ) | 8 | ) | ||
| 8 | 9 | ||||
| 9 | - | // Buffer release. A replaced wl_buffer goes back to the client (release) | 10 | + | // Buffer release. Outputs read client buffers in place: the renderer |
| 10 | - | // when nothing reads it any more. Composed outputs copy buffers into their | 11 | + | // samples them when it draws, and an output scanning out a client buffer |
| 11 | - | // own image, so buffers are released at once. While an output scans out a | 12 | + | // (ADR 006) shows it until the next flip. A replaced wl_buffer therefore |
| 12 | - | // client buffer directly (ADR 006), a replaced GPU buffer of a surface on | 13 | + | // goes back to the client (release) only when the output showing its |
| 13 | - | // it may be on screen or queued for a page flip: it is held until the | 14 | + | // window reports (ports.OutputPresented) that it has a later content of |
| 14 | - | // output presents a frame that scans out another buffer, or heldTimeout | 15 | + | // that window and neither shows nor queues the buffer. Outputs keep only |
| 15 | - | // passes without a flip. The first buffer of a switch to scanout is not | 16 | + | // the latest content of a window, so they never draw an older buffer |
| 16 | - | // held (known limitation: it may be released while its flip is queued). | 17 | + | // again. heldTimeout guards against an output that stopped reporting |
| 18 | + | // (switched away, stalled): a buffer it does not scan out is released then. | |||
| 17 | 19 | ||||
| 18 | - | // heldTimeout releases held buffers of an output that stopped flipping; a | 20 | + | // heldTimeout releases held buffers of an output that stopped reporting. |
| 19 | - | // queued flip completes well within it. | |||
| 20 | const heldTimeout = 100 * time.Millisecond | 21 | const heldTimeout = 100 * time.Millisecond | ||
| 21 | 22 | ||||
| 22 | type heldBuffer struct { | 23 | type heldBuffer struct { | ||
| 23 | - | res *wayland.Buffer | 24 | + | res *wayland.Buffer |
| 24 | - | id uint64 | 25 | + | window ports.WindowID |
| 25 | - | at time.Time | 26 | + | output string |
| 27 | + | id uint64 // DMABuf ID, 0 for wl_shm | |||
| 28 | + | after uint64 // the window's content Seq when the buffer was replaced | |||
| 29 | + | at time.Time | |||
| 26 | } | 30 | } | ||
| 27 | 31 | ||||
| 28 | - | // releaseBuffer releases a replaced buffer, or holds a GPU buffer until | 32 | + | // releaseBuffer releases a replaced buffer, or holds it until the output |
| 29 | - | // its output flipped away from it. | 33 | + | // showing its window no longer reads it. |
| 30 | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | 34 | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | ||
| 31 | if !b.Resource.Alive() { | 35 | if !b.Resource.Alive() { | ||
| 32 | return | 36 | return | ||
| 33 | } | 37 | } | ||
| 34 | - | d, ok := s.buffers[b.Resource].(*dmabufBuffer) | 38 | + | window, name := surf.root().windowID(), s.frameOutput(surf) |
| 35 | - | name := s.frameOutput(surf) | 39 | + | if window == 0 || name == "" { |
| 36 | - | // Only an output scanning out a client buffer reads it after the | 40 | + | // Not drawn (unmapped, hidden, a cursor): nothing reads it. |
| 37 | - | // frame: composed outputs copied it already. | |||
| 38 | - | if !ok || name == "" || s.scanned[name] == 0 { | |||
| 39 | b.SendRelease() | 41 | b.SendRelease() | ||
| 40 | return | 42 | return | ||
| 41 | } | 43 | } | ||
| 42 | - | s.held[name] = append(s.held[name], heldBuffer{res: b, id: d.buf.ID, at: time.Now()}) | 44 | + | h := heldBuffer{res: b, window: window, output: name, at: time.Now()} |
| 43 | - | // Wake the pacer: it releases the buffer after the next flip. | 45 | + | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { |
| 46 | + | h.id = d.buf.ID | |||
| 47 | + | } | |||
| 48 | + | s.contentMu.Lock() | |||
| 49 | + | h.after = s.contentSeq[window] | |||
| 50 | + | s.contentMu.Unlock() | |||
| 51 | + | s.held = append(s.held, h) | |||
| 52 | + | // Wake the pacer: it releases the buffer on the next output report. | |||
| 44 | select { | 53 | select { | ||
| 45 | case s.frameReady <- struct{}{}: | 54 | case s.frameReady <- struct{}{}: | ||
| 46 | default: | 55 | default: | ||
| 47 | } | 56 | } | ||
| 48 | } | 57 | } | ||
| 49 | 58 | ||||
| 50 | - | // releaseHeld records the buffers scanned out by the outputs that flipped | 59 | + | // releaseHeld records the output reports and releases the buffers no |
| 51 | - | // and releases held buffers no output shows. It reports whether buffers | 60 | + | // output reads. It reports whether buffers are still held. |
| 52 | - | // are still held. | 61 | + | func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool { |
| 53 | - | func (s *Server) releaseHeld(now time.Time, scanned map[string]uint64) bool { | 62 | + | for _, r := range reports { |
| 54 | - | for name, id := range scanned { | 63 | + | s.reports[r.Output] = r |
| 55 | - | if id == 0 { | |||
| 56 | - | delete(s.scanned, name) | |||
| 57 | - | } else { | |||
| 58 | - | s.scanned[name] = id | |||
| 59 | - | } | |||
| 60 | } | 64 | } | ||
| 61 | live := map[string]bool{} | 65 | live := map[string]bool{} | ||
| 62 | for _, o := range s.outputs { | 66 | for _, o := range s.outputs { | ||
| 63 | live[o.name()] = true | 67 | live[o.name()] = true | ||
| 64 | } | 68 | } | ||
| 65 | - | for name := range s.scanned { | 69 | + | for name := range s.reports { |
| 66 | if !live[name] { | 70 | if !live[name] { | ||
| 67 | - | delete(s.scanned, name) | 71 | + | delete(s.reports, name) |
| 68 | } | 72 | } | ||
| 69 | } | 73 | } | ||
| 70 | - | for name, list := range s.held { | 74 | + | kept := s.held[:0] |
| 71 | - | _, flipped := scanned[name] | 75 | + | for _, h := range s.held { |
| 72 | - | kept := list[:0] | 76 | + | r, reported := s.reports[h.output] |
| 73 | - | for _, h := range list { | 77 | + | if live[h.output] && keepHeld(h, r, reported, now) { |
| 74 | - | if keepHeld(h, s.scanned[name], live[name], flipped, now) { | 78 | + | kept = append(kept, h) |
| 75 | - | kept = append(kept, h) | 79 | + | continue |
| 76 | - | continue | |||
| 77 | - | } | |||
| 78 | - | if h.res.Resource.Alive() { | |||
| 79 | - | h.res.SendRelease() | |||
| 80 | - | } | |||
| 81 | } | 80 | } | ||
| 82 | - | if len(kept) == 0 { | 81 | + | if h.res.Resource.Alive() { |
| 83 | - | delete(s.held, name) | 82 | + | h.res.SendRelease() |
| 84 | - | } else { | |||
| 85 | - | s.held[name] = kept | |||
| 86 | } | 83 | } | ||
| 87 | } | 84 | } | ||
| 85 | + | clear(s.held[len(kept):]) | |||
| 86 | + | s.held = kept | |||
| 88 | return len(s.held) > 0 | 87 | return len(s.held) > 0 | ||
| 89 | } | 88 | } | ||
| 90 | 89 | ||||
| 91 | - | // keepHeld reports whether a held buffer must stay with the client's | 90 | + | // keepHeld reports whether the output (last report r) may still read a |
| 92 | - | // compositor: its output scans it out, or has not flipped since it was | 91 | + | // held buffer: it scans it out or queued it, it has not got a later |
| 93 | - | // replaced (a queued flip may still show it) and heldTimeout has not | 92 | + | // content of the window yet, or it has not reported within heldTimeout. |
| 94 | - | // passed. A buffer of an unplugged output is released. | 93 | + | func keepHeld(h heldBuffer, r ports.OutputPresented, reported bool, now time.Time) bool { |
| 95 | - | func keepHeld(h heldBuffer, scanned uint64, live, flipped bool, now time.Time) bool { | 94 | + | if reported && h.id != 0 && (r.Shown == h.id || r.Queued == h.id) { |
| 96 | - | if scanned == h.id { | |||
| 97 | return true | 95 | return true | ||
| 98 | } | 96 | } | ||
| 99 | - | return live && !flipped && now.Sub(h.at) < heldTimeout | 97 | + | if reported && r.Seen[h.window] > h.after { |
| 98 | + | return false | |||
| 99 | + | } | |||
| 100 | + | return now.Sub(h.at) < heldTimeout | |||
| 100 | } | 101 | } | ||
M internal/adapters/wayland/release_test.go +20 -12
| @@ -3,25 +3,33 @@ package wayland | |||
| 3 | 3 | import ( | |
| 4 | 4 | "testing" | |
| 5 | 5 | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 6 | 8 | ) | |
| 7 | 9 | ||
| 8 | 10 | func TestKeepHeld(t *testing.T) { | |
| 9 | 11 | now := time.Unix(100, 0) | |
| 10 | - | h := heldBuffer{id: 7, at: now} | |
| 12 | + | dma := heldBuffer{window: 1, id: 7, after: 5, at: now} | |
| 13 | + | shm := heldBuffer{window: 1, after: 5, at: now} | |
| 14 | + | seen := func(seq uint64) map[ports.WindowID]uint64 { return map[ports.WindowID]uint64{1: seq} } | |
| 11 | 15 | for _, tc := range []struct { | |
| 12 | - | name string | |
| 13 | - | scanned uint64 | |
| 14 | - | live, flipped, want bool | |
| 15 | - | after time.Duration | |
| 16 | + | name string | |
| 17 | + | h heldBuffer | |
| 18 | + | r ports.OutputPresented | |
| 19 | + | reported bool | |
| 20 | + | after time.Duration | |
| 21 | + | want bool | |
| 16 | 22 | }{ | |
| 17 | - | {"waits for a flip", 0, true, false, true, 0}, | |
| 18 | - | {"composed flip releases", 0, true, true, false, 0}, | |
| 19 | - | {"scanned out stays", 7, true, true, true, time.Second}, | |
| 20 | - | {"another buffer scanned out", 8, true, true, false, 0}, | |
| 21 | - | {"idle output times out", 0, true, false, false, heldTimeout}, | |
| 22 | - | {"unplugged output releases", 0, false, false, false, 0}, | |
| 23 | + | {"no report yet", dma, ports.OutputPresented{}, false, 0, true}, | |
| 24 | + | {"output has only this content", dma, ports.OutputPresented{Seen: seen(5)}, true, 0, true}, | |
| 25 | + | {"later content seen", dma, ports.OutputPresented{Seen: seen(6)}, true, 0, false}, | |
| 26 | + | {"scanned out", dma, ports.OutputPresented{Shown: 7, Seen: seen(6)}, true, time.Second, true}, | |
| 27 | + | {"queued for scanout", dma, ports.OutputPresented{Queued: 7, Seen: seen(6)}, true, time.Second, true}, | |
| 28 | + | {"another buffer scanned out", dma, ports.OutputPresented{Shown: 8, Seen: seen(6)}, true, 0, false}, | |
| 29 | + | {"silent output times out", dma, ports.OutputPresented{Seen: seen(5)}, true, heldTimeout, false}, | |
| 30 | + | {"shm follows content", shm, ports.OutputPresented{Shown: 0, Seen: seen(6)}, true, 0, false}, | |
| 23 | 31 | } { | |
| 24 | - | if got := keepHeld(h, tc.scanned, tc.live, tc.flipped, now.Add(tc.after)); got != tc.want { | |
| 32 | + | if got := keepHeld(tc.h, tc.r, tc.reported, now.Add(tc.after)); got != tc.want { | |
| 25 | 33 | t.Errorf("%s: %v", tc.name, got) | |
| 26 | 34 | } | |
| 27 | 35 | } | |
| @@ -3,25 +3,33 @@ package wayland | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "testing" | 4 | "testing" | ||
| 5 | "time" | 5 | "time" | ||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 6 | ) | 8 | ) | ||
| 7 | 9 | ||||
| 8 | func TestKeepHeld(t *testing.T) { | 10 | func TestKeepHeld(t *testing.T) { | ||
| 9 | now := time.Unix(100, 0) | 11 | now := time.Unix(100, 0) | ||
| 10 | - | h := heldBuffer{id: 7, at: now} | 12 | + | dma := heldBuffer{window: 1, id: 7, after: 5, at: now} |
| 13 | + | shm := heldBuffer{window: 1, after: 5, at: now} | |||
| 14 | + | seen := func(seq uint64) map[ports.WindowID]uint64 { return map[ports.WindowID]uint64{1: seq} } | |||
| 11 | for _, tc := range []struct { | 15 | for _, tc := range []struct { | ||
| 12 | - | name string | 16 | + | name string |
| 13 | - | scanned uint64 | 17 | + | h heldBuffer |
| 14 | - | live, flipped, want bool | 18 | + | r ports.OutputPresented |
| 15 | - | after time.Duration | 19 | + | reported bool |
| 20 | + | after time.Duration | |||
| 21 | + | want bool | |||
| 16 | }{ | 22 | }{ | ||
| 17 | - | {"waits for a flip", 0, true, false, true, 0}, | 23 | + | {"no report yet", dma, ports.OutputPresented{}, false, 0, true}, |
| 18 | - | {"composed flip releases", 0, true, true, false, 0}, | 24 | + | {"output has only this content", dma, ports.OutputPresented{Seen: seen(5)}, true, 0, true}, |
| 19 | - | {"scanned out stays", 7, true, true, true, time.Second}, | 25 | + | {"later content seen", dma, ports.OutputPresented{Seen: seen(6)}, true, 0, false}, |
| 20 | - | {"another buffer scanned out", 8, true, true, false, 0}, | 26 | + | {"scanned out", dma, ports.OutputPresented{Shown: 7, Seen: seen(6)}, true, time.Second, true}, |
| 21 | - | {"idle output times out", 0, true, false, false, heldTimeout}, | 27 | + | {"queued for scanout", dma, ports.OutputPresented{Queued: 7, Seen: seen(6)}, true, time.Second, true}, |
| 22 | - | {"unplugged output releases", 0, false, false, false, 0}, | 28 | + | {"another buffer scanned out", dma, ports.OutputPresented{Shown: 8, Seen: seen(6)}, true, 0, false}, |
| 29 | + | {"silent output times out", dma, ports.OutputPresented{Seen: seen(5)}, true, heldTimeout, false}, | |||
| 30 | + | {"shm follows content", shm, ports.OutputPresented{Shown: 0, Seen: seen(6)}, true, 0, false}, | |||
| 23 | } { | 31 | } { | ||
| 24 | - | if got := keepHeld(h, tc.scanned, tc.live, tc.flipped, now.Add(tc.after)); got != tc.want { | 32 | + | if got := keepHeld(tc.h, tc.r, tc.reported, now.Add(tc.after)); got != tc.want { |
| 25 | t.Errorf("%s: %v", tc.name, got) | 33 | t.Errorf("%s: %v", tc.name, got) | ||
| 26 | } | 34 | } | ||
| 27 | } | 35 | } | ||
M internal/adapters/wayland/server.go +7 -6
| @@ -52,10 +52,10 @@ type Server struct { | |||
| 52 | 52 | awaiting map[string][]*wayland.Callback | |
| 53 | 53 | frameDue map[string]time.Time | |
| 54 | 54 | frameReady chan struct{} | |
| 55 | - | // held buffers wait for a flip (release.go); scanned is the dmabuf ID | |
| 56 | - | // each output scans out directly. | |
| 57 | - | held map[string][]heldBuffer | |
| 58 | - | scanned map[string]uint64 | |
| 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 | 59 | frames uint64 | |
| 60 | 60 | started time.Time | |
| 61 | 61 | surfaces map[*server.Resource]*surface | |
| @@ -147,7 +147,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 147 | 147 | d.Close() | |
| 148 | 148 | return nil, err | |
| 149 | 149 | } | |
| 150 | - | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), held: map[string][]heldBuffer{}, scanned: map[string]uint64{}, 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} | |
| 151 | 151 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | |
| 152 | 152 | if opts.Keymap != "" { | |
| 153 | 153 | fd, size, e := keymapFile(opts.Keymap) | |
| @@ -266,8 +266,9 @@ func (s *Server) emitContent(c ports.SurfaceContent) { | |||
| 266 | 266 | return | |
| 267 | 267 | } | |
| 268 | 268 | s.contentMu.Lock() | |
| 269 | - | s.contents[c.ID] = c | |
| 270 | 269 | s.contentSeq[c.ID]++ | |
| 270 | + | c.Seq = s.contentSeq[c.ID] | |
| 271 | + | s.contents[c.ID] = c | |
| 271 | 272 | close(s.contentNotify) | |
| 272 | 273 | s.contentNotify = make(chan struct{}) | |
| 273 | 274 | s.contentMu.Unlock() | |
| @@ -52,10 +52,10 @@ 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 | - | // held buffers wait for a flip (release.go); scanned is the dmabuf ID | 55 | + | // held buffers wait until no output reads them (release.go); reports |
| 56 | - | // each output scans out directly. | 56 | + | // are the outputs' latest ports.OutputPresented. |
| 57 | - | held map[string][]heldBuffer | 57 | + | held []heldBuffer |
| 58 | - | scanned map[string]uint64 | 58 | + | reports map[string]ports.OutputPresented |
| 59 | frames uint64 | 59 | frames uint64 | ||
| 60 | started time.Time | 60 | started time.Time | ||
| 61 | surfaces map[*server.Resource]*surface | 61 | surfaces map[*server.Resource]*surface | ||
| @@ -147,7 +147,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 147 | d.Close() | 147 | d.Close() | ||
| 148 | return nil, err | 148 | return nil, err | ||
| 149 | } | 149 | } | ||
| 150 | - | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), held: map[string][]heldBuffer{}, scanned: map[string]uint64{}, 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} |
| 151 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | 151 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | ||
| 152 | if opts.Keymap != "" { | 152 | if opts.Keymap != "" { | ||
| 153 | fd, size, e := keymapFile(opts.Keymap) | 153 | fd, size, e := keymapFile(opts.Keymap) | ||
| @@ -266,8 +266,9 @@ func (s *Server) emitContent(c ports.SurfaceContent) { | |||||
| 266 | return | 266 | return | ||
| 267 | } | 267 | } | ||
| 268 | s.contentMu.Lock() | 268 | s.contentMu.Lock() | ||
| 269 | - | s.contents[c.ID] = c | |||
| 270 | s.contentSeq[c.ID]++ | 269 | s.contentSeq[c.ID]++ | ||
| 270 | + | c.Seq = s.contentSeq[c.ID] | |||
| 271 | + | s.contents[c.ID] = c | |||
| 271 | close(s.contentNotify) | 272 | close(s.contentNotify) | ||
| 272 | s.contentNotify = make(chan struct{}) | 273 | s.contentNotify = make(chan struct{}) | ||
| 273 | s.contentMu.Unlock() | 274 | s.contentMu.Unlock() | ||
M internal/app/app.go +2 -2
| @@ -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 { | |
| @@ -213,7 +213,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 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 { | |
| @@ -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 { | ||
| @@ -213,7 +213,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 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/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 +14 -7
| @@ -278,13 +278,18 @@ 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 | - | // Scanout is the DMABuf ID now scanned out directly, 0 for a composed | |
| 284 | - | // frame: until another flip, that client buffer must not be released. | |
| 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. | |
| 285 | 288 | type OutputPresented struct { | |
| 286 | - | Output string | |
| 287 | - | Scanout uint64 | |
| 289 | + | Output string | |
| 290 | + | Flip bool | |
| 291 | + | Shown, Queued uint64 | |
| 292 | + | Seen map[WindowID]uint64 | |
| 288 | 293 | } | |
| 289 | 294 | ||
| 290 | 295 | // OutputRemoved carries output → core an unplugged display. | |
| @@ -525,7 +530,9 @@ type SceneLayer struct { | |||
| 525 | 530 | // Geometry is the part of the surface that is the window (xdg window | |
| 526 | 531 | // geometry), the rest being client shadows. | |
| 527 | 532 | type SurfaceContent struct { | |
| 528 | - | ID WindowID | |
| 533 | + | ID WindowID | |
| 534 | + | // Seq counts the window's contents (wayland sets it), for release. | |
| 535 | + | Seq uint64 | |
| 529 | 536 | Width, Height int | |
| 530 | 537 | LogicalW, LogicalH int | |
| 531 | 538 | Opaque bool // x formats: ignore the alpha byte | |
| @@ -278,13 +278,18 @@ 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 | - | // Scanout is the DMABuf ID now scanned out directly, 0 for a composed | 283 | + | // on it are due. Shown and Queued are the DMABuf IDs scanned out directly |
| 284 | - | // frame: until another flip, that client buffer must not be released. | 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. | |||
| 285 | type OutputPresented struct { | 288 | type OutputPresented struct { | ||
| 286 | - | Output string | 289 | + | Output string |
| 287 | - | Scanout uint64 | 290 | + | Flip bool |
| 291 | + | Shown, Queued uint64 | |||
| 292 | + | Seen map[WindowID]uint64 | |||
| 288 | } | 293 | } | ||
| 289 | 294 | ||||
| 290 | // OutputRemoved carries output → core an unplugged display. | 295 | // OutputRemoved carries output → core an unplugged display. | ||
| @@ -525,7 +530,9 @@ type SceneLayer struct { | |||||
| 525 | // 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 | ||
| 526 | // geometry), the rest being client shadows. | 531 | // geometry), the rest being client shadows. | ||
| 527 | type SurfaceContent struct { | 532 | type SurfaceContent struct { | ||
| 528 | - | ID WindowID | 533 | + | ID WindowID |
| 534 | + | // Seq counts the window's contents (wayland sets it), for release. | |||
| 535 | + | Seq uint64 | |||
| 529 | Width, Height int | 536 | Width, Height int | ||
| 530 | LogicalW, LogicalH int | 537 | LogicalW, LogicalH int | ||
| 531 | Opaque bool // x formats: ignore the alpha byte | 538 | Opaque bool // x formats: ignore the alpha byte | ||