feat(drm): scan out fullscreen client buffers directly
- commit
- 44724718e7b6b36ee938ad9ededd981a68dedf0f
- author
- brice · · 2026-09-25 16:07 UTC
- parent
- 1364461
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
14 files · +464 -27
M internal/adapters/drm/kms.go +2 -0
| @@ -119,6 +119,8 @@ type Want struct { | |||
| 119 | 119 | Disabled map[string]bool | |
| 120 | 120 | // Modes maps a connector to W, H and Hz (0: highest refresh at W x H). | |
| 121 | 121 | Modes map[string][3]float64 | |
| 122 | + | // NoScanout disables direct scanout (render.direct-scanout = off). | |
| 123 | + | NoScanout bool | |
| 122 | 124 | } | |
| 123 | 125 | ||
| 124 | 126 | // usable reports whether a connector should be driven. | |
| @@ -119,6 +119,8 @@ type Want struct { | |||||
| 119 | Disabled map[string]bool | 119 | Disabled map[string]bool | ||
| 120 | // Modes maps a connector to W, H and Hz (0: highest refresh at W x H). | 120 | // Modes maps a connector to W, H and Hz (0: highest refresh at W x H). | ||
| 121 | Modes map[string][3]float64 | 121 | Modes map[string][3]float64 | ||
| 122 | + | // NoScanout disables direct scanout (render.direct-scanout = off). | |||
| 123 | + | NoScanout bool | |||
| 122 | } | 124 | } | ||
| 123 | 125 | ||||
| 124 | // usable reports whether a connector should be driven. | 126 | // usable reports whether a connector should be driven. | ||
M internal/adapters/drm/kms_test.go +3 -0
| @@ -20,6 +20,9 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||
| 20 | 20 | {"page_flip", unsafe.Sizeof(pageFlip{}), ioctlPageFlip}, | |
| 21 | 21 | {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb}, | |
| 22 | 22 | {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb}, | |
| 23 | + | {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2}, | |
| 24 | + | {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle}, | |
| 25 | + | {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose}, | |
| 23 | 26 | } { | |
| 24 | 27 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | |
| 25 | 28 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | |
| @@ -20,6 +20,9 @@ func TestStructSizesMatchIoctls(t *testing.T) { | |||||
| 20 | {"page_flip", unsafe.Sizeof(pageFlip{}), ioctlPageFlip}, | 20 | {"page_flip", unsafe.Sizeof(pageFlip{}), ioctlPageFlip}, | ||
| 21 | {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb}, | 21 | {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb}, | ||
| 22 | {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb}, | 22 | {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb}, | ||
| 23 | + | {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2}, | |||
| 24 | + | {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle}, | |||
| 25 | + | {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose}, | |||
| 23 | } { | 26 | } { | ||
| 24 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | 27 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | ||
| 25 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | 28 | t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want) | ||
M internal/adapters/drm/output.go +66 -5
| @@ -29,6 +29,13 @@ type Output struct { | |||
| 29 | 29 | monitor Monitor | |
| 30 | 30 | cursor *Cursor | |
| 31 | 31 | log zerowrap.Logger | |
| 32 | + | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | |
| 33 | + | // and the buffer on screen and queued (0: the composed image). | |
| 34 | + | scanout bool | |
| 35 | + | clientFBs map[uint64]*clientFB | |
| 36 | + | handles map[uint32]int | |
| 37 | + | shown, queued uint64 | |
| 38 | + | reason string // why the last frame was composed ("" = scanout) | |
| 32 | 39 | } | |
| 33 | 40 | ||
| 34 | 41 | // CursorLoader returns the image of a cursor at an output scale, at most | |
| @@ -38,7 +45,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu | |||
| 38 | 45 | // newOutput allocates buffers and the cursor for a connector on crtc. | |
| 39 | 46 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | |
| 40 | 47 | log := card.log | |
| 41 | - | o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name)} | |
| 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"} | |
| 42 | 49 | var err error | |
| 43 | 50 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 44 | 51 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | |
| @@ -76,6 +83,7 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) } | |||
| 76 | 83 | // modeset shows the front buffer; needed at start and after every VT resume. | |
| 77 | 84 | func (o *Output) modeset() error { | |
| 78 | 85 | o.pending = false | |
| 86 | + | o.shown, o.queued = 0, 0 | |
| 79 | 87 | front := o.bufs[1-o.back] | |
| 80 | 88 | if err := setCrtc(o.fd, o.crtc, o.conn.id, front.fbID, &o.mode); err != nil { | |
| 81 | 89 | return fmt.Errorf("set crtc: %w", err) | |
| @@ -92,10 +100,55 @@ func (o *Output) present(r ports.Renderer) error { | |||
| 92 | 100 | return err | |
| 93 | 101 | } | |
| 94 | 102 | o.pending, o.flipStart = true, time.Now() | |
| 103 | + | o.queued = 0 | |
| 95 | 104 | o.back = 1 - o.back | |
| 96 | 105 | return nil | |
| 97 | 106 | } | |
| 98 | 107 | ||
| 108 | + | // tryScanout flips a fullscreen client buffer directly. It reports false, | |
| 109 | + | // with the reason logged on change, when the frame must be composed. | |
| 110 | + | func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (bool, error) { | |
| 111 | + | reason := "disabled" | |
| 112 | + | var c ports.SurfaceContent | |
| 113 | + | if o.scanout { | |
| 114 | + | c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height()) | |
| 115 | + | } | |
| 116 | + | var fb uint32 | |
| 117 | + | 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 | + | } | |
| 125 | + | } | |
| 126 | + | if reason != o.reason { | |
| 127 | + | o.log.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout") | |
| 128 | + | o.reason = reason | |
| 129 | + | } | |
| 130 | + | if reason != "" { | |
| 131 | + | return false, nil | |
| 132 | + | } | |
| 133 | + | if o.shown == c.DMABuf.ID && o.queued == 0 { | |
| 134 | + | // Already on screen: nothing to flip. | |
| 135 | + | return true, nil | |
| 136 | + | } | |
| 137 | + | if err := flip(o.fd, o.crtc, fb); err != nil { | |
| 138 | + | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | |
| 139 | + | // 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") | |
| 141 | + | o.clientFBs[c.DMABuf.ID].failed = true | |
| 142 | + | o.reason = "flip_refused" | |
| 143 | + | return false, nil | |
| 144 | + | } | |
| 145 | + | return true, err | |
| 146 | + | } | |
| 147 | + | o.pending, o.flipStart = true, time.Now() | |
| 148 | + | o.queued = c.DMABuf.ID | |
| 149 | + | return true, nil | |
| 150 | + | } | |
| 151 | + | ||
| 99 | 152 | // Close restores the CRTC state found at startup, when it had a mode, and | |
| 100 | 153 | // frees buffers. | |
| 101 | 154 | func (o *Output) Close() { | |
| @@ -114,6 +167,8 @@ func (o *Output) Close() { | |||
| 114 | 167 | b.destroy(o.fd) | |
| 115 | 168 | } | |
| 116 | 169 | } | |
| 170 | + | o.shown, o.queued = 0, 0 | |
| 171 | + | o.dropClientFBs(true) | |
| 117 | 172 | } | |
| 118 | 173 | ||
| 119 | 174 | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | |
| @@ -170,8 +225,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 170 | 225 | } | |
| 171 | 226 | o.pending = false | |
| 172 | 227 | o.flips += n | |
| 228 | + | o.shown = o.queued | |
| 229 | + | o.dropClientFBs(false) | |
| 173 | 230 | select { | |
| 174 | - | case presented <- ports.OutputPresented{Output: o.conn.name}: | |
| 231 | + | case presented <- ports.OutputPresented{Output: o.conn.name, Scanout: o.shown}: | |
| 175 | 232 | default: | |
| 176 | 233 | } | |
| 177 | 234 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | |
| @@ -210,11 +267,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 210 | 267 | continue | |
| 211 | 268 | } | |
| 212 | 269 | start := time.Now() | |
| 213 | - | if err := r.Render(scene, surfaces); err != nil { | |
| 214 | - | return fmt.Errorf("render frame: %w", err) | |
| 270 | + | direct, err := o.tryScanout(scene, surfaces) | |
| 271 | + | if !direct { | |
| 272 | + | if err := r.Render(scene, surfaces); err != nil { | |
| 273 | + | return fmt.Errorf("render frame: %w", err) | |
| 274 | + | } | |
| 275 | + | err = o.present(r) | |
| 215 | 276 | } | |
| 216 | 277 | copyStart := time.Now() | |
| 217 | - | if err := o.present(r); err != nil { | |
| 278 | + | if err != nil { | |
| 218 | 279 | if errors.Is(err, unix.EBUSY) { | |
| 219 | 280 | // A flip is still in flight; retry after its event. | |
| 220 | 281 | o.pending, o.flipStart = true, time.Now() | |
| @@ -29,6 +29,13 @@ type Output struct { | |||||
| 29 | monitor Monitor | 29 | monitor Monitor | ||
| 30 | cursor *Cursor | 30 | cursor *Cursor | ||
| 31 | log zerowrap.Logger | 31 | log zerowrap.Logger | ||
| 32 | + | // Direct scanout: client framebuffers by DMABuf ID, GEM handle counts, | |||
| 33 | + | // and the buffer on screen and queued (0: the composed image). | |||
| 34 | + | scanout bool | |||
| 35 | + | clientFBs map[uint64]*clientFB | |||
| 36 | + | handles map[uint32]int | |||
| 37 | + | shown, queued uint64 | |||
| 38 | + | reason string // why the last frame was composed ("" = scanout) | |||
| 32 | } | 39 | } | ||
| 33 | 40 | ||||
| 34 | // CursorLoader returns the image of a cursor at an output scale, at most | 41 | // CursorLoader returns the image of a cursor at an output scale, at most | ||
| @@ -38,7 +45,7 @@ type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.Cu | |||||
| 38 | // newOutput allocates buffers and the cursor for a connector on crtc. | 45 | // newOutput allocates buffers and the cursor for a connector on crtc. | ||
| 39 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | 46 | func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) { | ||
| 40 | log := card.log | 47 | log := card.log | ||
| 41 | - | o := &Output{fd: card.fd, flipped: card.flips[crtc], crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name)} | 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"} |
| 42 | var err error | 49 | var err error | ||
| 43 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | 50 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | ||
| 44 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | 51 | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | ||
| @@ -76,6 +83,7 @@ func (o *Output) Height() int { return int(o.mode.VDisplay) } | |||||
| 76 | // modeset shows the front buffer; needed at start and after every VT resume. | 83 | // modeset shows the front buffer; needed at start and after every VT resume. | ||
| 77 | func (o *Output) modeset() error { | 84 | func (o *Output) modeset() error { | ||
| 78 | o.pending = false | 85 | o.pending = false | ||
| 86 | + | o.shown, o.queued = 0, 0 | |||
| 79 | front := o.bufs[1-o.back] | 87 | front := o.bufs[1-o.back] | ||
| 80 | if err := setCrtc(o.fd, o.crtc, o.conn.id, front.fbID, &o.mode); err != nil { | 88 | if err := setCrtc(o.fd, o.crtc, o.conn.id, front.fbID, &o.mode); err != nil { | ||
| 81 | return fmt.Errorf("set crtc: %w", err) | 89 | return fmt.Errorf("set crtc: %w", err) | ||
| @@ -92,10 +100,55 @@ func (o *Output) present(r ports.Renderer) error { | |||||
| 92 | return err | 100 | return err | ||
| 93 | } | 101 | } | ||
| 94 | o.pending, o.flipStart = true, time.Now() | 102 | o.pending, o.flipStart = true, time.Now() | ||
| 103 | + | o.queued = 0 | |||
| 95 | o.back = 1 - o.back | 104 | o.back = 1 - o.back | ||
| 96 | return nil | 105 | return nil | ||
| 97 | } | 106 | } | ||
| 98 | 107 | ||||
| 108 | + | // tryScanout flips a fullscreen client buffer directly. It reports false, | |||
| 109 | + | // with the reason logged on change, when the frame must be composed. | |||
| 110 | + | func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (bool, error) { | |||
| 111 | + | reason := "disabled" | |||
| 112 | + | var c ports.SurfaceContent | |||
| 113 | + | if o.scanout { | |||
| 114 | + | c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height()) | |||
| 115 | + | } | |||
| 116 | + | var fb uint32 | |||
| 117 | + | 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 | + | } | |||
| 125 | + | } | |||
| 126 | + | if reason != o.reason { | |||
| 127 | + | o.log.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout") | |||
| 128 | + | o.reason = reason | |||
| 129 | + | } | |||
| 130 | + | if reason != "" { | |||
| 131 | + | return false, nil | |||
| 132 | + | } | |||
| 133 | + | if o.shown == c.DMABuf.ID && o.queued == 0 { | |||
| 134 | + | // Already on screen: nothing to flip. | |||
| 135 | + | return true, nil | |||
| 136 | + | } | |||
| 137 | + | if err := flip(o.fd, o.crtc, fb); err != nil { | |||
| 138 | + | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | |||
| 139 | + | // 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") | |||
| 141 | + | o.clientFBs[c.DMABuf.ID].failed = true | |||
| 142 | + | o.reason = "flip_refused" | |||
| 143 | + | return false, nil | |||
| 144 | + | } | |||
| 145 | + | return true, err | |||
| 146 | + | } | |||
| 147 | + | o.pending, o.flipStart = true, time.Now() | |||
| 148 | + | o.queued = c.DMABuf.ID | |||
| 149 | + | return true, nil | |||
| 150 | + | } | |||
| 151 | + | ||||
| 99 | // Close restores the CRTC state found at startup, when it had a mode, and | 152 | // Close restores the CRTC state found at startup, when it had a mode, and | ||
| 100 | // frees buffers. | 153 | // frees buffers. | ||
| 101 | func (o *Output) Close() { | 154 | func (o *Output) Close() { | ||
| @@ -114,6 +167,8 @@ func (o *Output) Close() { | |||||
| 114 | b.destroy(o.fd) | 167 | b.destroy(o.fd) | ||
| 115 | } | 168 | } | ||
| 116 | } | 169 | } | ||
| 170 | + | o.shown, o.queued = 0, 0 | |||
| 171 | + | o.dropClientFBs(true) | |||
| 117 | } | 172 | } | ||
| 118 | 173 | ||||
| 119 | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | 174 | // Run renders scenes and flips until ctx ends. active reports seat enable/disable. | ||
| @@ -170,8 +225,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 170 | } | 225 | } | ||
| 171 | o.pending = false | 226 | o.pending = false | ||
| 172 | o.flips += n | 227 | o.flips += n | ||
| 228 | + | o.shown = o.queued | |||
| 229 | + | o.dropClientFBs(false) | |||
| 173 | select { | 230 | select { | ||
| 174 | - | case presented <- ports.OutputPresented{Output: o.conn.name}: | 231 | + | case presented <- ports.OutputPresented{Output: o.conn.name, Scanout: o.shown}: |
| 175 | default: | 232 | default: | ||
| 176 | } | 233 | } | ||
| 177 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | 234 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | ||
| @@ -210,11 +267,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 210 | continue | 267 | continue | ||
| 211 | } | 268 | } | ||
| 212 | start := time.Now() | 269 | start := time.Now() | ||
| 213 | - | if err := r.Render(scene, surfaces); err != nil { | 270 | + | direct, err := o.tryScanout(scene, surfaces) |
| 214 | - | return fmt.Errorf("render frame: %w", err) | 271 | + | if !direct { |
| 272 | + | if err := r.Render(scene, surfaces); err != nil { | |||
| 273 | + | return fmt.Errorf("render frame: %w", err) | |||
| 274 | + | } | |||
| 275 | + | err = o.present(r) | |||
| 215 | } | 276 | } | ||
| 216 | copyStart := time.Now() | 277 | copyStart := time.Now() | ||
| 217 | - | if err := o.present(r); err != nil { | 278 | + | if err != nil { |
| 218 | if errors.Is(err, unix.EBUSY) { | 279 | if errors.Is(err, unix.EBUSY) { | ||
| 219 | // A flip is still in flight; retry after its event. | 280 | // A flip is still in flight; retry after its event. | ||
| 220 | o.pending, o.flipStart = true, time.Now() | 281 | o.pending, o.flipStart = true, time.Now() | ||
A internal/adapters/drm/scanout.go +176 -0
| @@ -0,0 +1,176 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "os" | |
| 6 | + | "unsafe" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Direct scanout (ADR 006): when one fullscreen window covers the output | |
| 12 | + | // with a GPU buffer of the output's size, the output flips to that buffer | |
| 13 | + | // instead of compositing it. The buffer becomes a KMS framebuffer once | |
| 14 | + | // (PRIME import + ADDFB2) and is cached by DMABuf ID while the client | |
| 15 | + | // keeps using it. KMS waits on the buffer's implicit fences before showing | |
| 16 | + | // it. Anything else on screen, a scale, or a buffer KMS refuses falls back | |
| 17 | + | // to composition. | |
| 18 | + | ||
| 19 | + | const ( | |
| 20 | + | ioctlGemClose = 0x40086409 | |
| 21 | + | ioctlPrimeFDToHandle = 0xC00C642E | |
| 22 | + | ioctlAddFB2 = 0xC06864B8 | |
| 23 | + | ||
| 24 | + | fbModifiers = 1 << 1 | |
| 25 | + | modInvalid = 0x00ffffffffffffff | |
| 26 | + | scanoutIdleTTL = 240 // flips a cached framebuffer survives unused | |
| 27 | + | ) | |
| 28 | + | ||
| 29 | + | type primeHandle struct { | |
| 30 | + | handle, flags uint32 | |
| 31 | + | fd int32 | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | type gemClose struct{ handle, pad uint32 } | |
| 35 | + | ||
| 36 | + | type fbCmd2 struct { | |
| 37 | + | fbID, width, height, format, flags uint32 | |
| 38 | + | handles, pitches, offsets [4]uint32 | |
| 39 | + | modifiers [4]uint64 | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // scanoutCandidate returns the window content the output can scan out | |
| 43 | + | // directly, or a reason why it cannot. w, h are the output's physical size. | |
| 44 | + | func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) { | |
| 45 | + | var full *ports.SceneWindow | |
| 46 | + | for i := range s.Windows { | |
| 47 | + | win := &s.Windows[i] | |
| 48 | + | if win.Hidden || win.Rect.W <= 0 || win.Rect.H <= 0 { | |
| 49 | + | continue | |
| 50 | + | } | |
| 51 | + | covers := win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight | |
| 52 | + | if !win.Fullscreen || !covers || full != nil { | |
| 53 | + | return ports.SurfaceContent{}, "other_windows" | |
| 54 | + | } | |
| 55 | + | full = win | |
| 56 | + | } | |
| 57 | + | if full == nil { | |
| 58 | + | return ports.SurfaceContent{}, "no_fullscreen" | |
| 59 | + | } | |
| 60 | + | for _, l := range s.Layers { | |
| 61 | + | if l.Layer == ports.LayerOverlay && l.Rect.W > 0 && l.Rect.H > 0 { | |
| 62 | + | return ports.SurfaceContent{}, "overlay_surface" | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | c := surfaces[full.ID] | |
| 66 | + | switch { | |
| 67 | + | case c.DMABuf == nil: | |
| 68 | + | return c, "not_dmabuf" | |
| 69 | + | case len(c.Children) > 0: | |
| 70 | + | return c, "subsurfaces" | |
| 71 | + | case len(c.DMABuf.Planes) != 1: | |
| 72 | + | return c, "multi_plane" | |
| 73 | + | case c.Width != w || c.Height != h: | |
| 74 | + | return c, "size_mismatch" | |
| 75 | + | 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" | |
| 77 | + | } | |
| 78 | + | return c, "" | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // clientFB is a client buffer imported as a KMS framebuffer. | |
| 82 | + | type clientFB struct { | |
| 83 | + | fbID, handle uint32 | |
| 84 | + | last int // flip count when last shown | |
| 85 | + | failed bool | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // 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) { | |
| 91 | + | 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 | |
| 97 | + | } | |
| 98 | + | fb := &clientFB{last: o.flips} | |
| 99 | + | o.clientFBs[b.ID] = fb | |
| 100 | + | handle, err := o.importHandle(b.Planes[0].File) | |
| 101 | + | 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 | |
| 109 | + | } | |
| 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) | |
| 114 | + | } | |
| 115 | + | fb.fbID, fb.handle = cmd.fbID, handle | |
| 116 | + | return fb.fbID, nil | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | var errScanoutRefused = errors.New("buffer refused for scanout") | |
| 120 | + | ||
| 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() | |
| 126 | + | if err != nil { | |
| 127 | + | return 0, err | |
| 128 | + | } | |
| 129 | + | var p primeHandle | |
| 130 | + | var ioErr error | |
| 131 | + | if err := raw.Control(func(fd uintptr) { | |
| 132 | + | p.fd = int32(fd) | |
| 133 | + | ioErr = ioctl(o.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | |
| 134 | + | }); err != nil { | |
| 135 | + | return 0, err | |
| 136 | + | } | |
| 137 | + | if ioErr != nil { | |
| 138 | + | return 0, ioErr | |
| 139 | + | } | |
| 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 | |
| 147 | + | } | |
| 148 | + | delete(o.handles, handle) | |
| 149 | + | c := gemClose{handle: handle} | |
| 150 | + | _ = ioctl(o.fd, ioctlGemClose, unsafe.Pointer(&c)) | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // dropClientFBs frees framebuffers unused for scanoutIdleTTL flips, or all | |
| 154 | + | // of them, except those on screen or queued. | |
| 155 | + | func (o *Output) dropClientFBs(all bool) { | |
| 156 | + | for id, fb := range o.clientFBs { | |
| 157 | + | if id == o.shown || id == o.queued { | |
| 158 | + | continue | |
| 159 | + | } | |
| 160 | + | if !all && o.flips-fb.last < scanoutIdleTTL { | |
| 161 | + | continue | |
| 162 | + | } | |
| 163 | + | o.freeFB(fb) | |
| 164 | + | delete(o.clientFBs, id) | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | ||
| 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 | + | } | |
| @@ -0,0 +1,176 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "os" | |||
| 6 | + | "unsafe" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Direct scanout (ADR 006): when one fullscreen window covers the output | |||
| 12 | + | // with a GPU buffer of the output's size, the output flips to that buffer | |||
| 13 | + | // instead of compositing it. The buffer becomes a KMS framebuffer once | |||
| 14 | + | // (PRIME import + ADDFB2) and is cached by DMABuf ID while the client | |||
| 15 | + | // keeps using it. KMS waits on the buffer's implicit fences before showing | |||
| 16 | + | // it. Anything else on screen, a scale, or a buffer KMS refuses falls back | |||
| 17 | + | // to composition. | |||
| 18 | + | ||||
| 19 | + | const ( | |||
| 20 | + | ioctlGemClose = 0x40086409 | |||
| 21 | + | ioctlPrimeFDToHandle = 0xC00C642E | |||
| 22 | + | ioctlAddFB2 = 0xC06864B8 | |||
| 23 | + | ||||
| 24 | + | fbModifiers = 1 << 1 | |||
| 25 | + | modInvalid = 0x00ffffffffffffff | |||
| 26 | + | scanoutIdleTTL = 240 // flips a cached framebuffer survives unused | |||
| 27 | + | ) | |||
| 28 | + | ||||
| 29 | + | type primeHandle struct { | |||
| 30 | + | handle, flags uint32 | |||
| 31 | + | fd int32 | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | type gemClose struct{ handle, pad uint32 } | |||
| 35 | + | ||||
| 36 | + | type fbCmd2 struct { | |||
| 37 | + | fbID, width, height, format, flags uint32 | |||
| 38 | + | handles, pitches, offsets [4]uint32 | |||
| 39 | + | modifiers [4]uint64 | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // scanoutCandidate returns the window content the output can scan out | |||
| 43 | + | // directly, or a reason why it cannot. w, h are the output's physical size. | |||
| 44 | + | func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) { | |||
| 45 | + | var full *ports.SceneWindow | |||
| 46 | + | for i := range s.Windows { | |||
| 47 | + | win := &s.Windows[i] | |||
| 48 | + | if win.Hidden || win.Rect.W <= 0 || win.Rect.H <= 0 { | |||
| 49 | + | continue | |||
| 50 | + | } | |||
| 51 | + | covers := win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight | |||
| 52 | + | if !win.Fullscreen || !covers || full != nil { | |||
| 53 | + | return ports.SurfaceContent{}, "other_windows" | |||
| 54 | + | } | |||
| 55 | + | full = win | |||
| 56 | + | } | |||
| 57 | + | if full == nil { | |||
| 58 | + | return ports.SurfaceContent{}, "no_fullscreen" | |||
| 59 | + | } | |||
| 60 | + | for _, l := range s.Layers { | |||
| 61 | + | if l.Layer == ports.LayerOverlay && l.Rect.W > 0 && l.Rect.H > 0 { | |||
| 62 | + | return ports.SurfaceContent{}, "overlay_surface" | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | c := surfaces[full.ID] | |||
| 66 | + | switch { | |||
| 67 | + | case c.DMABuf == nil: | |||
| 68 | + | return c, "not_dmabuf" | |||
| 69 | + | case len(c.Children) > 0: | |||
| 70 | + | return c, "subsurfaces" | |||
| 71 | + | case len(c.DMABuf.Planes) != 1: | |||
| 72 | + | return c, "multi_plane" | |||
| 73 | + | case c.Width != w || c.Height != h: | |||
| 74 | + | return c, "size_mismatch" | |||
| 75 | + | 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" | |||
| 77 | + | } | |||
| 78 | + | return c, "" | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // clientFB is a client buffer imported as a KMS framebuffer. | |||
| 82 | + | type clientFB struct { | |||
| 83 | + | fbID, handle uint32 | |||
| 84 | + | last int // flip count when last shown | |||
| 85 | + | failed bool | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // 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) { | |||
| 91 | + | 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 | |||
| 97 | + | } | |||
| 98 | + | fb := &clientFB{last: o.flips} | |||
| 99 | + | o.clientFBs[b.ID] = fb | |||
| 100 | + | handle, err := o.importHandle(b.Planes[0].File) | |||
| 101 | + | 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 | |||
| 109 | + | } | |||
| 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) | |||
| 114 | + | } | |||
| 115 | + | fb.fbID, fb.handle = cmd.fbID, handle | |||
| 116 | + | return fb.fbID, nil | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | var errScanoutRefused = errors.New("buffer refused for scanout") | |||
| 120 | + | ||||
| 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() | |||
| 126 | + | if err != nil { | |||
| 127 | + | return 0, err | |||
| 128 | + | } | |||
| 129 | + | var p primeHandle | |||
| 130 | + | var ioErr error | |||
| 131 | + | if err := raw.Control(func(fd uintptr) { | |||
| 132 | + | p.fd = int32(fd) | |||
| 133 | + | ioErr = ioctl(o.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p)) | |||
| 134 | + | }); err != nil { | |||
| 135 | + | return 0, err | |||
| 136 | + | } | |||
| 137 | + | if ioErr != nil { | |||
| 138 | + | return 0, ioErr | |||
| 139 | + | } | |||
| 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 | |||
| 147 | + | } | |||
| 148 | + | delete(o.handles, handle) | |||
| 149 | + | c := gemClose{handle: handle} | |||
| 150 | + | _ = ioctl(o.fd, ioctlGemClose, unsafe.Pointer(&c)) | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // dropClientFBs frees framebuffers unused for scanoutIdleTTL flips, or all | |||
| 154 | + | // of them, except those on screen or queued. | |||
| 155 | + | func (o *Output) dropClientFBs(all bool) { | |||
| 156 | + | for id, fb := range o.clientFBs { | |||
| 157 | + | if id == o.shown || id == o.queued { | |||
| 158 | + | continue | |||
| 159 | + | } | |||
| 160 | + | if !all && o.flips-fb.last < scanoutIdleTTL { | |||
| 161 | + | continue | |||
| 162 | + | } | |||
| 163 | + | o.freeFB(fb) | |||
| 164 | + | delete(o.clientFBs, id) | |||
| 165 | + | } | |||
| 166 | + | } | |||
| 167 | + | ||||
| 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 | + | } | |||
A internal/adapters/drm/scanout_test.go +55 -0
| @@ -0,0 +1,55 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestScanoutCandidate(t *testing.T) { | |
| 10 | + | full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true} | |
| 11 | + | scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{full}} | |
| 12 | + | buf := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{}}} | |
| 13 | + | game := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, DMABuf: buf} | |
| 14 | + | for _, tc := range []struct { | |
| 15 | + | name string | |
| 16 | + | scene func(s *ports.Scene) | |
| 17 | + | cont func(c *ports.SurfaceContent) | |
| 18 | + | reason string | |
| 19 | + | }{ | |
| 20 | + | {"scanout", nil, nil, ""}, | |
| 21 | + | {"hidden others ignored", func(s *ports.Scene) { | |
| 22 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Hidden: true}) | |
| 23 | + | }, nil, ""}, | |
| 24 | + | {"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"}, | |
| 25 | + | {"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"}, | |
| 26 | + | {"float above", func(s *ports.Scene) { | |
| 27 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Rect: ports.Rect{W: 10, H: 10}}) | |
| 28 | + | }, nil, "other_windows"}, | |
| 29 | + | {"not covering", func(s *ports.Scene) { s.Windows[0].Rect.W = 90 }, nil, "other_windows"}, | |
| 30 | + | {"overlay layer", func(s *ports.Scene) { | |
| 31 | + | s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}} | |
| 32 | + | }, nil, "overlay_surface"}, | |
| 33 | + | {"top layer covered", func(s *ports.Scene) { | |
| 34 | + | s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerTop, Rect: ports.Rect{W: 10, H: 10}}} | |
| 35 | + | }, nil, ""}, | |
| 36 | + | {"shm", nil, func(c *ports.SurfaceContent) { c.DMABuf, c.SHM = nil, &ports.SHMBuffer{} }, "not_dmabuf"}, | |
| 37 | + | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | |
| 38 | + | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"}, | |
| 39 | + | {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"}, | |
| 40 | + | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | |
| 41 | + | } { | |
| 42 | + | s := scene | |
| 43 | + | s.Windows = append([]ports.SceneWindow(nil), scene.Windows...) | |
| 44 | + | c := game | |
| 45 | + | if tc.scene != nil { | |
| 46 | + | tc.scene(&s) | |
| 47 | + | } | |
| 48 | + | if tc.cont != nil { | |
| 49 | + | tc.cont(&c) | |
| 50 | + | } | |
| 51 | + | if _, reason := scanoutCandidate(s, map[ports.WindowID]ports.SurfaceContent{1: c}, 200, 100); reason != tc.reason { | |
| 52 | + | t.Errorf("%s: reason %q, want %q", tc.name, reason, tc.reason) | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | } | |
| @@ -0,0 +1,55 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestScanoutCandidate(t *testing.T) { | |||
| 10 | + | full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true} | |||
| 11 | + | scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{full}} | |||
| 12 | + | buf := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Planes: []ports.DMABufPlane{{}}} | |||
| 13 | + | game := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, DMABuf: buf} | |||
| 14 | + | for _, tc := range []struct { | |||
| 15 | + | name string | |||
| 16 | + | scene func(s *ports.Scene) | |||
| 17 | + | cont func(c *ports.SurfaceContent) | |||
| 18 | + | reason string | |||
| 19 | + | }{ | |||
| 20 | + | {"scanout", nil, nil, ""}, | |||
| 21 | + | {"hidden others ignored", func(s *ports.Scene) { | |||
| 22 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Hidden: true}) | |||
| 23 | + | }, nil, ""}, | |||
| 24 | + | {"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"}, | |||
| 25 | + | {"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"}, | |||
| 26 | + | {"float above", func(s *ports.Scene) { | |||
| 27 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Rect: ports.Rect{W: 10, H: 10}}) | |||
| 28 | + | }, nil, "other_windows"}, | |||
| 29 | + | {"not covering", func(s *ports.Scene) { s.Windows[0].Rect.W = 90 }, nil, "other_windows"}, | |||
| 30 | + | {"overlay layer", func(s *ports.Scene) { | |||
| 31 | + | s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}} | |||
| 32 | + | }, nil, "overlay_surface"}, | |||
| 33 | + | {"top layer covered", func(s *ports.Scene) { | |||
| 34 | + | s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerTop, Rect: ports.Rect{W: 10, H: 10}}} | |||
| 35 | + | }, nil, ""}, | |||
| 36 | + | {"shm", nil, func(c *ports.SurfaceContent) { c.DMABuf, c.SHM = nil, &ports.SHMBuffer{} }, "not_dmabuf"}, | |||
| 37 | + | {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"}, | |||
| 38 | + | {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, "size_mismatch"}, | |||
| 39 | + | {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"}, | |||
| 40 | + | {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""}, | |||
| 41 | + | } { | |||
| 42 | + | s := scene | |||
| 43 | + | s.Windows = append([]ports.SceneWindow(nil), scene.Windows...) | |||
| 44 | + | c := game | |||
| 45 | + | if tc.scene != nil { | |||
| 46 | + | tc.scene(&s) | |||
| 47 | + | } | |||
| 48 | + | if tc.cont != nil { | |||
| 49 | + | tc.cont(&c) | |||
| 50 | + | } | |||
| 51 | + | if _, reason := scanoutCandidate(s, map[ports.WindowID]ports.SurfaceContent{1: c}, 200, 100); reason != tc.reason { | |||
| 52 | + | t.Errorf("%s: reason %q, want %q", tc.name, reason, tc.reason) | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | } | |||
M internal/adapters/wayland/frames.go +7 -2
| @@ -25,13 +25,14 @@ func (s *Server) pace(ctx context.Context) { | |||
| 25 | 25 | presented := s.channels.Presented | |
| 26 | 26 | for { | |
| 27 | 27 | flipped := map[string]bool{} | |
| 28 | + | scanned := map[string]uint64{} | |
| 28 | 29 | select { | |
| 29 | 30 | case <-ctx.Done(): | |
| 30 | 31 | return | |
| 31 | 32 | case <-s.display.Stopped(): | |
| 32 | 33 | return | |
| 33 | 34 | case p := <-presented: | |
| 34 | - | flipped[p.Output] = true | |
| 35 | + | flipped[p.Output], scanned[p.Output] = true, p.Scanout | |
| 35 | 36 | case <-s.frameReady: | |
| 36 | 37 | case <-timer.C: | |
| 37 | 38 | } | |
| @@ -41,7 +42,7 @@ func (s *Server) pace(ctx context.Context) { | |||
| 41 | 42 | for { | |
| 42 | 43 | select { | |
| 43 | 44 | case p := <-presented: | |
| 44 | - | flipped[p.Output] = true | |
| 45 | + | flipped[p.Output], scanned[p.Output] = true, p.Scanout | |
| 45 | 46 | default: | |
| 46 | 47 | break drain | |
| 47 | 48 | } | |
| @@ -54,6 +55,10 @@ func (s *Server) pace(ctx context.Context) { | |||
| 54 | 55 | for _, name := range due { | |
| 55 | 56 | s.sendFrames(name) | |
| 56 | 57 | } | |
| 58 | + | if s.releaseHeld(time.Now(), scanned) && idle { | |
| 59 | + | // Held buffers wait for a flip or heldTimeout. | |
| 60 | + | wait, idle = heldTimeout, false | |
| 61 | + | } | |
| 57 | 62 | } | |
| 58 | 63 | }) { | |
| 59 | 64 | return | |
| @@ -25,13 +25,14 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 25 | presented := s.channels.Presented | 25 | presented := s.channels.Presented | ||
| 26 | for { | 26 | for { | ||
| 27 | flipped := map[string]bool{} | 27 | flipped := map[string]bool{} | ||
| 28 | + | scanned := map[string]uint64{} | |||
| 28 | select { | 29 | select { | ||
| 29 | case <-ctx.Done(): | 30 | case <-ctx.Done(): | ||
| 30 | return | 31 | return | ||
| 31 | case <-s.display.Stopped(): | 32 | case <-s.display.Stopped(): | ||
| 32 | return | 33 | return | ||
| 33 | case p := <-presented: | 34 | case p := <-presented: | ||
| 34 | - | flipped[p.Output] = true | 35 | + | flipped[p.Output], scanned[p.Output] = true, p.Scanout |
| 35 | case <-s.frameReady: | 36 | case <-s.frameReady: | ||
| 36 | case <-timer.C: | 37 | case <-timer.C: | ||
| 37 | } | 38 | } | ||
| @@ -41,7 +42,7 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 41 | for { | 42 | for { | ||
| 42 | select { | 43 | select { | ||
| 43 | case p := <-presented: | 44 | case p := <-presented: | ||
| 44 | - | flipped[p.Output] = true | 45 | + | flipped[p.Output], scanned[p.Output] = true, p.Scanout |
| 45 | default: | 46 | default: | ||
| 46 | break drain | 47 | break drain | ||
| 47 | } | 48 | } | ||
| @@ -54,6 +55,10 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 54 | for _, name := range due { | 55 | for _, name := range due { | ||
| 55 | s.sendFrames(name) | 56 | s.sendFrames(name) | ||
| 56 | } | 57 | } | ||
| 58 | + | if s.releaseHeld(time.Now(), scanned) && idle { | |||
| 59 | + | // Held buffers wait for a flip or heldTimeout. | |||
| 60 | + | wait, idle = heldTimeout, false | |||
| 61 | + | } | |||
| 57 | } | 62 | } | ||
| 58 | }) { | 63 | }) { | ||
| 59 | return | 64 | return | ||
A internal/adapters/wayland/release.go +100 -0
| @@ -0,0 +1,100 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "time" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 7 | + | ) | |
| 8 | + | ||
| 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). | |
| 17 | + | ||
| 18 | + | // heldTimeout releases held buffers of an output that stopped flipping; a | |
| 19 | + | // queued flip completes well within it. | |
| 20 | + | const heldTimeout = 100 * time.Millisecond | |
| 21 | + | ||
| 22 | + | type heldBuffer struct { | |
| 23 | + | res *wayland.Buffer | |
| 24 | + | id uint64 | |
| 25 | + | at time.Time | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // releaseBuffer releases a replaced buffer, or holds a GPU buffer until | |
| 29 | + | // its output flipped away from it. | |
| 30 | + | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |
| 31 | + | if !b.Resource.Alive() { | |
| 32 | + | return | |
| 33 | + | } | |
| 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 { | |
| 39 | + | b.SendRelease() | |
| 40 | + | return | |
| 41 | + | } | |
| 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 | + | select { | |
| 45 | + | case s.frameReady <- struct{}{}: | |
| 46 | + | default: | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 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 | + | } | |
| 60 | + | } | |
| 61 | + | live := map[string]bool{} | |
| 62 | + | for _, o := range s.outputs { | |
| 63 | + | live[o.name()] = true | |
| 64 | + | } | |
| 65 | + | for name := range s.scanned { | |
| 66 | + | if !live[name] { | |
| 67 | + | delete(s.scanned, name) | |
| 68 | + | } | |
| 69 | + | } | |
| 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 | + | } | |
| 81 | + | } | |
| 82 | + | if len(kept) == 0 { | |
| 83 | + | delete(s.held, name) | |
| 84 | + | } else { | |
| 85 | + | s.held[name] = kept | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | return len(s.held) > 0 | |
| 89 | + | } | |
| 90 | + | ||
| 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 { | |
| 97 | + | return true | |
| 98 | + | } | |
| 99 | + | return live && !flipped && now.Sub(h.at) < heldTimeout | |
| 100 | + | } | |
| @@ -0,0 +1,100 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "time" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 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). | |||
| 17 | + | ||||
| 18 | + | // heldTimeout releases held buffers of an output that stopped flipping; a | |||
| 19 | + | // queued flip completes well within it. | |||
| 20 | + | const heldTimeout = 100 * time.Millisecond | |||
| 21 | + | ||||
| 22 | + | type heldBuffer struct { | |||
| 23 | + | res *wayland.Buffer | |||
| 24 | + | id uint64 | |||
| 25 | + | at time.Time | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // releaseBuffer releases a replaced buffer, or holds a GPU buffer until | |||
| 29 | + | // its output flipped away from it. | |||
| 30 | + | func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |||
| 31 | + | if !b.Resource.Alive() { | |||
| 32 | + | return | |||
| 33 | + | } | |||
| 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 { | |||
| 39 | + | b.SendRelease() | |||
| 40 | + | return | |||
| 41 | + | } | |||
| 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 | + | select { | |||
| 45 | + | case s.frameReady <- struct{}{}: | |||
| 46 | + | default: | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 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 | + | } | |||
| 60 | + | } | |||
| 61 | + | live := map[string]bool{} | |||
| 62 | + | for _, o := range s.outputs { | |||
| 63 | + | live[o.name()] = true | |||
| 64 | + | } | |||
| 65 | + | for name := range s.scanned { | |||
| 66 | + | if !live[name] { | |||
| 67 | + | delete(s.scanned, name) | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 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 | + | } | |||
| 81 | + | } | |||
| 82 | + | if len(kept) == 0 { | |||
| 83 | + | delete(s.held, name) | |||
| 84 | + | } else { | |||
| 85 | + | s.held[name] = kept | |||
| 86 | + | } | |||
| 87 | + | } | |||
| 88 | + | return len(s.held) > 0 | |||
| 89 | + | } | |||
| 90 | + | ||||
| 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 { | |||
| 97 | + | return true | |||
| 98 | + | } | |||
| 99 | + | return live && !flipped && now.Sub(h.at) < heldTimeout | |||
| 100 | + | } | |||
A internal/adapters/wayland/release_test.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestKeepHeld(t *testing.T) { | |
| 9 | + | now := time.Unix(100, 0) | |
| 10 | + | h := heldBuffer{id: 7, at: now} | |
| 11 | + | for _, tc := range []struct { | |
| 12 | + | name string | |
| 13 | + | scanned uint64 | |
| 14 | + | live, flipped, want bool | |
| 15 | + | after time.Duration | |
| 16 | + | }{ | |
| 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 | + | } { | |
| 24 | + | if got := keepHeld(h, tc.scanned, tc.live, tc.flipped, now.Add(tc.after)); got != tc.want { | |
| 25 | + | t.Errorf("%s: %v", tc.name, got) | |
| 26 | + | } | |
| 27 | + | } | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestKeepHeld(t *testing.T) { | |||
| 9 | + | now := time.Unix(100, 0) | |||
| 10 | + | h := heldBuffer{id: 7, at: now} | |||
| 11 | + | for _, tc := range []struct { | |||
| 12 | + | name string | |||
| 13 | + | scanned uint64 | |||
| 14 | + | live, flipped, want bool | |||
| 15 | + | after time.Duration | |||
| 16 | + | }{ | |||
| 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 | + | } { | |||
| 24 | + | if got := keepHeld(h, tc.scanned, tc.live, tc.flipped, now.Add(tc.after)); got != tc.want { | |||
| 25 | + | t.Errorf("%s: %v", tc.name, got) | |||
| 26 | + | } | |||
| 27 | + | } | |||
| 28 | + | } | |||
M internal/adapters/wayland/server.go +11 -7
| @@ -52,12 +52,16 @@ type Server struct { | |||
| 52 | 52 | awaiting map[string][]*wayland.Callback | |
| 53 | 53 | frameDue map[string]time.Time | |
| 54 | 54 | frameReady chan struct{} | |
| 55 | - | frames uint64 | |
| 56 | - | started time.Time | |
| 57 | - | surfaces map[*server.Resource]*surface | |
| 58 | - | buffers map[*server.Resource]clientBuffer | |
| 59 | - | dmabuf *dmabufGlobal | |
| 60 | - | serial uint32 | |
| 55 | + | // held buffers wait 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 | |
| 59 | + | frames uint64 | |
| 60 | + | started time.Time | |
| 61 | + | surfaces map[*server.Resource]*surface | |
| 62 | + | buffers map[*server.Resource]clientBuffer | |
| 63 | + | dmabuf *dmabufGlobal | |
| 64 | + | serial uint32 | |
| 61 | 65 | // press is the serial of the last button or key press, sent to | |
| 62 | 66 | // pressClient: popup grabs must come from it. | |
| 63 | 67 | press uint32 | |
| @@ -143,7 +147,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 143 | 147 | d.Close() | |
| 144 | 148 | return nil, err | |
| 145 | 149 | } | |
| 146 | - | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 150 | + | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), 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} | |
| 147 | 151 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | |
| 148 | 152 | if opts.Keymap != "" { | |
| 149 | 153 | fd, size, e := keymapFile(opts.Keymap) | |
| @@ -52,12 +52,16 @@ type Server struct { | |||||
| 52 | awaiting map[string][]*wayland.Callback | 52 | awaiting map[string][]*wayland.Callback | ||
| 53 | frameDue map[string]time.Time | 53 | frameDue map[string]time.Time | ||
| 54 | frameReady chan struct{} | 54 | frameReady chan struct{} | ||
| 55 | - | frames uint64 | 55 | + | // held buffers wait for a flip (release.go); scanned is the dmabuf ID |
| 56 | - | started time.Time | 56 | + | // each output scans out directly. |
| 57 | - | surfaces map[*server.Resource]*surface | 57 | + | held map[string][]heldBuffer |
| 58 | - | buffers map[*server.Resource]clientBuffer | 58 | + | scanned map[string]uint64 |
| 59 | - | dmabuf *dmabufGlobal | 59 | + | frames uint64 |
| 60 | - | serial uint32 | 60 | + | started time.Time |
| 61 | + | surfaces map[*server.Resource]*surface | |||
| 62 | + | buffers map[*server.Resource]clientBuffer | |||
| 63 | + | dmabuf *dmabufGlobal | |||
| 64 | + | serial uint32 | |||
| 61 | // press is the serial of the last button or key press, sent to | 65 | // press is the serial of the last button or key press, sent to | ||
| 62 | // pressClient: popup grabs must come from it. | 66 | // pressClient: popup grabs must come from it. | ||
| 63 | press uint32 | 67 | press uint32 | ||
| @@ -143,7 +147,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 143 | d.Close() | 147 | d.Close() | ||
| 144 | return nil, err | 148 | return nil, err | ||
| 145 | } | 149 | } | ||
| 146 | - | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | 150 | + | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), 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} |
| 147 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | 151 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | ||
| 148 | if opts.Keymap != "" { | 152 | if opts.Keymap != "" { | ||
| 149 | fd, size, e := keymapFile(opts.Keymap) | 153 | fd, size, e := keymapFile(opts.Keymap) | ||
M internal/adapters/wayland/surface.go +3 -5
| @@ -150,8 +150,8 @@ func (s *surface) Destroy(*wayland.Surface) { | |||
| 150 | 150 | } | |
| 151 | 151 | s.callbacks = nil | |
| 152 | 152 | // The client may reuse the buffer on another surface. | |
| 153 | - | if s.current != nil && s.current.Resource.Alive() { | |
| 154 | - | s.current.SendRelease() | |
| 153 | + | if s.current != nil { | |
| 154 | + | s.server.releaseBuffer(s, s.current) | |
| 155 | 155 | } | |
| 156 | 156 | s.current, s.pending = nil, nil | |
| 157 | 157 | if s.role != nil { | |
| @@ -189,9 +189,7 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 189 | 189 | } | |
| 190 | 190 | if s.attached { | |
| 191 | 191 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | |
| 192 | - | if s.current.Resource.Alive() { | |
| 193 | - | s.current.SendRelease() | |
| 194 | - | } | |
| 192 | + | s.server.releaseBuffer(s, s.current) | |
| 195 | 193 | } | |
| 196 | 194 | s.current = s.pending | |
| 197 | 195 | s.pending = nil | |
| @@ -150,8 +150,8 @@ func (s *surface) Destroy(*wayland.Surface) { | |||||
| 150 | } | 150 | } | ||
| 151 | s.callbacks = nil | 151 | s.callbacks = nil | ||
| 152 | // The client may reuse the buffer on another surface. | 152 | // The client may reuse the buffer on another surface. | ||
| 153 | - | if s.current != nil && s.current.Resource.Alive() { | 153 | + | if s.current != nil { |
| 154 | - | s.current.SendRelease() | 154 | + | s.server.releaseBuffer(s, s.current) |
| 155 | } | 155 | } | ||
| 156 | s.current, s.pending = nil, nil | 156 | s.current, s.pending = nil, nil | ||
| 157 | if s.role != nil { | 157 | if s.role != nil { | ||
| @@ -189,9 +189,7 @@ func (s *surface) Commit(*wayland.Surface) { | |||||
| 189 | } | 189 | } | ||
| 190 | if s.attached { | 190 | if s.attached { | ||
| 191 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | 191 | if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) { | ||
| 192 | - | if s.current.Resource.Alive() { | 192 | + | s.server.releaseBuffer(s, s.current) |
| 193 | - | s.current.SendRelease() | |||
| 194 | - | } | |||
| 195 | } | 193 | } | ||
| 196 | s.current = s.pending | 194 | s.current = s.pending | ||
| 197 | s.pending = nil | 195 | s.pending = nil | ||
M internal/app/app.go +2 -2
| @@ -64,7 +64,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 64 | 64 | var hw *drmBackend | |
| 65 | 65 | if opts.Backend == "drm" { | |
| 66 | 66 | var err error | |
| 67 | - | if hw, err = openDRM(ctx, opts.Config.Outputs); err != nil { | |
| 67 | + | if hw, err = openDRM(ctx, opts.Config.Outputs, opts.Config.Render.DirectScanout); err != nil { | |
| 68 | 68 | return err | |
| 69 | 69 | } | |
| 70 | 70 | defer hw.close() | |
| @@ -208,7 +208,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 208 | 208 | } | |
| 209 | 209 | if hw != nil { | |
| 210 | 210 | done <- safe("output", func() error { | |
| 211 | - | want := func() drm.Want { return wantFromConfig(opts.Config.Outputs) } | |
| 211 | + | want := func() drm.Want { return wantFromConfig(opts.Config.Outputs, opts.Config.Render.DirectScanout) } | |
| 212 | 212 | return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm")) | |
| 213 | 213 | }) | |
| 214 | 214 | return | |
| @@ -64,7 +64,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 64 | var hw *drmBackend | 64 | var hw *drmBackend | ||
| 65 | if opts.Backend == "drm" { | 65 | if opts.Backend == "drm" { | ||
| 66 | var err error | 66 | var err error | ||
| 67 | - | if hw, err = openDRM(ctx, opts.Config.Outputs); err != nil { | 67 | + | if hw, err = openDRM(ctx, opts.Config.Outputs, opts.Config.Render.DirectScanout); err != nil { |
| 68 | return err | 68 | return err | ||
| 69 | } | 69 | } | ||
| 70 | defer hw.close() | 70 | defer hw.close() | ||
| @@ -208,7 +208,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 208 | } | 208 | } | ||
| 209 | if hw != nil { | 209 | if hw != nil { | ||
| 210 | done <- safe("output", func() error { | 210 | done <- safe("output", func() error { | ||
| 211 | - | want := func() drm.Want { return wantFromConfig(opts.Config.Outputs) } | 211 | + | want := func() drm.Want { return wantFromConfig(opts.Config.Outputs, opts.Config.Render.DirectScanout) } |
| 212 | return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm")) | 212 | return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm")) | ||
| 213 | }) | 213 | }) | ||
| 214 | return | 214 | return | ||
M internal/app/drm.go +4 -4
| @@ -42,14 +42,14 @@ func safe(name string, fn func() error) (err error) { | |||
| 42 | 42 | ||
| 43 | 43 | // openDRM opens the seat and every card with a connected display. Outputs | |
| 44 | 44 | // are found by the first Scan of each card. | |
| 45 | - | func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, error) { | |
| 45 | + | func openDRM(ctx context.Context, outputs []ports.OutputConfig, scanout bool) (*drmBackend, error) { | |
| 46 | 46 | log := logging.For(ctx, "drm") | |
| 47 | 47 | s, err := seat.Open(ctx, logging.For(ctx, "seat")) | |
| 48 | 48 | if err != nil { | |
| 49 | 49 | log.Error().Err(err).Msg("open seat") | |
| 50 | 50 | return nil, fmt.Errorf("open seat: %w", err) | |
| 51 | 51 | } | |
| 52 | - | want := wantFromConfig(outputs) | |
| 52 | + | want := wantFromConfig(outputs, scanout) | |
| 53 | 53 | paths, _ := filepath.Glob("/dev/dri/card[0-9]*") | |
| 54 | 54 | sort.Strings(paths) | |
| 55 | 55 | b := &drmBackend{seat: s} | |
| @@ -79,8 +79,8 @@ func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, er | |||
| 79 | 79 | } | |
| 80 | 80 | ||
| 81 | 81 | // wantFromConfig turns output.<name> entries into connector choices. | |
| 82 | - | func wantFromConfig(outputs []ports.OutputConfig) drm.Want { | |
| 83 | - | want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}} | |
| 82 | + | func wantFromConfig(outputs []ports.OutputConfig, scanout bool) drm.Want { | |
| 83 | + | want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}, NoScanout: !scanout} | |
| 84 | 84 | for _, o := range outputs { | |
| 85 | 85 | if o.Off { | |
| 86 | 86 | want.Disabled[o.Name] = true | |
| @@ -42,14 +42,14 @@ func safe(name string, fn func() error) (err error) { | |||||
| 42 | 42 | ||||
| 43 | // openDRM opens the seat and every card with a connected display. Outputs | 43 | // openDRM opens the seat and every card with a connected display. Outputs | ||
| 44 | // are found by the first Scan of each card. | 44 | // are found by the first Scan of each card. | ||
| 45 | - | func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, error) { | 45 | + | func openDRM(ctx context.Context, outputs []ports.OutputConfig, scanout bool) (*drmBackend, error) { |
| 46 | log := logging.For(ctx, "drm") | 46 | log := logging.For(ctx, "drm") | ||
| 47 | s, err := seat.Open(ctx, logging.For(ctx, "seat")) | 47 | s, err := seat.Open(ctx, logging.For(ctx, "seat")) | ||
| 48 | if err != nil { | 48 | if err != nil { | ||
| 49 | log.Error().Err(err).Msg("open seat") | 49 | log.Error().Err(err).Msg("open seat") | ||
| 50 | return nil, fmt.Errorf("open seat: %w", err) | 50 | return nil, fmt.Errorf("open seat: %w", err) | ||
| 51 | } | 51 | } | ||
| 52 | - | want := wantFromConfig(outputs) | 52 | + | want := wantFromConfig(outputs, scanout) |
| 53 | paths, _ := filepath.Glob("/dev/dri/card[0-9]*") | 53 | paths, _ := filepath.Glob("/dev/dri/card[0-9]*") | ||
| 54 | sort.Strings(paths) | 54 | sort.Strings(paths) | ||
| 55 | b := &drmBackend{seat: s} | 55 | b := &drmBackend{seat: s} | ||
| @@ -79,8 +79,8 @@ func openDRM(ctx context.Context, outputs []ports.OutputConfig) (*drmBackend, er | |||||
| 79 | } | 79 | } | ||
| 80 | 80 | ||||
| 81 | // wantFromConfig turns output.<name> entries into connector choices. | 81 | // wantFromConfig turns output.<name> entries into connector choices. | ||
| 82 | - | func wantFromConfig(outputs []ports.OutputConfig) drm.Want { | 82 | + | func wantFromConfig(outputs []ports.OutputConfig, scanout bool) drm.Want { |
| 83 | - | want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}} | 83 | + | want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}, NoScanout: !scanout} |
| 84 | for _, o := range outputs { | 84 | for _, o := range outputs { | ||
| 85 | if o.Off { | 85 | if o.Off { | ||
| 86 | want.Disabled[o.Name] = true | 86 | want.Disabled[o.Name] = true | ||
M internal/app/drm_test.go +1 -1
| @@ -7,7 +7,7 @@ import ( | |||
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | 9 | func TestWantFromConfig(t *testing.T) { | |
| 10 | - | want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}) | |
| 10 | + | want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}, true) | |
| 11 | 11 | if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 { | |
| 12 | 12 | t.Fatalf("%+v", want) | |
| 13 | 13 | } | |
| @@ -7,7 +7,7 @@ import ( | |||||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | func TestWantFromConfig(t *testing.T) { | 9 | func TestWantFromConfig(t *testing.T) { | ||
| 10 | - | want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}) | 10 | + | want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}, true) |
| 11 | if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 { | 11 | if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 { | ||
| 12 | t.Fatalf("%+v", want) | 12 | t.Fatalf("%+v", want) | ||
| 13 | } | 13 | } | ||
M internal/ports/messages.go +6 -1
| @@ -280,7 +280,12 @@ func (OutputAdded) outputEvent() {} | |||
| 280 | 280 | ||
| 281 | 281 | // OutputPresented carries output → wayland that a frame reached the screen | |
| 282 | 282 | // (a page flip completed): frame callbacks of the surfaces on it are due. | |
| 283 | - | type OutputPresented struct{ Output string } | |
| 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. | |
| 285 | + | type OutputPresented struct { | |
| 286 | + | Output string | |
| 287 | + | Scanout uint64 | |
| 288 | + | } | |
| 284 | 289 | ||
| 285 | 290 | // OutputRemoved carries output → core an unplugged display. | |
| 286 | 291 | type OutputRemoved struct{ Name string } | |
| @@ -280,7 +280,12 @@ func (OutputAdded) outputEvent() {} | |||||
| 280 | 280 | ||||
| 281 | // OutputPresented carries output → wayland that a frame reached the screen | 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. | 282 | // (a page flip completed): frame callbacks of the surfaces on it are due. | ||
| 283 | - | type OutputPresented struct{ Output string } | 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. | |||
| 285 | + | type OutputPresented struct { | |||
| 286 | + | Output string | |||
| 287 | + | Scanout uint64 | |||
| 288 | + | } | |||
| 284 | 289 | ||||
| 285 | // OutputRemoved carries output → core an unplugged display. | 290 | // OutputRemoved carries output → core an unplugged display. | ||
| 286 | type OutputRemoved struct{ Name string } | 291 | type OutputRemoved struct{ Name string } | ||