detached 7e8d744

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
9191
9292 // pendingFrame is what the pending frame commit shows.
9393 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
9899 }
99100
100101 // 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
561562 // flip from the composed image into scanout waits for vblank.
562563 async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync
563564 vrr := o.wantVRR(true)
564- f.queued, f.zeroCopy = c.DMABuf.ID, true
565+ f.queued, f.zeroCopy = c.DMABuf.ID, c.ID
565566 // Explicit sync: KMS waits on the client's acquire fence. Wayland may
566567 // close its file at any time: commit a duplicate taken while it is
567568 // 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
847848 }
848849 // The overlay buffer is on screen like a scanned-out one: it
849850 // 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
851852 err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov)
852853 if done != nil {
853854 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 bool94+ frame bool
95- queued uint6495+ queued uint64
96- zeroCopy, async bool96+ zeroCopy ports.WindowID // window shown without composition
97- shows map[ports.WindowID]uint6497+ async bool
98+ shows map[ports.WindowID]uint64
98 }99 }
99 100
100 // Commit event userData: the commit serial above userKindBits, the kind101 // 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.noAsync563 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, true565+ f.queued, f.zeroCopy = c.DMABuf.ID, c.ID
565 // Explicit sync: KMS waits on the client's acquire fence. Wayland may566 // 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 is567 // 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: it849 // 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 != 0851+ 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
161161 if f.HardwareClock {
162162 flags |= uint32(presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion)
163163 }
164- if f.ZeroCopy {
164+ if f.ZeroCopy != 0 && f.ZeroCopy == w.win {
165165 flags |= uint32(presentationtime.WpPresentationFeedbackKindZeroCopy)
166166 }
167167 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) {
155155 t.Fatal(err)
156156 }
157157 }
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) {
159159 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}}}
160160 }
161161 // Presented with the flip's time, counter and flags.
162162 a := feedback()
163163 commit()
164164 seqA := lastSeq()
165- flip(5*time.Second+7, 42, seqA, true)
165+ flip(5*time.Second+7, 42, seqA, w.ID)
166166 ev := answer(a, "presented")
167167 if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 {
168168 t.Fatalf("presented %+v", ev)
@@ -178,9 +178,9 @@ func TestPresentationFeedback(t *testing.T) {
178178 d := feedback()
179179 commit()
180180 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 {
184184 t.Fatalf("first %+v", ev)
185185 }
186186 if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 {
@@ -192,7 +192,7 @@ func TestPresentationFeedback(t *testing.T) {
192192 lastSeq()
193193 commit()
194194 seqNew := lastSeq()
195- flip(8*time.Second, 45, seqNew, false)
195+ flip(8*time.Second, 45, seqNew, 0)
196196 if ev := answer(old, "replaced"); !ev.discarded {
197197 t.Fatalf("replaced: %+v", ev)
198198 }
@@ -213,7 +213,7 @@ func TestPresentationFeedback(t *testing.T) {
213213 requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
214214 commit() // the parent applies the subsurface
215215 seqC := lastSeq()
216- flip(8500*time.Millisecond, 50, seqC, false)
216+ flip(8500*time.Millisecond, 50, seqC, 0)
217217 if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 {
218218 t.Fatalf("subsurface %+v", ev)
219219 }
@@ -222,7 +222,7 @@ func TestPresentationFeedback(t *testing.T) {
222222 commit()
223223 lastSeq()
224224 requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
225- flip(9*time.Second, 46, 0, false)
225+ flip(9*time.Second, 46, 0, 0)
226226 if ev := answer(gone, "destroyed"); !ev.discarded {
227227 t.Fatalf("destroyed: %+v", ev)
228228 }
@@ -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 subsurface214 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
33 import (
44 "context"
55 "os"
6- "sync"
76 "strings"
7+ "sync"
88 "testing"
99 "time"
1010 "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 {
361361
362362 // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time
363363 // (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.
366367 // Shows is the content Seq per window the flipped frame shows. Merged
367368 // counts earlier flips folded into this one when the reader fell behind:
368369 // their presentation feedback is discarded.
@@ -370,7 +371,7 @@ type FlipInfo struct {
370371 When time.Duration
371372 Seq uint64
372373 Refresh time.Duration
373- ZeroCopy bool
374+ ZeroCopy WindowID
374375 Async bool
375376 HardwareClock bool
376377 Merged int
@@ -655,6 +656,8 @@ type SurfaceContent struct {
655656 // buffer, instead of the buffer's implicit fences. Wayland owns it and
656657 // closes it when the buffer is released; a reader that keeps it past
657658 // 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.
658661 Acquire *os.File
659662 // DamageHistory is what changed in the root surface's buffer over the
660663 // 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 time362 // FlipInfo is one completed page flip. When is its CLOCK_MONOTONIC time
363 // (hardware clock on DRM), Seq the output's vblank counter, Refresh the363 // (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 a364+// 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. Merged367 // 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.Duration371 When time.Duration
371 Seq uint64372 Seq uint64
372 Refresh time.Duration373 Refresh time.Duration
373- ZeroCopy bool374+ ZeroCopy WindowID
374 Async bool375 Async bool
375 HardwareClock bool376 HardwareClock bool
376 Merged int377 Merged int
@@ -655,6 +656,8 @@ type SurfaceContent struct {
655 // buffer, instead of the buffer's implicit fences. Wayland owns it and656 // 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 past657 // 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.File661 Acquire *os.File
659 // DamageHistory is what changed in the root surface's buffer over the662 // 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).

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 26

Tags 10

↑↓ moveenter openesc close