fix(presentation): flag zero-copy only for the window shown without composition
- commit
- 7e8d74490cb4e3d29c7d6e4dd83cb14aa1fd7096
- author
- brice · · 2026-09-26 12:38 UTC
- parent
- 9fd9758
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +23 -19
M internal/adapters/drm/output.go +7 -6
| @@ -91,10 +91,11 @@ type Output struct { | |||
| 91 | 91 | ||
| 92 | 92 | // pendingFrame is what the pending frame commit shows. | |
| 93 | 93 | type pendingFrame struct { | |
| 94 | - | frame bool | |
| 95 | - | queued uint64 | |
| 96 | - | zeroCopy, async bool | |
| 97 | - | shows map[ports.WindowID]uint64 | |
| 94 | + | frame bool | |
| 95 | + | queued uint64 | |
| 96 | + | zeroCopy ports.WindowID // window shown without composition | |
| 97 | + | async bool | |
| 98 | + | shows map[ports.WindowID]uint64 | |
| 98 | 99 | } | |
| 99 | 100 | ||
| 100 | 101 | // Commit event userData: the commit serial above userKindBits, the kind | |
| @@ -561,7 +562,7 @@ func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame | |||
| 561 | 562 | // flip from the composed image into scanout waits for vblank. | |
| 562 | 563 | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | |
| 563 | 564 | vrr := o.wantVRR(true) | |
| 564 | - | f.queued, f.zeroCopy = c.DMABuf.ID, true | |
| 565 | + | f.queued, f.zeroCopy = c.DMABuf.ID, c.ID | |
| 565 | 566 | // Explicit sync: KMS waits on the client's acquire fence. Wayland may | |
| 566 | 567 | // close its file at any time: commit a duplicate taken while it is | |
| 567 | 568 | // held open (the kernel keeps its own reference). | |
| @@ -847,7 +848,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 847 | 848 | } | |
| 848 | 849 | // The overlay buffer is on screen like a scanned-out one: it | |
| 849 | 850 | // is reported shown, so it is not released under the plane. | |
| 850 | - | f.queued, f.zeroCopy = ov.buf, ov.buf != 0 | |
| 851 | + | f.queued, f.zeroCopy = ov.buf, ov.id | |
| 851 | 852 | err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov) | |
| 852 | 853 | if done != nil { | |
| 853 | 854 | done.Close() | |
| @@ -91,10 +91,11 @@ type Output struct { | |||||
| 91 | 91 | ||||
| 92 | // pendingFrame is what the pending frame commit shows. | 92 | // pendingFrame is what the pending frame commit shows. | ||
| 93 | type pendingFrame struct { | 93 | type pendingFrame struct { | ||
| 94 | - | frame bool | 94 | + | frame bool |
| 95 | - | queued uint64 | 95 | + | queued uint64 |
| 96 | - | zeroCopy, async bool | 96 | + | zeroCopy ports.WindowID // window shown without composition |
| 97 | - | shows map[ports.WindowID]uint64 | 97 | + | async bool |
| 98 | + | shows map[ports.WindowID]uint64 | |||
| 98 | } | 99 | } | ||
| 99 | 100 | ||||
| 100 | // Commit event userData: the commit serial above userKindBits, the kind | 101 | // Commit event userData: the commit serial above userKindBits, the kind | ||
| @@ -561,7 +562,7 @@ func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame | |||||
| 561 | // flip from the composed image into scanout waits for vblank. | 562 | // flip from the composed image into scanout waits for vblank. | ||
| 562 | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | 563 | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | ||
| 563 | vrr := o.wantVRR(true) | 564 | vrr := o.wantVRR(true) | ||
| 564 | - | f.queued, f.zeroCopy = c.DMABuf.ID, true | 565 | + | f.queued, f.zeroCopy = c.DMABuf.ID, c.ID |
| 565 | // Explicit sync: KMS waits on the client's acquire fence. Wayland may | 566 | // Explicit sync: KMS waits on the client's acquire fence. Wayland may | ||
| 566 | // close its file at any time: commit a duplicate taken while it is | 567 | // close its file at any time: commit a duplicate taken while it is | ||
| 567 | // held open (the kernel keeps its own reference). | 568 | // held open (the kernel keeps its own reference). | ||
| @@ -847,7 +848,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 847 | } | 848 | } | ||
| 848 | // The overlay buffer is on screen like a scanned-out one: it | 849 | // The overlay buffer is on screen like a scanned-out one: it | ||
| 849 | // is reported shown, so it is not released under the plane. | 850 | // is reported shown, so it is not released under the plane. | ||
| 850 | - | f.queued, f.zeroCopy = ov.buf, ov.buf != 0 | 851 | + | f.queued, f.zeroCopy = ov.buf, ov.id |
| 851 | err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov) | 852 | err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov) | ||
| 852 | if done != nil { | 853 | if done != nil { | ||
| 853 | done.Close() | 854 | done.Close() | ||
M internal/adapters/wayland/presentation.go +1 -1
| @@ -161,7 +161,7 @@ func (s *Server) sendPresented(w feedbackWait, o *output, f *ports.FlipInfo, exa | |||
| 161 | 161 | if f.HardwareClock { | |
| 162 | 162 | flags |= uint32(presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion) | |
| 163 | 163 | } | |
| 164 | - | if f.ZeroCopy { | |
| 164 | + | if f.ZeroCopy != 0 && f.ZeroCopy == w.win { | |
| 165 | 165 | flags |= uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) | |
| 166 | 166 | } | |
| 167 | 167 | sec, nsec := uint64(f.When/time.Second), uint32(f.When%time.Second) | |
| @@ -161,7 +161,7 @@ func (s *Server) sendPresented(w feedbackWait, o *output, f *ports.FlipInfo, exa | |||||
| 161 | if f.HardwareClock { | 161 | if f.HardwareClock { | ||
| 162 | flags |= uint32(presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion) | 162 | flags |= uint32(presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion) | ||
| 163 | } | 163 | } | ||
| 164 | - | if f.ZeroCopy { | 164 | + | if f.ZeroCopy != 0 && f.ZeroCopy == w.win { |
| 165 | flags |= uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) | 165 | flags |= uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) | ||
| 166 | } | 166 | } | ||
| 167 | sec, nsec := uint64(f.When/time.Second), uint32(f.When%time.Second) | 167 | sec, nsec := uint64(f.When/time.Second), uint32(f.When%time.Second) | ||
M internal/adapters/wayland/presentation_test.go +8 -8
| @@ -155,14 +155,14 @@ func TestPresentationFeedback(t *testing.T) { | |||
| 155 | 155 | t.Fatal(err) | |
| 156 | 156 | } | |
| 157 | 157 | } | |
| 158 | - | flip := func(when time.Duration, seq uint64, shows uint64, zero bool) { | |
| 158 | + | flip := func(when time.Duration, seq uint64, shows uint64, zero ports.WindowID) { | |
| 159 | 159 | presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}} | |
| 160 | 160 | } | |
| 161 | 161 | // Presented with the flip's time, counter and flags. | |
| 162 | 162 | a := feedback() | |
| 163 | 163 | commit() | |
| 164 | 164 | seqA := lastSeq() | |
| 165 | - | flip(5*time.Second+7, 42, seqA, true) | |
| 165 | + | flip(5*time.Second+7, 42, seqA, w.ID) | |
| 166 | 166 | ev := answer(a, "presented") | |
| 167 | 167 | if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 { | |
| 168 | 168 | t.Fatalf("presented %+v", ev) | |
| @@ -178,9 +178,9 @@ func TestPresentationFeedback(t *testing.T) { | |||
| 178 | 178 | d := feedback() | |
| 179 | 179 | commit() | |
| 180 | 180 | seqD := lastSeq() | |
| 181 | - | flip(6*time.Second, 43, seqB, false) | |
| 182 | - | flip(7*time.Second, 44, seqD, false) | |
| 183 | - | if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 { | |
| 181 | + | flip(6*time.Second, 43, seqB, w.ID+1) // another window on the overlay | |
| 182 | + | flip(7*time.Second, 44, seqD, 0) | |
| 183 | + | if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 || ev.flags&uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) != 0 { | |
| 184 | 184 | t.Fatalf("first %+v", ev) | |
| 185 | 185 | } | |
| 186 | 186 | if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 { | |
| @@ -192,7 +192,7 @@ func TestPresentationFeedback(t *testing.T) { | |||
| 192 | 192 | lastSeq() | |
| 193 | 193 | commit() | |
| 194 | 194 | seqNew := lastSeq() | |
| 195 | - | flip(8*time.Second, 45, seqNew, false) | |
| 195 | + | flip(8*time.Second, 45, seqNew, 0) | |
| 196 | 196 | if ev := answer(old, "replaced"); !ev.discarded { | |
| 197 | 197 | t.Fatalf("replaced: %+v", ev) | |
| 198 | 198 | } | |
| @@ -213,7 +213,7 @@ func TestPresentationFeedback(t *testing.T) { | |||
| 213 | 213 | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 214 | 214 | commit() // the parent applies the subsurface | |
| 215 | 215 | seqC := lastSeq() | |
| 216 | - | flip(8500*time.Millisecond, 50, seqC, false) | |
| 216 | + | flip(8500*time.Millisecond, 50, seqC, 0) | |
| 217 | 217 | if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 { | |
| 218 | 218 | t.Fatalf("subsurface %+v", ev) | |
| 219 | 219 | } | |
| @@ -222,7 +222,7 @@ func TestPresentationFeedback(t *testing.T) { | |||
| 222 | 222 | commit() | |
| 223 | 223 | lastSeq() | |
| 224 | 224 | requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy) | |
| 225 | - | flip(9*time.Second, 46, 0, false) | |
| 225 | + | flip(9*time.Second, 46, 0, 0) | |
| 226 | 226 | if ev := answer(gone, "destroyed"); !ev.discarded { | |
| 227 | 227 | t.Fatalf("destroyed: %+v", ev) | |
| 228 | 228 | } | |
| @@ -155,14 +155,14 @@ func TestPresentationFeedback(t *testing.T) { | |||||
| 155 | t.Fatal(err) | 155 | t.Fatal(err) | ||
| 156 | } | 156 | } | ||
| 157 | } | 157 | } | ||
| 158 | - | flip := func(when time.Duration, seq uint64, shows uint64, zero bool) { | 158 | + | flip := func(when time.Duration, seq uint64, shows uint64, zero ports.WindowID) { |
| 159 | presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}} | 159 | presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}} | ||
| 160 | } | 160 | } | ||
| 161 | // Presented with the flip's time, counter and flags. | 161 | // Presented with the flip's time, counter and flags. | ||
| 162 | a := feedback() | 162 | a := feedback() | ||
| 163 | commit() | 163 | commit() | ||
| 164 | seqA := lastSeq() | 164 | seqA := lastSeq() | ||
| 165 | - | flip(5*time.Second+7, 42, seqA, true) | 165 | + | flip(5*time.Second+7, 42, seqA, w.ID) |
| 166 | ev := answer(a, "presented") | 166 | ev := answer(a, "presented") | ||
| 167 | if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 { | 167 | if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 { | ||
| 168 | t.Fatalf("presented %+v", ev) | 168 | t.Fatalf("presented %+v", ev) | ||
| @@ -178,9 +178,9 @@ func TestPresentationFeedback(t *testing.T) { | |||||
| 178 | d := feedback() | 178 | d := feedback() | ||
| 179 | commit() | 179 | commit() | ||
| 180 | seqD := lastSeq() | 180 | seqD := lastSeq() | ||
| 181 | - | flip(6*time.Second, 43, seqB, false) | 181 | + | flip(6*time.Second, 43, seqB, w.ID+1) // another window on the overlay |
| 182 | - | flip(7*time.Second, 44, seqD, false) | 182 | + | flip(7*time.Second, 44, seqD, 0) |
| 183 | - | if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 { | 183 | + | if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 || ev.flags&uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) != 0 { |
| 184 | t.Fatalf("first %+v", ev) | 184 | t.Fatalf("first %+v", ev) | ||
| 185 | } | 185 | } | ||
| 186 | if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 { | 186 | if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 { | ||
| @@ -192,7 +192,7 @@ func TestPresentationFeedback(t *testing.T) { | |||||
| 192 | lastSeq() | 192 | lastSeq() | ||
| 193 | commit() | 193 | commit() | ||
| 194 | seqNew := lastSeq() | 194 | seqNew := lastSeq() | ||
| 195 | - | flip(8*time.Second, 45, seqNew, false) | 195 | + | flip(8*time.Second, 45, seqNew, 0) |
| 196 | if ev := answer(old, "replaced"); !ev.discarded { | 196 | if ev := answer(old, "replaced"); !ev.discarded { | ||
| 197 | t.Fatalf("replaced: %+v", ev) | 197 | t.Fatalf("replaced: %+v", ev) | ||
| 198 | } | 198 | } | ||
| @@ -213,7 +213,7 @@ func TestPresentationFeedback(t *testing.T) { | |||||
| 213 | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | 213 | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | ||
| 214 | commit() // the parent applies the subsurface | 214 | commit() // the parent applies the subsurface | ||
| 215 | seqC := lastSeq() | 215 | seqC := lastSeq() | ||
| 216 | - | flip(8500*time.Millisecond, 50, seqC, false) | 216 | + | flip(8500*time.Millisecond, 50, seqC, 0) |
| 217 | if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 { | 217 | if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 { | ||
| 218 | t.Fatalf("subsurface %+v", ev) | 218 | t.Fatalf("subsurface %+v", ev) | ||
| 219 | } | 219 | } | ||
| @@ -222,7 +222,7 @@ func TestPresentationFeedback(t *testing.T) { | |||||
| 222 | commit() | 222 | commit() | ||
| 223 | lastSeq() | 223 | lastSeq() | ||
| 224 | requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy) | 224 | requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy) | ||
| 225 | - | flip(9*time.Second, 46, 0, false) | 225 | + | flip(9*time.Second, 46, 0, 0) |
| 226 | if ev := answer(gone, "destroyed"); !ev.discarded { | 226 | if ev := answer(gone, "destroyed"); !ev.discarded { | ||
| 227 | t.Fatalf("destroyed: %+v", ev) | 227 | t.Fatalf("destroyed: %+v", ev) | ||
| 228 | } | 228 | } | ||
M internal/adapters/wayland/syncobj_test.go +1 -1
| @@ -3,8 +3,8 @@ package wayland | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "os" | |
| 6 | - | "sync" | |
| 7 | 6 | "strings" | |
| 7 | + | "sync" | |
| 8 | 8 | "testing" | |
| 9 | 9 | "time" | |
| 10 | 10 | "unsafe" | |
| @@ -3,8 +3,8 @@ package wayland | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "os" | 5 | "os" | ||
| 6 | - | "sync" | |||
| 7 | "strings" | 6 | "strings" | ||
| 7 | + | "sync" | |||
| 8 | "testing" | 8 | "testing" | ||
| 9 | "time" | 9 | "time" | ||
| 10 | "unsafe" | 10 | "unsafe" | ||
M internal/ports/messages.go +6 -3
| @@ -361,8 +361,9 @@ type OutputFormats struct { | |||
| 361 | 361 | ||
| 362 | 362 | // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time | |
| 363 | 363 | // (hardware clock on DRM), Seq the output's vblank counter, Refresh the | |
| 364 | - | // refresh period (0 while variable refresh is on). ZeroCopy is set when a | |
| 365 | - | // client buffer was shown without composition, Async for a tearing flip. | |
| 364 | + | // refresh period (0 while variable refresh is on). ZeroCopy is the window | |
| 365 | + | // whose buffer was shown without composition (direct scanout or overlay | |
| 366 | + | // plane; 0: none), Async set for a tearing flip. | |
| 366 | 367 | // Shows is the content Seq per window the flipped frame shows. Merged | |
| 367 | 368 | // counts earlier flips folded into this one when the reader fell behind: | |
| 368 | 369 | // their presentation feedback is discarded. | |
| @@ -370,7 +371,7 @@ type FlipInfo struct { | |||
| 370 | 371 | When time.Duration | |
| 371 | 372 | Seq uint64 | |
| 372 | 373 | Refresh time.Duration | |
| 373 | - | ZeroCopy bool | |
| 374 | + | ZeroCopy WindowID | |
| 374 | 375 | Async bool | |
| 375 | 376 | HardwareClock bool | |
| 376 | 377 | Merged int | |
| @@ -655,6 +656,8 @@ type SurfaceContent struct { | |||
| 655 | 656 | // buffer, instead of the buffer's implicit fences. Wayland owns it and | |
| 656 | 657 | // closes it when the buffer is released; a reader that keeps it past | |
| 657 | 658 | // the call duplicates it under Acquire.SyscallConn. nil: implicit sync. | |
| 659 | + | // Wayland closes it only after every output reported a later content of | |
| 660 | + | // the window as seen, so a reader never gets it already closed. | |
| 658 | 661 | Acquire *os.File | |
| 659 | 662 | // DamageHistory is what changed in the root surface's buffer over the | |
| 660 | 663 | // window's last contents, oldest first, ending with this one (Seq). | |
| @@ -361,8 +361,9 @@ type OutputFormats struct { | |||||
| 361 | 361 | ||||
| 362 | // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time | 362 | // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time | ||
| 363 | // (hardware clock on DRM), Seq the output's vblank counter, Refresh the | 363 | // (hardware clock on DRM), Seq the output's vblank counter, Refresh the | ||
| 364 | - | // refresh period (0 while variable refresh is on). ZeroCopy is set when a | 364 | + | // refresh period (0 while variable refresh is on). ZeroCopy is the window |
| 365 | - | // client buffer was shown without composition, Async for a tearing flip. | 365 | + | // whose buffer was shown without composition (direct scanout or overlay |
| 366 | + | // plane; 0: none), Async set for a tearing flip. | |||
| 366 | // Shows is the content Seq per window the flipped frame shows. Merged | 367 | // Shows is the content Seq per window the flipped frame shows. Merged | ||
| 367 | // counts earlier flips folded into this one when the reader fell behind: | 368 | // counts earlier flips folded into this one when the reader fell behind: | ||
| 368 | // their presentation feedback is discarded. | 369 | // their presentation feedback is discarded. | ||
| @@ -370,7 +371,7 @@ type FlipInfo struct { | |||||
| 370 | When time.Duration | 371 | When time.Duration | ||
| 371 | Seq uint64 | 372 | Seq uint64 | ||
| 372 | Refresh time.Duration | 373 | Refresh time.Duration | ||
| 373 | - | ZeroCopy bool | 374 | + | ZeroCopy WindowID |
| 374 | Async bool | 375 | Async bool | ||
| 375 | HardwareClock bool | 376 | HardwareClock bool | ||
| 376 | Merged int | 377 | Merged int | ||
| @@ -655,6 +656,8 @@ type SurfaceContent struct { | |||||
| 655 | // buffer, instead of the buffer's implicit fences. Wayland owns it and | 656 | // buffer, instead of the buffer's implicit fences. Wayland owns it and | ||
| 656 | // closes it when the buffer is released; a reader that keeps it past | 657 | // closes it when the buffer is released; a reader that keeps it past | ||
| 657 | // the call duplicates it under Acquire.SyscallConn. nil: implicit sync. | 658 | // the call duplicates it under Acquire.SyscallConn. nil: implicit sync. | ||
| 659 | + | // Wayland closes it only after every output reported a later content of | |||
| 660 | + | // the window as seen, so a reader never gets it already closed. | |||
| 658 | Acquire *os.File | 661 | Acquire *os.File | ||
| 659 | // DamageHistory is what changed in the root surface's buffer over the | 662 | // DamageHistory is what changed in the root surface's buffer over the | ||
| 660 | // window's last contents, oldest first, ending with this one (Seq). | 663 | // window's last contents, oldest first, ending with this one (Seq). | ||