detached 4a9b1d1

test(wayland,vulkan): cover relocation tail clearing and mixed deferred frees

TestRelocateCallbacksClearsFilteredTail checks that a callback moved to another queue leaves no pointer in the kept queue's backing array. TestRetireMixedDeferredEntries checks that a completed frame's semaphore is destroyed at once and that semaphore and closure entries are freed in order as their frames complete.
commit
4a9b1d177c983408bfd0505e48042fdf11cce5a0
author
brice · · 2026-10-16 14:19 UTC
parent
dff8f48
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +56 -0
M internal/adapters/vulkan/scene_perf_test.go +32 -0
@@ -213,6 +213,38 @@ func TestRetireSemaphoreAllocatesNothing(t *testing.T) {
213213 }
214214 }
215215
216+// Semaphore and closure entries share the deferred list: each is freed once
217+// its frame completes, in order, and a semaphore whose frame already
218+// completed is destroyed at once instead of queued.
219+func TestRetireMixedDeferredEntries(t *testing.T) {
220+ r, err := New(32, 32)
221+ if err != nil {
222+ t.Skipf("Vulkan unavailable: %v", err)
223+ }
224+ defer r.Close()
225+ r.runDeferred()
226+ r.completed = 5
227+ // A null handle is a valid vkDestroySemaphore argument.
228+ r.retireSemaphore(5, 0)
229+ if len(r.deferred) != 0 {
230+ t.Fatalf("completed frame's semaphore queued: %d entries", len(r.deferred))
231+ }
232+ var freed []string
233+ r.retire(7, func() { freed = append(freed, "late") })
234+ r.retireSemaphore(6, 0)
235+ r.retire(6, func() { freed = append(freed, "early") })
236+ r.completed = 6
237+ r.runDeferred()
238+ if !slices.Equal(freed, []string{"early"}) || len(r.deferred) != 1 || r.deferred[0].frame != 7 {
239+ t.Fatalf("frame 6: freed %v, %d entries left", freed, len(r.deferred))
240+ }
241+ r.completed = 7
242+ r.runDeferred()
243+ if !slices.Equal(freed, []string{"early", "late"}) || len(r.deferred) != 0 {
244+ t.Fatalf("frame 7: freed %v, %d entries left", freed, len(r.deferred))
245+ }
246+}
247+
216248 // TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged
217249 // subsurface tree. Each child has its own real wl_shm pool.
218250 func TestSceneWalkUnchangedTiledSHM(t *testing.T) {
@@ -213,6 +213,38 @@ func TestRetireSemaphoreAllocatesNothing(t *testing.T) {
213 }213 }
214 }214 }
215 215
216+// Semaphore and closure entries share the deferred list: each is freed once
217+// its frame completes, in order, and a semaphore whose frame already
218+// completed is destroyed at once instead of queued.
219+func TestRetireMixedDeferredEntries(t *testing.T) {
220+ r, err := New(32, 32)
221+ if err != nil {
222+ t.Skipf("Vulkan unavailable: %v", err)
223+ }
224+ defer r.Close()
225+ r.runDeferred()
226+ r.completed = 5
227+ // A null handle is a valid vkDestroySemaphore argument.
228+ r.retireSemaphore(5, 0)
229+ if len(r.deferred) != 0 {
230+ t.Fatalf("completed frame's semaphore queued: %d entries", len(r.deferred))
231+ }
232+ var freed []string
233+ r.retire(7, func() { freed = append(freed, "late") })
234+ r.retireSemaphore(6, 0)
235+ r.retire(6, func() { freed = append(freed, "early") })
236+ r.completed = 6
237+ r.runDeferred()
238+ if !slices.Equal(freed, []string{"early"}) || len(r.deferred) != 1 || r.deferred[0].frame != 7 {
239+ t.Fatalf("frame 6: freed %v, %d entries left", freed, len(r.deferred))
240+ }
241+ r.completed = 7
242+ r.runDeferred()
243+ if !slices.Equal(freed, []string{"early", "late"}) || len(r.deferred) != 0 {
244+ t.Fatalf("frame 7: freed %v, %d entries left", freed, len(r.deferred))
245+ }
246+}
247+
216 // TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged248 // TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged
217 // subsurface tree. Each child has its own real wl_shm pool.249 // subsurface tree. Each child has its own real wl_shm pool.
218 func TestSceneWalkUnchangedTiledSHM(t *testing.T) {250 func TestSceneWalkUnchangedTiledSHM(t *testing.T) {
M internal/adapters/wayland/frames_test.go +24 -0
@@ -144,3 +144,27 @@ func TestCapturedWindowKeepsOutputFramePacing(t *testing.T) {
144144 t.Fatalf("released: queues %+v", s.awaiting)
145145 }
146146 }
147+
148+// Relocation filters a queue in place: the callbacks that move away leave
149+// no pointer in the kept queue's backing array.
150+func TestRelocateCallbacksClearsFilteredTail(t *testing.T) {
151+ out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
152+ s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}}
153+ hidden := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}}
154+ shown := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}}
155+ root := &surface{xdg: &xdgSurface{window: hidden}}
156+ other := &surface{xdg: &xdgSurface{window: shown}}
157+ moving, staying := new(wayland.Callback), new(wayland.Callback)
158+ s.queueFrames(root, []*wayland.Callback{moving})
159+ s.queueFrames(other, []*wayland.Callback{staying})
160+ before := s.awaiting["DP-2"]
161+ hidden.last.Visible = false
162+ s.relocateCallbacks(root)
163+ kept := s.awaiting["DP-2"]
164+ if len(kept) != 1 || kept[0] != staying || len(s.awaiting[suspendedFrameQueue]) != 1 {
165+ t.Fatalf("relocation: queues %+v", s.awaiting)
166+ }
167+ if tail := before[:2][1]; tail != nil {
168+ t.Error("the filtered tail still pins the moved callback")
169+ }
170+}
@@ -144,3 +144,27 @@ func TestCapturedWindowKeepsOutputFramePacing(t *testing.T) {
144 t.Fatalf("released: queues %+v", s.awaiting)144 t.Fatalf("released: queues %+v", s.awaiting)
145 }145 }
146 }146 }
147+
148+// Relocation filters a queue in place: the callbacks that move away leave
149+// no pointer in the kept queue's backing array.
150+func TestRelocateCallbacksClearsFilteredTail(t *testing.T) {
151+ out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
152+ s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}}
153+ hidden := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}}
154+ shown := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}}
155+ root := &surface{xdg: &xdgSurface{window: hidden}}
156+ other := &surface{xdg: &xdgSurface{window: shown}}
157+ moving, staying := new(wayland.Callback), new(wayland.Callback)
158+ s.queueFrames(root, []*wayland.Callback{moving})
159+ s.queueFrames(other, []*wayland.Callback{staying})
160+ before := s.awaiting["DP-2"]
161+ hidden.last.Visible = false
162+ s.relocateCallbacks(root)
163+ kept := s.awaiting["DP-2"]
164+ if len(kept) != 1 || kept[0] != staying || len(s.awaiting[suspendedFrameQueue]) != 1 {
165+ t.Fatalf("relocation: queues %+v", s.awaiting)
166+ }
167+ if tail := before[:2][1]; tail != nil {
168+ t.Error("the filtered tail still pins the moved callback")
169+ }
170+}

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 23

Tags 10

↑↓ moveenter openesc close