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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 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 | 248 | // TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged | |
| 217 | 249 | // subsurface tree. Each child has its own real wl_shm pool. | |
| 218 | 250 | 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 unchanged | 248 | // 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) { | |||
| 144 | 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 | + | } | |
| @@ -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 | + | } | |||